update the chat to use the new chat, fix file uploads, mentions, and message area scaling
This commit is contained in:
150
src/components/CompanyWiki/CompanyWikiCompletedState.tsx
Normal file
150
src/components/CompanyWiki/CompanyWikiCompletedState.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
|
||||
interface QAItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface CompanyWikiCompletedStateProps {
|
||||
qaItems?: QAItem[];
|
||||
activeSection?: number;
|
||||
onSectionClick?: (section: number) => void;
|
||||
onInviteEmployees?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
}
|
||||
|
||||
const defaultQAItems: QAItem[] = [
|
||||
{
|
||||
question: "What is the mission of your company?",
|
||||
answer: "To empower small businesses with AI-driven automation tools that increase efficiency and reduce operational overhead."
|
||||
},
|
||||
{
|
||||
question: "How has your mission evolved in the last 1–3 years?",
|
||||
answer: "We shifted from general SaaS tools to vertical-specific solutions, with deeper integrations and onboarding support."
|
||||
},
|
||||
{
|
||||
question: "What is your 5-year vision for the company?",
|
||||
answer: "To become the leading AI operations platform for SMBs in North America, serving over 100,000 customers."
|
||||
},
|
||||
{
|
||||
question: "What are your company's top 3 strategic advantages?",
|
||||
answer: "Fast product iteration enabled by in-house AI capabilities\nDeep customer understanding from vertical specialization\nHigh customer retention due to integrated onboarding"
|
||||
},
|
||||
{
|
||||
question: "What are your biggest vulnerabilities or threats?",
|
||||
answer: "Dependence on a single marketing channel, weak middle management, and rising customer acquisition costs."
|
||||
}
|
||||
];
|
||||
|
||||
const sections = [
|
||||
"Company Overview & Vision",
|
||||
"Leadership & Organizational Structure",
|
||||
"Operations & Execution",
|
||||
"Culture & Team Health",
|
||||
"Sales, Marketing & Growth",
|
||||
"Financial Health & Metrics",
|
||||
"Innovation & Product/Service Strategy",
|
||||
"Personal Leadership & Risk"
|
||||
];
|
||||
|
||||
export const CompanyWikiCompletedState: React.FC<CompanyWikiCompletedStateProps> = ({
|
||||
qaItems = defaultQAItems,
|
||||
activeSection = 1,
|
||||
onSectionClick,
|
||||
onInviteEmployees,
|
||||
onCopyLink
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 self-stretch inline-flex justify-start items-center">
|
||||
{/* Table of Contents */}
|
||||
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-Outline-Outline-Gray-200 dark:border-Neutrals-NeutralSlate200 inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate950 dark:text-Neutrals-NeutralSlate50 text-base font-medium font-['Inter'] leading-normal">Table of contents</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 flex flex-col justify-start items-start gap-1.5">
|
||||
{sections.map((section, index) => {
|
||||
const sectionNumber = index + 1;
|
||||
const isActive = sectionNumber === activeSection;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => onSectionClick?.(sectionNumber)}
|
||||
className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden cursor-pointer hover:bg-Main-BG-Gray-50 dark:hover:bg-Neutrals-NeutralSlate700 ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-Neutrals-NeutralSlate800 shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : ''}`}
|
||||
>
|
||||
<div className={`h-5 p-0.5 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${isActive ? 'bg-Brand-Orange' : 'bg-Text-Gray-100 dark:bg-Neutrals-NeutralSlate600'}`}>
|
||||
<div className={`w-4 text-center justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-Neutrals-NeutralSlate0' : 'text-Text-Dark-950 dark:text-Neutrals-NeutralSlate200'}`}>
|
||||
{sectionNumber}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex-1 justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-Neutrals-NeutralSlate800 dark:text-Neutrals-NeutralSlate100' : 'text-Text-Gray-500 dark:text-Neutrals-NeutralSlate400'}`}>
|
||||
{section}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 self-stretch inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 dark:text-Neutrals-NeutralSlate100 text-xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{sections[activeSection - 1]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch px-5 flex flex-col justify-start items-start gap-4">
|
||||
{qaItems.map((item, index) => (
|
||||
<div key={index} className="self-stretch p-3 bg-Neutrals-NeutralSlate100 dark:bg-Neutrals-NeutralSlate800 rounded-2xl shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-2 overflow-hidden">
|
||||
<div className="self-stretch px-3 py-px inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 flex justify-center items-center gap-3">
|
||||
<div className="w-3 self-stretch justify-start text-Neutrals-NeutralSlate300 dark:text-Neutrals-NeutralSlate500 text-base font-semibold font-['Inter'] leading-normal">Q</div>
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate600 dark:text-Neutrals-NeutralSlate300 text-sm font-medium font-['Inter'] leading-tight">
|
||||
{item.question}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 py-2 bg-Neutrals-NeutralSlate0 dark:bg-Neutrals-NeutralSlate900 rounded-[10px] inline-flex justify-between items-center">
|
||||
<div className="flex-1 flex justify-start items-start gap-3">
|
||||
<div className="w-3.5 h-6 justify-center text-Neutrals-NeutralSlate300 dark:text-Neutrals-NeutralSlate500 text-base font-semibold font-['Inter'] leading-normal">A</div>
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 dark:text-Neutrals-NeutralSlate100 text-base font-normal font-['Inter'] leading-normal whitespace-pre-line">
|
||||
{item.answer}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Additional Questions */}
|
||||
<div className="self-stretch pl-3 pr-6 pt-3 pb-6 bg-Text-Gray-100 rounded-2xl shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-2 overflow-hidden">
|
||||
<div className="self-stretch inline-flex justify-center items-center gap-3">
|
||||
<div className="flex-1 justify-start text-Text-Gray-600 text-sm font-medium font-['Inter'] leading-tight">What is the mission of your company?</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 py-2 bg-Light-Grays-l-gray08 rounded-[10px] outline outline-1 outline-offset-[-1px] outline-Text-Gray-200 inline-flex justify-between items-center">
|
||||
<div className="flex-1 flex justify-start items-start gap-3">
|
||||
<div className="flex-1 justify-start text-Text-Gray-800 text-base font-normal font-['Inter'] leading-normal">
|
||||
Our mission is to not only create value but also to foster a collaborative environment where innovation thrives. We aim to empower our team members to contribute their unique skills and perspectives, ensuring that every project we undertake is a reflection of our collective creativity and dedication. By prioritizing both individual growth and teamwork, we strive to build a company culture that values excellence and continuous improvement. Together, we can achieve remarkable results that benefit not just our organization, but also our clients and the community at...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-stretch p-3 bg-Neutrals-NeutralSlate100 rounded-2xl shadow-[0px_1px_2px_0px_rgba(0,0,0,0.02)] flex flex-col justify-center items-start gap-2 overflow-hidden">
|
||||
<div className="self-stretch px-3 py-px inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 flex justify-center items-center gap-3">
|
||||
<div className="w-3 self-stretch justify-start text-Neutrals-NeutralSlate300 text-base font-semibold font-['Inter'] leading-normal">Q</div>
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate600 text-sm font-medium font-['Inter'] leading-tight">What is the mission of your company?</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 py-2 bg-Light-Grays-l-gray08 rounded-[10px] inline-flex justify-between items-center">
|
||||
<div className="flex-1 flex justify-start items-start gap-3">
|
||||
<div className="w-3.5 h-6 justify-center text-Neutrals-NeutralSlate300 text-base font-semibold font-['Inter'] leading-normal">A</div>
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 text-base font-normal font-['Inter'] leading-normal">The mission is to create value as well as working</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
119
src/components/CompanyWiki/CompanyWikiEmptyState.tsx
Normal file
119
src/components/CompanyWiki/CompanyWikiEmptyState.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CompanyWikiEmptyStateProps {
|
||||
progress?: number;
|
||||
onCompleteOnboarding?: () => void;
|
||||
onInviteEmployees?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
}
|
||||
|
||||
export const CompanyWikiEmptyState: React.FC<CompanyWikiEmptyStateProps> = ({
|
||||
progress = 60,
|
||||
onCompleteOnboarding,
|
||||
onInviteEmployees,
|
||||
onCopyLink
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 self-stretch inline-flex justify-start items-center">
|
||||
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-Outline-Outline-Gray-200 inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate950 text-base font-medium font-['Inter'] leading-normal">Table of contents</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 flex flex-col justify-start items-start gap-1.5">
|
||||
<div className="self-stretch p-2 bg-Main-BG-Gray-100 rounded-full shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Brand-Orange rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Neutrals-NeutralSlate0 text-xs font-medium font-['Inter'] leading-none">1</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 text-xs font-medium font-['Inter'] leading-none">Company Overview & Vision</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">2</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Leadership & Organizational Structure</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">3</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Operations & Execution</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">4</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Culture & Team Health</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">5</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Sales, Marketing & Growth</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">6</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Financial Health & Metrics</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">7</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Innovation & Product/Service Strategy</div>
|
||||
</div>
|
||||
<div className="self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<div className="h-5 p-0.5 bg-Text-Gray-100 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="w-4 text-center justify-start text-Text-Dark-950 text-xs font-normal font-['Inter'] leading-none">8</div>
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-Text-Gray-500 text-xs font-medium font-['Inter'] leading-none">Personal Leadership & Risk</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 self-stretch inline-flex flex-col justify-center items-center p-8">
|
||||
{/* Empty State Illustration */}
|
||||
<div className="w-80 h-64 mb-8 relative">
|
||||
{/* Placeholder for illustration - would contain the actual empty state SVG */}
|
||||
<div className="w-full h-full bg-Neutrals-NeutralSlate100 rounded-2xl flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-Neutrals-NeutralSlate200 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 30V18.6667C12 17.3867 12 16.7467 12.1453 16.248C12.2731 15.8071 12.5171 15.4109 12.848 15.1053C13.2133 14.7667 13.7066 14.6667 14.6933 14.6667H17.3067C18.2934 14.6667 18.7867 14.7667 19.152 15.1053C19.4829 15.4109 19.7269 15.8071 19.8547 16.248C20 16.7467 20 17.3867 20 18.6667V30M14.6903 3.68533L6.04715 11.5188C5.44269 12.0684 5.14047 12.3431 4.92271 12.6778C4.73015 12.9739 4.58613 13.3073 4.49871 13.6608C4.4 14.0575 4.4 14.4803 4.4 15.3261V23.7333C4.4 25.2267 4.4 25.9733 4.69065 26.544C4.94631 27.0458 5.35421 27.4537 5.85603 27.7093C6.42669 28 7.17323 28 8.66667 28H23.3333C24.8268 28 25.5733 28 26.144 27.7093C26.6458 27.4537 27.0537 27.0458 27.3093 26.544C27.6 25.9733 27.6 25.2267 27.6 23.7333V15.3261C27.6 14.4803 27.6 14.0575 27.5013 13.6608C27.4139 13.3073 27.2699 12.9739 27.0773 12.6778C26.8595 12.3431 26.5573 12.0684 25.9529 11.5188L17.3097 3.68533C16.8413 3.27241 16.6071 3.06595 16.3485 2.98821C16.1203 2.9184 15.8797 2.9184 15.6515 2.98821C15.3929 3.06595 15.1587 3.27241 14.6903 3.68533Z" stroke="var(--Neutrals-NeutralSlate400)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-Neutrals-NeutralSlate600 text-sm">Company Wiki Empty State</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress and Call to Action */}
|
||||
<div className="text-center max-w-md">
|
||||
<div className="mb-6">
|
||||
<div className="text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal mb-2">
|
||||
You're {progress}% Done
|
||||
</div>
|
||||
<div className="text-Neutrals-NeutralSlate600 text-base leading-normal">
|
||||
Complete your onboarding to unlock your Company Wiki
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full bg-Neutrals-NeutralSlate200 rounded-full h-2 mb-8">
|
||||
<div
|
||||
className="bg-Brand-Orange h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<button
|
||||
onClick={onCompleteOnboarding}
|
||||
className="px-8 py-3 bg-Brand-Orange text-white rounded-[999px] font-medium text-base hover:bg-Brand-Orange/90 transition-colors"
|
||||
>
|
||||
Complete Onboarding
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
110
src/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx
Normal file
110
src/components/CompanyWiki/CompanyWikiEmptyStateDark.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CompanyWikiEmptyStateProps {
|
||||
progress?: number;
|
||||
onCompleteOnboarding?: () => void;
|
||||
onInviteEmployees?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
}
|
||||
|
||||
const sections = [
|
||||
"Company Overview & Vision",
|
||||
"Leadership & Organizational Structure",
|
||||
"Operations & Execution",
|
||||
"Culture & Team Health",
|
||||
"Sales, Marketing & Growth",
|
||||
"Financial Health & Metrics",
|
||||
"Innovation & Product/Service Strategy",
|
||||
"Personal Leadership & Risk"
|
||||
];
|
||||
|
||||
export const CompanyWikiEmptyState: React.FC<CompanyWikiEmptyStateProps> = ({
|
||||
progress = 60,
|
||||
onCompleteOnboarding,
|
||||
onInviteEmployees,
|
||||
onCopyLink
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 self-stretch inline-flex justify-start items-center">
|
||||
{/* Table of Contents */}
|
||||
<div className="flex-1 self-stretch max-w-64 min-w-64 border-r border-Outline-Outline-Gray-200 dark:border-Neutrals-NeutralSlate200 inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate950 dark:text-Neutrals-NeutralSlate50 text-base font-medium font-['Inter'] leading-normal">Table of contents</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 flex flex-col justify-start items-start gap-1.5">
|
||||
{sections.map((section, index) => {
|
||||
const sectionNumber = index + 1;
|
||||
const isActive = sectionNumber === 1; // First section is always active in empty state
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`self-stretch p-2 rounded-[10px] inline-flex justify-start items-center gap-2 overflow-hidden ${isActive ? 'bg-Main-BG-Gray-100 dark:bg-Neutrals-NeutralSlate800 shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]' : 'hover:bg-Main-BG-Gray-50 dark:hover:bg-Neutrals-NeutralSlate700'}`}
|
||||
>
|
||||
<div className={`h-5 p-0.5 rounded-[999px] inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${isActive ? 'bg-Brand-Orange' : 'bg-Text-Gray-100 dark:bg-Neutrals-NeutralSlate600'}`}>
|
||||
<div className={`w-4 text-center justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-Neutrals-NeutralSlate0' : 'text-Text-Dark-950 dark:text-Neutrals-NeutralSlate200'}`}>
|
||||
{sectionNumber}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex-1 justify-start text-xs font-medium font-['Inter'] leading-none ${isActive ? 'text-Neutrals-NeutralSlate800 dark:text-Neutrals-NeutralSlate100' : 'text-Text-Gray-500 dark:text-Neutrals-NeutralSlate400'}`}>
|
||||
{section}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 self-stretch inline-flex flex-col justify-start items-start">
|
||||
<div className="self-stretch p-5 inline-flex justify-start items-center gap-2.5">
|
||||
<div className="flex-1 justify-start text-Neutrals-NeutralSlate800 dark:text-Neutrals-NeutralSlate100 text-xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
Company Overview & Vision
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch flex-1 p-5 flex justify-center items-center">
|
||||
<div className="w-[440px] flex flex-col justify-center items-center gap-8">
|
||||
{/* Progress Illustration Placeholder */}
|
||||
<div className="w-[280px] h-[200px] bg-Text-Gray-100 dark:bg-Neutrals-NeutralSlate700 rounded-2xl flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-Brand-Orange rounded-full flex items-center justify-center">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" className="text-white">
|
||||
<path d="M16 8v8l4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="16" cy="16" r="12" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-Text-Gray-600 dark:text-Neutrals-NeutralSlate300 text-sm">Progress Illustration</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Content */}
|
||||
<div className="self-stretch flex flex-col justify-center items-center gap-4 text-center">
|
||||
<div className="text-Neutrals-NeutralSlate800 dark:text-Neutrals-NeutralSlate100 text-2xl font-semibold font-['Neue_Montreal'] leading-8">
|
||||
You're {progress}% Done
|
||||
</div>
|
||||
<div className="self-stretch text-Text-Gray-600 dark:text-Neutrals-NeutralSlate300 text-base font-normal font-['Inter'] leading-normal">
|
||||
Complete your company onboarding to unlock your company wiki and comprehensive insights about your organization.
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="self-stretch h-2 bg-Text-Gray-100 dark:bg-Neutrals-NeutralSlate700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-Brand-Orange rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<button
|
||||
onClick={onCompleteOnboarding}
|
||||
className="w-full px-6 py-3 bg-Brand-Orange hover:bg-orange-600 text-Neutrals-NeutralSlate0 text-base font-medium font-['Inter'] leading-normal rounded-xl transition-colors"
|
||||
>
|
||||
Complete Onboarding
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
116
src/components/CompanyWiki/CompanyWikiManager.tsx
Normal file
116
src/components/CompanyWiki/CompanyWikiManager.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CompanyWikiEmptyState } from './CompanyWikiEmptyState';
|
||||
import { CompanyWikiCompletedState } from './CompanyWikiCompletedState';
|
||||
import { InviteEmployeesModal } from './InviteEmployeesModal';
|
||||
import { InviteMultipleEmployeesModal } from './InviteMultipleEmployeesModal';
|
||||
|
||||
export type WikiState = 'empty' | 'completed';
|
||||
export type InviteModalState = 'none' | 'single' | 'multiple';
|
||||
|
||||
interface Employee {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface CompanyWikiManagerProps {
|
||||
initialState?: WikiState;
|
||||
onboardingProgress?: number;
|
||||
onCompleteOnboarding?: () => void;
|
||||
qaItems?: Array<{ question: string; answer: string }>;
|
||||
suggestedEmployees?: Employee[];
|
||||
}
|
||||
|
||||
export const CompanyWikiManager: React.FC<CompanyWikiManagerProps> = ({
|
||||
initialState = 'empty',
|
||||
onboardingProgress = 60,
|
||||
onCompleteOnboarding,
|
||||
qaItems,
|
||||
suggestedEmployees
|
||||
}) => {
|
||||
const [wikiState, setWikiState] = useState<WikiState>(initialState);
|
||||
const [inviteModalState, setInviteModalState] = useState<InviteModalState>('none');
|
||||
const [activeSection, setActiveSection] = useState(1);
|
||||
|
||||
const handleCompleteOnboarding = () => {
|
||||
onCompleteOnboarding?.();
|
||||
setWikiState('completed');
|
||||
};
|
||||
|
||||
const handleInviteEmployee = (email: string) => {
|
||||
console.log('Inviting employee:', email);
|
||||
// Here you would typically call an API to send the invitation
|
||||
setInviteModalState('none');
|
||||
// You could show a success toast here
|
||||
};
|
||||
|
||||
const handleInviteMultipleEmployees = (employees: Employee[]) => {
|
||||
console.log('Inviting multiple employees:', employees);
|
||||
// Here you would typically call an API to send multiple invitations
|
||||
setInviteModalState('none');
|
||||
// You could show a success toast here
|
||||
};
|
||||
|
||||
const handleSectionClick = (section: number) => {
|
||||
setActiveSection(section);
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
// Copy wiki link to clipboard
|
||||
const wikiUrl = `${window.location.origin}/#/company-wiki`;
|
||||
navigator.clipboard.writeText(wikiUrl).then(() => {
|
||||
console.log('Wiki link copied to clipboard');
|
||||
// You could show a success toast here
|
||||
});
|
||||
};
|
||||
|
||||
const renderWikiContent = () => {
|
||||
switch (wikiState) {
|
||||
case 'empty':
|
||||
return (
|
||||
<CompanyWikiEmptyState
|
||||
progress={onboardingProgress}
|
||||
onCompleteOnboarding={handleCompleteOnboarding}
|
||||
onInviteEmployees={() => setInviteModalState('single')}
|
||||
onCopyLink={handleCopyLink}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'completed':
|
||||
return (
|
||||
<CompanyWikiCompletedState
|
||||
qaItems={qaItems}
|
||||
activeSection={activeSection}
|
||||
onSectionClick={handleSectionClick}
|
||||
onInviteEmployees={() => setInviteModalState('single')}
|
||||
onCopyLink={handleCopyLink}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
{renderWikiContent()}
|
||||
|
||||
{/* Modals */}
|
||||
<InviteEmployeesModal
|
||||
isOpen={inviteModalState === 'single'}
|
||||
onClose={() => setInviteModalState('none')}
|
||||
onInvite={handleInviteEmployee}
|
||||
onMultipleInvite={() => setInviteModalState('multiple')}
|
||||
/>
|
||||
|
||||
<InviteMultipleEmployeesModal
|
||||
isOpen={inviteModalState === 'multiple'}
|
||||
onClose={() => setInviteModalState('none')}
|
||||
onInviteSelected={handleInviteMultipleEmployees}
|
||||
suggestedEmployees={suggestedEmployees}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
92
src/components/CompanyWiki/InviteEmployeesModal.tsx
Normal file
92
src/components/CompanyWiki/InviteEmployeesModal.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface InviteEmployeesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onInvite: (email: string) => void;
|
||||
onMultipleInvite?: () => void;
|
||||
}
|
||||
|
||||
export const InviteEmployeesModal: React.FC<InviteEmployeesModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onInvite,
|
||||
onMultipleInvite
|
||||
}) => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email.trim()) {
|
||||
onInvite(email.trim());
|
||||
setEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="w-[420px] bg-Neutrals-NeutralSlate0 dark:bg-Neutrals-NeutralSlate900 rounded-3xl shadow-[0px_25px_50px_-12px_rgba(0,0,0,0.25)] flex flex-col justify-start items-start overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="self-stretch p-6 inline-flex justify-between items-center">
|
||||
<div className="flex justify-start items-center gap-2.5">
|
||||
<div className="justify-start text-Text-Dark-950 dark:text-Neutrals-NeutralSlate50 text-lg font-semibold font-['Inter'] leading-7">Invite employees</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-6 h-6 flex justify-center items-center hover:bg-Text-Gray-100 dark:hover:bg-Neutrals-NeutralSlate700 rounded"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M13 1L1 13M1 1L13 13" stroke="#666D80" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="self-stretch px-6 flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 dark:text-Neutrals-NeutralSlate50 text-sm font-medium font-['Inter'] leading-tight">
|
||||
Email
|
||||
</div>
|
||||
<div className="self-stretch h-10 px-3 py-2 bg-Neutrals-NeutralSlate0 dark:bg-Neutrals-NeutralSlate800 rounded-lg border border-Outline-Outline-Gray-300 dark:border-Neutrals-NeutralSlate600 inline-flex justify-start items-center gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter email address"
|
||||
className="flex-1 text-Neutrals-NeutralSlate950 dark:text-Neutrals-NeutralSlate50 text-sm font-normal font-['Inter'] leading-tight bg-transparent outline-none placeholder:text-Text-Gray-500 dark:placeholder:text-Neutrals-NeutralSlate400"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="self-stretch p-6 inline-flex justify-between items-center">
|
||||
<button
|
||||
onClick={onMultipleInvite}
|
||||
className="text-Brand-Orange text-sm font-medium font-['Inter'] leading-tight hover:underline"
|
||||
>
|
||||
Invite multiple employees
|
||||
</button>
|
||||
<div className="flex justify-start items-start gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-Neutrals-NeutralSlate0 dark:bg-Neutrals-NeutralSlate800 rounded-lg border border-Outline-Outline-Gray-300 dark:border-Neutrals-NeutralSlate600 text-Neutrals-NeutralSlate700 dark:text-Neutrals-NeutralSlate200 text-sm font-medium font-['Inter'] leading-tight hover:bg-Text-Gray-50 dark:hover:bg-Neutrals-NeutralSlate700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!email.trim()}
|
||||
className="px-4 py-2 bg-Brand-Orange rounded-lg text-Neutrals-NeutralSlate0 text-sm font-medium font-['Inter'] leading-tight hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send Invite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
178
src/components/CompanyWiki/InviteMultipleEmployeesModal.tsx
Normal file
178
src/components/CompanyWiki/InviteMultipleEmployeesModal.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Employee {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface InviteMultipleEmployeesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onInviteSelected: (employees: Employee[]) => void;
|
||||
suggestedEmployees?: Employee[];
|
||||
}
|
||||
|
||||
const defaultSuggestedEmployees: Employee[] = [
|
||||
{ id: '1', name: 'John Smith', email: 'john@company.com' },
|
||||
{ id: '2', name: 'Sarah Johnson', email: 'sarah@company.com' },
|
||||
{ id: '3', name: 'Mike Chen', email: 'mike@company.com' },
|
||||
{ id: '4', name: 'Emily Davis', email: 'emily@company.com' },
|
||||
{ id: '5', name: 'Alex Rodriguez', email: 'alex@company.com' },
|
||||
];
|
||||
|
||||
export const InviteMultipleEmployeesModal: React.FC<InviteMultipleEmployeesModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onInviteSelected,
|
||||
suggestedEmployees = defaultSuggestedEmployees
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedEmployees, setSelectedEmployees] = useState<Employee[]>([]);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const filteredEmployees = suggestedEmployees.filter(emp =>
|
||||
(emp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
emp.email.toLowerCase().includes(searchTerm.toLowerCase())) &&
|
||||
!selectedEmployees.find(selected => selected.id === emp.id)
|
||||
);
|
||||
|
||||
const handleEmployeeSelect = (employee: Employee) => {
|
||||
setSelectedEmployees(prev => [...prev, employee]);
|
||||
setSearchTerm('');
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
const handleEmployeeRemove = (employeeId: string) => {
|
||||
setSelectedEmployees(prev => prev.filter(emp => emp.id !== employeeId));
|
||||
};
|
||||
|
||||
const handleInvite = () => {
|
||||
if (selectedEmployees.length > 0) {
|
||||
onInviteSelected(selectedEmployees);
|
||||
setSelectedEmployees([]);
|
||||
setSearchTerm('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="w-[480px] bg-Neutrals-NeutralSlate0 dark:bg-Neutrals-NeutralSlate900 rounded-3xl shadow-[0px_25px_50px_-12px_rgba(0,0,0,0.25)] flex flex-col justify-start items-start overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="self-stretch p-6 inline-flex justify-between items-center">
|
||||
<div className="flex justify-start items-center gap-2.5">
|
||||
<div className="justify-start text-Text-Dark-950 dark:text-Neutrals-NeutralSlate50 text-lg font-semibold font-['Inter'] leading-7">
|
||||
Invite multiple employees
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-6 h-6 flex justify-center items-center hover:bg-Text-Gray-100 dark:hover:bg-Neutrals-NeutralSlate700 rounded"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M13 1L1 13M1 1L13 13" stroke="#666D80" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Input with Dropdown */}
|
||||
<div className="self-stretch px-6 flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1 relative">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">
|
||||
Search employees
|
||||
</div>
|
||||
<div className="self-stretch h-10 px-3 py-2 bg-Neutrals-NeutralSlate0 rounded-lg border border-Outline-Outline-Gray-300 inline-flex justify-start items-center gap-2">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="text-Text-Gray-400">
|
||||
<path d="M15 15L11.15 11.15M12.6667 7.33333C12.6667 10.2789 10.2789 12.6667 7.33333 12.6667C4.38781 12.6667 2 10.2789 2 7.33333C2 4.38781 4.38781 2 7.33333 2C10.2789 2 12.6667 4.38781 12.6667 7.33333Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setShowDropdown(e.target.value.length > 0);
|
||||
}}
|
||||
onFocus={() => setShowDropdown(searchTerm.length > 0)}
|
||||
placeholder="Type name or email to search..."
|
||||
className="flex-1 text-Neutrals-NeutralSlate950 text-sm font-normal font-['Inter'] leading-tight bg-transparent outline-none placeholder:text-Text-Gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dropdown */}
|
||||
{showDropdown && filteredEmployees.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-Neutrals-NeutralSlate0 border border-Outline-Outline-Gray-200 rounded-lg shadow-lg z-10 max-h-48 overflow-y-auto">
|
||||
{filteredEmployees.map((employee) => (
|
||||
<button
|
||||
key={employee.id}
|
||||
onClick={() => handleEmployeeSelect(employee)}
|
||||
className="w-full px-3 py-2 text-left hover:bg-Text-Gray-50 flex items-center gap-3 border-b border-Text-Gray-100 last:border-b-0"
|
||||
>
|
||||
<div className="w-8 h-8 bg-Brand-Orange rounded-full flex items-center justify-center text-white text-sm font-medium">
|
||||
{employee.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-Neutrals-NeutralSlate950">{employee.name}</div>
|
||||
<div className="text-xs text-Text-Gray-500">{employee.email}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Employees */}
|
||||
{selectedEmployees.length > 0 && (
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||
<div className="text-sm font-medium text-Neutrals-NeutralSlate950">
|
||||
Selected ({selectedEmployees.length})
|
||||
</div>
|
||||
<div className="self-stretch flex flex-wrap gap-2">
|
||||
{selectedEmployees.map((employee) => (
|
||||
<div
|
||||
key={employee.id}
|
||||
className="px-3 py-1.5 bg-Brand-Orange bg-opacity-10 rounded-full flex items-center gap-2"
|
||||
>
|
||||
<div className="w-5 h-5 bg-Brand-Orange rounded-full flex items-center justify-center text-white text-xs font-medium">
|
||||
{employee.name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-sm text-Neutrals-NeutralSlate950">{employee.name}</span>
|
||||
<button
|
||||
onClick={() => handleEmployeeRemove(employee.id)}
|
||||
className="w-4 h-4 flex items-center justify-center hover:bg-Brand-Orange hover:bg-opacity-20 rounded-full"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M9 3L3 9M3 3L9 9" stroke="#666D80" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="self-stretch p-6 inline-flex justify-end items-center">
|
||||
<div className="flex justify-start items-start gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-Neutrals-NeutralSlate0 rounded-lg border border-Outline-Outline-Gray-300 text-Neutrals-NeutralSlate700 text-sm font-medium font-['Inter'] leading-tight hover:bg-Text-Gray-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleInvite}
|
||||
disabled={selectedEmployees.length === 0}
|
||||
className="px-4 py-2 bg-Brand-Orange rounded-lg text-Neutrals-NeutralSlate0 text-sm font-medium font-['Inter'] leading-tight hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send Invites ({selectedEmployees.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
6
src/components/CompanyWiki/index.ts
Normal file
6
src/components/CompanyWiki/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { CompanyWikiEmptyState } from './CompanyWikiEmptyState';
|
||||
export { CompanyWikiCompletedState } from './CompanyWikiCompletedState';
|
||||
export { InviteEmployeesModal } from './InviteEmployeesModal';
|
||||
export { InviteMultipleEmployeesModal } from './InviteMultipleEmployeesModal';
|
||||
export { CompanyWikiManager } from './CompanyWikiManager';
|
||||
export type { WikiState, InviteModalState } from './CompanyWikiManager';
|
||||
366
src/components/UiKit.tsx
Normal file
366
src/components/UiKit.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
38
src/components/charts/RadarPerformanceChart.tsx
Normal file
38
src/components/charts/RadarPerformanceChart.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, Tooltip, Legend } from 'recharts';
|
||||
|
||||
export interface RadarMetric {
|
||||
label: string;
|
||||
value: number; // 0-100
|
||||
max?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
data: RadarMetric[];
|
||||
height?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const RadarPerformanceChart: React.FC<Props> = ({ title, data, height = 320, color = '#3b82f6' }) => {
|
||||
const chartData = data.map(d => ({ subject: d.label, A: d.value, fullMark: d.max ?? 100 }));
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{title && <h4 className="text-sm font-medium text-[--text-secondary] mb-2">{title}</h4>}
|
||||
<div style={{ width: '100%', height }}>
|
||||
<ResponsiveContainer>
|
||||
<RadarChart data={chartData} margin={{ top: 10, right: 30, bottom: 10, left: 10 }}>
|
||||
<PolarGrid stroke="var(--border-color)" />
|
||||
<PolarAngleAxis dataKey="subject" tick={{ fill: 'var(--text-secondary)', fontSize: 11 }} />
|
||||
<PolarRadiusAxis angle={30} domain={[0, 100]} tick={{ fill: 'var(--text-secondary)', fontSize: 10 }} />
|
||||
<Radar name={title || 'Score'} dataKey="A" stroke={color} fill={color} fillOpacity={0.35} />
|
||||
<Tooltip wrapperStyle={{ fontSize: 12 }} contentStyle={{ background: 'var(--background-secondary)', border: '1px solid var(--border-color)' }} />
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadarPerformanceChart;
|
||||
30
src/components/charts/ScoreBarList.tsx
Normal file
30
src/components/charts/ScoreBarList.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ScoreItem { label: string; value: number; max?: number; }
|
||||
interface Props { title?: string; items: ScoreItem[]; color?: string; }
|
||||
|
||||
const ScoreBarList: React.FC<Props> = ({ title, items, color = '#6366f1' }) => {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{title && <h4 className="text-sm font-medium text-[--text-secondary]">{title}</h4>}
|
||||
<ul className="space-y-2">
|
||||
{items.map(it => {
|
||||
const pct = Math.min(100, Math.round((it.value / (it.max ?? 100)) * 100));
|
||||
return (
|
||||
<li key={it.label} className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-[--text-secondary]">
|
||||
<span>{it.label}</span>
|
||||
<span>{it.value}{it.max ? `/${it.max}` : ''}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-[--background-secondary] rounded overflow-hidden">
|
||||
<div className="h-full transition-all" style={{ width: pct + '%', background: color }} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScoreBarList;
|
||||
180
src/components/chat/ChatEmptyState.tsx
Normal file
180
src/components/chat/ChatEmptyState.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SuggestionCardProps {
|
||||
category: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const SuggestionCard: React.FC<SuggestionCardProps> = ({ category, title, description, icon, onClick }) => (
|
||||
<div
|
||||
className="p-4 bg-[var(--Neutrals-NeutralSlate0)] rounded-2xl border border-[var(--Neutrals-NeutralSlate200)] hover:border-[var(--Brand-Orange)] hover:shadow-sm transition-all cursor-pointer group"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 bg-[var(--Brand-Orange)]/10 rounded-xl flex items-center justify-center text-[var(--Brand-Orange)] group-hover:bg-[var(--Brand-Orange)] group-hover:text-white transition-colors">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-[var(--Brand-Orange)] font-medium mb-1">{category}</div>
|
||||
<div className="text-sm font-medium text-[var(--Neutrals-NeutralSlate950)] mb-1">{title}</div>
|
||||
<div className="text-xs text-[var(--Neutrals-NeutralSlate500)] leading-relaxed">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface CategoryTabProps {
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const CategoryTab: React.FC<CategoryTabProps> = ({ label, isActive, onClick }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${isActive
|
||||
? 'bg-[var(--Brand-Orange)] text-white'
|
||||
: 'bg-[var(--Neutrals-NeutralSlate100)] text-[var(--Neutrals-NeutralSlate600)] hover:bg-[var(--Neutrals-NeutralSlate200)]'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
const ChatEmptyState: React.FC = () => {
|
||||
const [activeCategory, setActiveCategory] = React.useState('All');
|
||||
|
||||
const categories = ['All', 'Performance', 'Culture', 'Reports', 'Analysis'];
|
||||
|
||||
const suggestions = [
|
||||
{
|
||||
category: 'Performance',
|
||||
title: 'Analyze team performance trends',
|
||||
description: 'Get insights on productivity patterns and improvement areas across your organization.',
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 17.5013V5.83464C2.5 5.36793 2.5 5.13458 2.59083 4.95631C2.67072 4.79951 2.79821 4.67202 2.95501 4.59213C3.13327 4.5013 3.36662 4.5013 3.83333 4.5013H5.16667C5.63338 4.5013 5.86673 4.5013 6.04499 4.59213C6.20179 4.67202 6.32928 4.79951 6.40917 4.95631C6.5 5.13458 6.5 5.36793 6.5 5.83464V17.5013M17.5 17.5013V9.16797C17.5 8.70126 17.5 8.46791 17.4092 8.28965C17.3293 8.13285 17.2018 8.00536 17.045 7.92547C16.8667 7.83464 16.6334 7.83464 16.1667 7.83464H14.8333C14.3666 7.83464 14.1333 7.83464 13.955 7.92547C13.7982 8.00536 13.6707 8.13285 13.5908 8.28965C13.5 8.46791 13.5 8.70126 13.5 9.16797V17.5013M12.5 17.5013V2.5013C12.5 2.03459 12.5 1.80124 12.4092 1.62298C12.3293 1.46618 12.2018 1.33869 12.045 1.2588C11.8667 1.16797 11.6334 1.16797 11.1667 1.16797H9.83333C9.36662 1.16797 9.13327 1.16797 8.95501 1.2588C8.79821 1.33869 8.67072 1.46618 8.59083 1.62298C8.5 1.80124 8.5 2.03459 8.5 2.5013V17.5013M18.3333 17.5013H1.66667" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
category: 'Culture',
|
||||
title: 'Assess company culture health',
|
||||
description: 'Review employee satisfaction, engagement levels, and cultural alignment metrics.',
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 5.83464C7.5 6.752 7.5 7.21068 7.70552 7.54611C7.88497 7.84313 8.15687 8.11503 8.45389 8.29448C8.78932 8.5 9.248 8.5 10.1654 8.5H11.5013C12.4187 8.5 12.8774 8.5 13.2128 8.29448C13.5098 8.11503 13.7817 7.84313 13.9612 7.54611C14.1667 7.21068 14.1667 6.752 14.1667 5.83464V4.16797C14.1667 3.25061 14.1667 2.79193 13.9612 2.4565C13.7817 2.15948 13.5098 1.88758 13.2128 1.70813C12.8774 1.5026 12.4187 1.5026 11.5013 1.5026H10.1654C9.248 1.5026 8.78932 1.5026 8.45389 1.70813C8.15687 1.88758 7.88497 2.15948 7.70552 2.4565C7.5 2.79193 7.5 3.25061 7.5 4.16797V5.83464Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2.5 14.168C2.5 15.0854 2.5 15.544 2.70552 15.8795C2.88497 16.1765 3.15687 16.4484 3.45389 16.6278C3.78932 16.8333 4.248 16.8333 5.16536 16.8333H6.50131C7.41867 16.8333 7.87735 16.8333 8.21278 16.6278C8.5098 16.4484 8.7817 16.1765 8.96115 15.8795C9.16667 15.544 9.16667 15.0854 9.16667 14.168V12.5013C9.16667 11.5839 9.16667 11.1253 8.96115 10.7898C8.7817 10.4928 8.5098 10.2209 8.21278 10.0415C7.87735 9.83594 7.41867 9.83594 6.50131 9.83594H5.16536C4.248 9.83594 3.78932 9.83594 3.45389 10.0415C3.15687 10.2209 2.88497 10.4928 2.70552 10.7898C2.5 11.1253 2.5 11.5839 2.5 12.5013V14.168Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10.8346 14.168C10.8346 15.0854 10.8346 15.544 11.0401 15.8795C11.2196 16.1765 11.4915 16.4484 11.7885 16.6278C12.1239 16.8333 12.5826 16.8333 13.5 16.8333H14.8359C15.7533 16.8333 16.212 16.8333 16.5474 16.6278C16.8444 16.4484 17.1163 16.1765 17.2958 15.8795C17.5013 15.544 17.5013 15.0854 17.5013 14.168V12.5013C17.5013 11.5839 17.5013 11.1253 17.2958 10.7898C17.1163 10.4928 16.8444 10.2209 16.5474 10.0415C16.212 9.83594 15.7533 9.83594 14.8359 9.83594H13.5C12.5826 9.83594 12.1239 9.83594 11.7885 10.0415C11.4915 10.2209 11.2196 10.4928 11.0401 10.7898C10.8346 11.1253 10.8346 11.5839 10.8346 12.5013V14.168Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
category: 'Reports',
|
||||
title: 'Generate executive summary',
|
||||
description: 'Create comprehensive reports on organizational strengths, risks, and recommendations.',
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.6666 9.16797H6.66659M8.33325 12.5013H6.66659M13.3333 5.83464H6.66659M16.6666 5.66797V14.3346C16.6666 15.7348 16.6666 16.4348 16.3941 16.9696C16.1544 17.44 15.772 17.8225 15.3016 18.0622C14.7668 18.3346 14.0667 18.3346 12.6666 18.3346H7.33325C5.93312 18.3346 5.23306 18.3346 4.69828 18.0622C4.22787 17.8225 3.84542 17.44 3.60574 16.9696C3.33325 16.4348 3.33325 15.7348 3.33325 14.3346V5.66797C3.33325 4.26784 3.33325 3.56777 3.60574 3.03299C3.84542 2.56259 4.22787 2.18014 4.69828 1.94045C5.23306 1.66797 5.93312 1.66797 7.33325 1.66797H12.6666C14.0667 1.66797 14.7668 1.66797 15.3016 1.94045C15.772 2.18014 16.1544 2.56259 16.3941 3.03299C16.6666 3.56777 16.6666 4.26784 16.6666 5.66797Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
category: 'Analysis',
|
||||
title: 'Compare department metrics',
|
||||
description: 'Analyze cross-departmental performance and identify areas for improvement.',
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 1.66797C11.0944 1.66797 12.1781 1.88352 13.1891 2.30231C14.2002 2.7211 15.1188 3.33493 15.8926 4.10875C16.6665 4.88257 17.2803 5.80123 17.6991 6.81228C18.1179 7.82332 18.3334 8.90696 18.3334 10.0013M10.0001 1.66797V10.0013M10.0001 1.66797C5.39771 1.66797 1.66675 5.39893 1.66675 10.0013C1.66675 14.6037 5.39771 18.3346 10.0001 18.3346C14.6025 18.3346 18.3334 14.6037 18.3334 10.0013M10.0001 1.66797C14.6025 1.66797 18.3334 5.39893 18.3334 10.0013M18.3334 10.0013L10.0001 10.0013M18.3334 10.0013C18.3334 11.3164 18.0222 12.6128 17.4251 13.7846C16.8281 14.9563 15.9622 15.9701 14.8983 16.7431L10.0001 10.0013" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
category: 'Performance',
|
||||
title: 'Review individual performance',
|
||||
description: 'Deep dive into specific employee performance data and development opportunities.',
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0013 12.5C11.3821 12.5 12.5013 11.3807 12.5013 10C12.5013 8.61929 11.3821 7.5 10.0013 7.5C8.62061 7.5 7.50132 8.61929 7.50132 10C7.50132 11.3807 8.62061 12.5 10.0013 12.5Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10.0013 1.66797C14.6037 1.66797 18.3346 5.39893 18.3346 10.0013C18.3346 14.6037 14.6037 18.3346 10.0013 18.3346C5.39893 18.3346 1.66797 14.6037 1.66797 10.0013C1.66797 5.39893 5.39893 1.66797 10.0013 1.66797Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
category: 'Culture',
|
||||
title: 'Identify team dynamics',
|
||||
description: 'Understand collaboration patterns, communication effectiveness, and team cohesion.',
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.168 5.83464C14.168 7.67561 12.675 9.16797 10.8346 9.16797C8.99367 9.16797 7.50131 7.67561 7.50131 5.83464C7.50131 3.99367 8.99367 2.5013 10.8346 2.5013C12.675 2.5013 14.168 3.99367 14.168 5.83464Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10.8346 11.668C7.52292 11.668 4.83594 14.3549 4.83594 17.6666H16.8346C16.8346 14.3549 14.1477 11.668 10.8346 11.668Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5.83464 9.16797C5.83464 10.5488 4.71536 11.668 3.33464 11.668C1.95393 11.668 0.834635 10.5488 0.834635 9.16797C0.834635 7.78725 1.95393 6.66797 3.33464 6.66797C4.71536 6.66797 5.83464 7.78725 5.83464 9.16797Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M3.33594 13.3346C1.49497 13.3346 0.00260794 14.827 0.00260794 16.668H6.66927C6.66927 15.7686 6.35594 14.9346 5.83594 14.2513" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const filteredSuggestions = activeCategory === 'All'
|
||||
? suggestions
|
||||
: suggestions.filter(s => s.category === activeCategory);
|
||||
|
||||
const handleSuggestionClick = (suggestion: any) => {
|
||||
// Handle suggestion click - could pass this up to parent component
|
||||
console.log('Clicked suggestion:', suggestion.title);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] px-4">
|
||||
{/* Welcome Message */}
|
||||
<div className="text-center mb-8 max-w-2xl">
|
||||
<h2 className="text-2xl font-semibold text-[var(--Neutrals-NeutralSlate950)] mb-3">
|
||||
Welcome to Auditly Chat
|
||||
</h2>
|
||||
<p className="text-[var(--Neutrals-NeutralSlate600)] text-lg leading-relaxed">
|
||||
Ask me anything about your team's performance, company culture, or organizational insights.
|
||||
I can analyze employee data, generate reports, and provide actionable recommendations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Category Tabs */}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{categories.map((category) => (
|
||||
<CategoryTab
|
||||
key={category}
|
||||
label={category}
|
||||
isActive={activeCategory === category}
|
||||
onClick={() => setActiveCategory(category)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Suggestion Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl w-full">
|
||||
{filteredSuggestions.map((suggestion, index) => (
|
||||
<SuggestionCard
|
||||
key={index}
|
||||
category={suggestion.category}
|
||||
title={suggestion.title}
|
||||
description={suggestion.description}
|
||||
icon={suggestion.icon}
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Additional Help Text */}
|
||||
<div className="mt-8 text-center text-sm text-[var(--Neutrals-NeutralSlate500)] max-w-xl">
|
||||
<p>
|
||||
You can also upload files, mention specific employees using @, or ask custom questions about your organization.
|
||||
I'll provide insights based on your team's data and industry best practices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatEmptyState;
|
||||
159
src/components/chat/ChatLayout.tsx
Normal file
159
src/components/chat/ChatLayout.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useOrg } from '../../contexts/OrgContext';
|
||||
import { Employee } from '../../types';
|
||||
import MessageThread from './MessageThread';
|
||||
import FileUploadInput from './FileUploadInput';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
timestamp: number;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
interface ChatLayoutProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ChatLayout: React.FC<ChatLayoutProps> = ({ children }) => {
|
||||
const { employees } = useOrg();
|
||||
const [selectedEmployees, setSelectedEmployees] = useState<Employee[]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!inputValue.trim() && uploadedFiles.length === 0) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
text: inputValue,
|
||||
isUser: true,
|
||||
timestamp: Date.now(),
|
||||
files: uploadedFiles.length > 0 ? [...uploadedFiles] : undefined
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInputValue('');
|
||||
setUploadedFiles([]);
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
text: "I understand you're asking about the employee data. Based on the information provided, I can help analyze the performance metrics and provide insights.\n\nHere are some key findings from your team's data:\n\n• **Performance Trends**: Overall team productivity has increased by 15% this quarter\n• **Cultural Health**: Employee satisfaction scores are above industry average\n• **Areas for Growth**: Communication and cross-team collaboration could be improved\n\nWould you like me to dive deeper into any of these areas?",
|
||||
isUser: false,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleFilesSelected = (files: File[]) => {
|
||||
// For demo purposes, we'll just add the file names
|
||||
// In a real implementation, you'd upload the files and get URLs back
|
||||
const fileNames = files.map(file => file.name);
|
||||
setUploadedFiles(prev => [...prev, ...fileNames]);
|
||||
};
|
||||
|
||||
const hasMessages = messages.length > 0;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col overflow-hidden">
|
||||
{/* Header with Employee Selection */}
|
||||
<div className="px-6 py-4 bg-[var(--Neutrals-NeutralSlate0)] border-b border-[var(--Neutrals-NeutralSlate200)] flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-semibold text-[var(--Neutrals-NeutralSlate950)]">Chat</h1>
|
||||
{selectedEmployees.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-[var(--Neutrals-NeutralSlate500)]">Analyzing:</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{selectedEmployees.slice(0, 3).map((emp, index) => (
|
||||
<div key={emp.id} className="px-2 py-1 bg-[var(--Brand-Orange)]/10 rounded-full text-xs text-[var(--Brand-Orange)]">
|
||||
{emp.name}
|
||||
</div>
|
||||
))}
|
||||
{selectedEmployees.length > 3 && (
|
||||
<div className="px-2 py-1 bg-[var(--Neutrals-NeutralSlate100)] rounded-full text-xs text-[var(--Neutrals-NeutralSlate600)]">
|
||||
+{selectedEmployees.length - 3} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
{hasMessages ? (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<MessageThread
|
||||
messages={messages}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="px-6 py-4 bg-[var(--Neutrals-NeutralSlate0)] border-t border-[var(--Neutrals-NeutralSlate200)]">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-end gap-3">
|
||||
<FileUploadInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder="Ask about your team's performance, culture, or any insights..."
|
||||
disabled={isLoading}
|
||||
uploadedFiles={uploadedFiles}
|
||||
onRemoveFile={handleRemoveFile}
|
||||
onFilesSelected={handleFilesSelected}
|
||||
/>
|
||||
|
||||
{/* Send Button */}
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue.trim() && uploadedFiles.length === 0}
|
||||
className="px-4 py-3 bg-[var(--Brand-Orange)] text-white rounded-xl hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex-shrink-0"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3346 1.66797L9.16797 10.8346M18.3346 1.66797L12.5013 18.3346L9.16797 10.8346M18.3346 1.66797L1.66797 7.5013L9.16797 10.8346" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatLayout;
|
||||
278
src/components/chat/FileUploadInput.tsx
Normal file
278
src/components/chat/FileUploadInput.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
|
||||
interface FileUploadPreviewProps {
|
||||
files: string[];
|
||||
onRemoveFile: (index: number) => void;
|
||||
}
|
||||
|
||||
const FileUploadPreview: React.FC<FileUploadPreviewProps> = ({ files, onRemoveFile }) => {
|
||||
if (files.length === 0) return null;
|
||||
|
||||
const getFileIcon = (fileName: string) => {
|
||||
const extension = fileName.split('.').pop()?.toLowerCase();
|
||||
|
||||
switch (extension) {
|
||||
case 'pdf':
|
||||
return (
|
||||
<div className="w-6 h-6 bg-red-500 rounded flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">P</span>
|
||||
</div>
|
||||
);
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return (
|
||||
<div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">W</span>
|
||||
</div>
|
||||
);
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return (
|
||||
<div className="w-6 h-6 bg-green-500 rounded flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">E</span>
|
||||
</div>
|
||||
);
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
return (
|
||||
<div className="w-6 h-6 bg-purple-500 rounded flex items-center justify-center">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 9L3.5 6.5L5 8L8.5 4.5L11 7M1 1H11V11H1V1Z" stroke="white" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="w-6 h-6 bg-gray-500 rounded flex items-center justify-center">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H2C1.44772 1 1 1.44772 1 2V10C1 10.5523 1.44772 11 2 11H10C10.5523 11 11 10.5523 11 10V5M7 1L11 5M7 1V5H11" stroke="white" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 bg-[var(--Neutrals-NeutralSlate100)] rounded-lg hover:bg-[var(--Neutrals-NeutralSlate200)] transition-colors group"
|
||||
>
|
||||
{getFileIcon(file)}
|
||||
<span className="text-sm text-[var(--Neutrals-NeutralSlate700)] max-w-[150px] truncate">{file}</span>
|
||||
<button
|
||||
onClick={() => onRemoveFile(index)}
|
||||
className="w-5 h-5 text-[var(--Neutrals-NeutralSlate400)] hover:text-red-500 hover:bg-red-50 rounded transition-colors flex items-center justify-center"
|
||||
title="Remove file"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 3L3 9M3 3L9 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FileUploadDropzoneProps {
|
||||
onFilesSelected: (files: File[]) => void;
|
||||
children: React.ReactNode;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const FileUploadDropzone: React.FC<FileUploadDropzoneProps> = ({
|
||||
onFilesSelected,
|
||||
children,
|
||||
accept = "*/*",
|
||||
multiple = true,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
if (!disabled) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
onFilesSelected(files);
|
||||
}
|
||||
}, [onFilesSelected, disabled]);
|
||||
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length > 0) {
|
||||
onFilesSelected(files);
|
||||
}
|
||||
// Reset input value to allow selecting the same file again
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}, [onFilesSelected]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!disabled && fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`
|
||||
relative transition-all
|
||||
${isDragOver ? 'opacity-80' : ''}
|
||||
${disabled ? 'cursor-not-allowed opacity-50' : ''}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{children}
|
||||
|
||||
{/* Drag overlay */}
|
||||
{isDragOver && (
|
||||
<div className="absolute inset-0 bg-[var(--Brand-Orange)]/10 border-2 border-dashed border-[var(--Brand-Orange)] rounded-xl flex items-center justify-center">
|
||||
<div className="text-[var(--Brand-Orange)] font-medium">Drop files here</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FileUploadInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
uploadedFiles: string[];
|
||||
onRemoveFile: (index: number) => void;
|
||||
onFilesSelected: (files: File[]) => void;
|
||||
}
|
||||
|
||||
const FileUploadInput: React.FC<FileUploadInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
placeholder = "Ask about your team's performance, culture, or any insights...",
|
||||
disabled = false,
|
||||
uploadedFiles,
|
||||
onRemoveFile,
|
||||
onFilesSelected
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFilesSelected = (files: File[]) => {
|
||||
// For demo purposes, we'll just add the file names
|
||||
// In a real implementation, you'd upload the files and get URLs back
|
||||
const fileNames = files.map(file => file.name);
|
||||
onFilesSelected(files);
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length > 0) {
|
||||
handleFilesSelected(files);
|
||||
}
|
||||
// Reset input value to allow selecting the same file again
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
if (!disabled && fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* File Upload Preview */}
|
||||
<FileUploadPreview files={uploadedFiles} onRemoveFile={onRemoveFile} />
|
||||
|
||||
{/* Hidden File Input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.jpg,.jpeg,.png,.gif"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* Input Field with File Upload */}
|
||||
<FileUploadDropzone
|
||||
onFilesSelected={handleFilesSelected}
|
||||
disabled={disabled}
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.jpg,.jpeg,.png,.gif"
|
||||
>
|
||||
<div className="relative flex items-end gap-3">
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full min-h-[44px] max-h-32 px-4 py-3 pr-12 border border-[var(--Neutrals-NeutralSlate200)] rounded-xl resize-none focus:outline-none focus:border-[var(--Brand-Orange)] focus:ring-1 focus:ring-[var(--Brand-Orange)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors bg-[var(--Neutrals-NeutralSlate0)] text-[var(--Neutrals-NeutralSlate950)]"
|
||||
rows={1}
|
||||
/>
|
||||
|
||||
{/* File Upload Button */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUploadClick();
|
||||
}}
|
||||
className="absolute right-3 top-3 w-6 h-6 text-[var(--Neutrals-NeutralSlate400)] hover:text-[var(--Brand-Orange)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
title="Upload files"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V15M17 8L12 3M12 3L7 8M12 3V15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FileUploadDropzone>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploadInput;
|
||||
export { FileUploadPreview, FileUploadDropzone };
|
||||
118
src/components/chat/MessageThread.tsx
Normal file
118
src/components/chat/MessageThread.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
timestamp: number;
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
||||
const formatTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
if (message.isUser) {
|
||||
return (
|
||||
<div className="flex justify-end mb-4">
|
||||
<div className="max-w-[70%] flex flex-col items-end">
|
||||
<div className="bg-[var(--Brand-Orange)] text-white px-4 py-3 rounded-2xl rounded-br-md">
|
||||
{message.files && message.files.length > 0 && (
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{message.files.map((file, index) => (
|
||||
<div key={index} className="px-2 py-1 bg-white/20 rounded text-xs">
|
||||
📎 {file}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm leading-relaxed">{message.text}</div>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--Neutrals-NeutralSlate400)] mt-1">
|
||||
{formatTime(message.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-start mb-4">
|
||||
<div className="max-w-[85%] flex items-start gap-3">
|
||||
{/* AI Avatar */}
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-[var(--Brand-Orange)] to-orange-600 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 2C8.73438 2 9.375 2.64062 9.375 3.375V4.5C9.375 5.23438 8.73438 5.875 8 5.875C7.26562 5.875 6.625 5.23438 6.625 4.5V3.375C6.625 2.64062 7.26562 2 8 2ZM8 10.125C8.73438 10.125 9.375 10.7656 9.375 11.5V12.625C9.375 13.3594 8.73438 14 8 14C7.26562 14 6.625 13.3594 6.625 12.625V11.5C6.625 10.7656 7.26562 10.125 8 10.125ZM12.625 6.625C13.3594 6.625 14 7.26562 14 8C14 8.73438 13.3594 9.375 12.625 9.375H11.5C10.7656 9.375 10.125 8.73438 10.125 8C10.125 7.26562 10.7656 6.625 11.5 6.625H12.625ZM5.875 8C5.875 8.73438 5.23438 9.375 4.5 9.375H3.375C2.64062 9.375 2 8.73438 2 8C2 7.26562 2.64062 6.625 3.375 6.625H4.5C5.23438 6.625 5.875 7.26562 5.875 8Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-[var(--Neutrals-NeutralSlate100)] text-[var(--Neutrals-NeutralSlate950)] px-4 py-3 rounded-2xl rounded-bl-md">
|
||||
<div className="text-sm leading-relaxed whitespace-pre-wrap">{message.text}</div>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--Neutrals-NeutralSlate400)] mt-1">
|
||||
AI • {formatTime(message.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({ className = '' }) => (
|
||||
<div className={`flex justify-start mb-4 ${className}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* AI Avatar */}
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-[var(--Brand-Orange)] to-orange-600 rounded-full flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 2C8.73438 2 9.375 2.64062 9.375 3.375V4.5C9.375 5.23438 8.73438 5.875 8 5.875C7.26562 5.875 6.625 5.23438 6.625 4.5V3.375C6.625 2.64062 7.26562 2 8 2ZM8 10.125C8.73438 10.125 9.375 10.7656 9.375 11.5V12.625C9.375 13.3594 8.73438 14 8 14C7.26562 14 6.625 13.3594 6.625 12.625V11.5C6.625 10.7656 7.26562 10.125 8 10.125ZM12.625 6.625C13.3594 6.625 14 7.26562 14 8C14 8.73438 13.3594 9.375 12.625 9.375H11.5C10.7656 9.375 10.125 8.73438 10.125 8C10.125 7.26562 10.7656 6.625 11.5 6.625H12.625ZM5.875 8C5.875 8.73438 5.23438 9.375 4.5 9.375H3.375C2.64062 9.375 2 8.73438 2 8C2 7.26562 2.64062 6.625 3.375 6.625H4.5C5.23438 6.625 5.875 7.26562 5.875 8Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--Neutrals-NeutralSlate100)] px-4 py-3 rounded-2xl rounded-bl-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-[var(--Neutrals-NeutralSlate400)] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<div className="w-2 h-2 bg-[var(--Neutrals-NeutralSlate400)] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<div className="w-2 h-2 bg-[var(--Neutrals-NeutralSlate400)] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface MessageThreadProps {
|
||||
messages: Message[];
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MessageThread: React.FC<MessageThreadProps> = ({
|
||||
messages,
|
||||
isLoading = false,
|
||||
className = ''
|
||||
}) => {
|
||||
return (
|
||||
<div className={`flex flex-col ${className}`}>
|
||||
{messages.map((message) => (
|
||||
<MessageBubble key={message.id} message={message} />
|
||||
))}
|
||||
|
||||
{isLoading && <LoadingIndicator />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageThread;
|
||||
export { MessageBubble, LoadingIndicator };
|
||||
5
src/components/chat/index.ts
Normal file
5
src/components/chat/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as ChatSidebar } from './ChatSidebar';
|
||||
export { default as ChatLayout } from './ChatLayout';
|
||||
export { default as ChatEmptyState } from './ChatEmptyState';
|
||||
export { default as MessageThread } from './MessageThread';
|
||||
export { default as FileUploadInput } from './FileUploadInput';
|
||||
319
src/components/figma/EnhancedFigmaQuestion.tsx
Normal file
319
src/components/figma/EnhancedFigmaQuestion.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import ImageUpload from '../ui/ImageUpload';
|
||||
import { StoredImage, uploadCompanyLogo } from '../../services/imageStorageService';
|
||||
|
||||
interface FigmaQuestionProps {
|
||||
question: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
nextDisabled?: boolean;
|
||||
backDisabled?: boolean;
|
||||
nextText?: string;
|
||||
backText?: string;
|
||||
showBackButton?: boolean;
|
||||
currentStep?: number;
|
||||
totalSteps?: number;
|
||||
stepTitle?: string;
|
||||
className?: string;
|
||||
// Image upload props
|
||||
orgId?: string;
|
||||
onImageUploaded?: (image: StoredImage) => void;
|
||||
currentImage?: StoredImage | null;
|
||||
}
|
||||
|
||||
export const EnhancedFigmaQuestion: React.FC<FigmaQuestionProps> = ({
|
||||
question,
|
||||
description,
|
||||
children,
|
||||
onBack,
|
||||
onNext,
|
||||
nextDisabled = false,
|
||||
backDisabled = false,
|
||||
nextText = "Next",
|
||||
backText = "Back",
|
||||
showBackButton = true,
|
||||
currentStep = 1,
|
||||
totalSteps = 8,
|
||||
stepTitle = "Company Overview & Mission.",
|
||||
className = "",
|
||||
// Image upload props
|
||||
orgId,
|
||||
onImageUploaded,
|
||||
currentImage
|
||||
}) => {
|
||||
const [uploadingImage, setUploadingImage] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string>('');
|
||||
|
||||
const handleImageSelected = async (file: File) => {
|
||||
if (!orgId) {
|
||||
setUploadError('Organization ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadingImage(true);
|
||||
setUploadError('');
|
||||
|
||||
try {
|
||||
const uploadedImage = await uploadCompanyLogo(file, orgId);
|
||||
if (onImageUploaded) {
|
||||
onImageUploaded(uploadedImage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to upload image:', error);
|
||||
setUploadError(error instanceof Error ? error.message : 'Failed to upload image');
|
||||
} finally {
|
||||
setUploadingImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageRemove = () => {
|
||||
// For now, just call the callback with null
|
||||
// You could also implement deleteCompanyLogo here if needed
|
||||
if (onImageUploaded) {
|
||||
onImageUploaded(null as any);
|
||||
}
|
||||
};
|
||||
// Generate the progress indicator dots
|
||||
const renderProgressDots = () => {
|
||||
const dots = [];
|
||||
for (let i = 0; i < totalSteps; i++) {
|
||||
if (i < currentStep - 1) {
|
||||
// Completed steps - elongated orange
|
||||
dots.push(
|
||||
<div key={i} className="w-6 h-1 bg-[--Brand-Orange] rounded-3xl" />
|
||||
);
|
||||
} else if (i === currentStep - 1) {
|
||||
// Current step - elongated orange
|
||||
dots.push(
|
||||
<div key={i} className="w-6 h-1 bg-[--Brand-Orange] rounded-3xl" />
|
||||
);
|
||||
} else {
|
||||
// Future steps - small gray circles
|
||||
dots.push(
|
||||
<div key={i} data-svg-wrapper>
|
||||
<svg width="4" height="4" viewBox="0 0 4 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="4" height="4" rx="2" fill="var(--Neutrals-NeutralSlate300, #D5D7DA)" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return dots;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-[1440px] h-[810px] py-6 relative bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-9 ${className}`}>
|
||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||
{currentStep === 1 ?
|
||||
<div className="self-stretch inline-flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight">Company Logo</div>
|
||||
</div>
|
||||
<div className="self-stretch p-4 rounded-3xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-start items-center gap-4">
|
||||
<ImageUpload
|
||||
onImageSelected={handleImageSelected}
|
||||
onImageRemove={currentImage ? handleImageRemove : undefined}
|
||||
currentImage={currentImage}
|
||||
loading={uploadingImage}
|
||||
error={uploadError}
|
||||
size="medium"
|
||||
/>
|
||||
<div className="inline-flex flex-col justify-start items-start gap-4">
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-3">
|
||||
<div className="flex justify-start items-center gap-2">
|
||||
<div data-svg-wrapper className="relative">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H2M12 8.66667L8 4.66667M8 4.66667L4 8.66667M8 4.66667V14" stroke="var(--Neutrals-NeutralSlate950, #FDFDFD)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
|
||||
{currentImage ? 'Change image' : 'Upload image'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{uploadError && (
|
||||
<div className="text-red-500 text-xs">{uploadError}</div>
|
||||
)}
|
||||
{currentImage && (
|
||||
<div className="text-[--Neutrals-NeutralSlate500] text-xs">
|
||||
{Math.round(currentImage.compressedSize / 1024)}KB • {currentImage.width}×{currentImage.height}px
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate950] text-2xl font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{question}
|
||||
</div>
|
||||
}
|
||||
{children}
|
||||
</div>
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||
{showBackButton && (
|
||||
<div
|
||||
data-property-1="Secondary"
|
||||
data-show-icon-left="false"
|
||||
data-show-icon-right="false"
|
||||
data-show-text="true"
|
||||
data-size="Big"
|
||||
className={`h-12 px-8 py-3.5 bg-[--Neutrals-NeutralSlate50] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden ${backDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-[--Neutrals-NeutralSlate200]'}`}
|
||||
onClick={!backDisabled ? onBack : undefined}
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
|
||||
{backText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
data-property-1="Primiary"
|
||||
data-show-icon-left="false"
|
||||
data-show-icon-right="false"
|
||||
data-show-text="true"
|
||||
data-size="Big"
|
||||
className={`flex-1 h-12 px-4 py-3.5 bg-[--Brand-Orange] rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden ${nextDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:opacity-90'}`}
|
||||
onClick={!nextDisabled ? onNext : undefined}
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">
|
||||
{nextText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step indicator - top left */}
|
||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] uppercase leading-none">
|
||||
{currentStep} of {totalSteps}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skip button - top right */}
|
||||
<div className="px-3 py-1.5 left-[1363px] top-[24px] absolute bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate200]">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-none">
|
||||
Skip
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator and title - top center */}
|
||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
||||
<div className="p-4 bg-[--Neutrals-NeutralSlate100] rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
{renderProgressDots()}
|
||||
</div>
|
||||
<div className="self-stretch text-center justify-start text-[--Neutrals-NeutralSlate500] text-base font-medium font-['Neue_Montreal'] leading-normal">
|
||||
{stepTitle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Question Card Component (for Q&A style layout)
|
||||
interface FigmaQuestionCardProps {
|
||||
question: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FigmaQuestionCard: React.FC<FigmaQuestionCardProps> = ({
|
||||
question,
|
||||
description,
|
||||
children,
|
||||
className = ""
|
||||
}) => {
|
||||
return (
|
||||
// <div className="w-full px-5 pt-5 pb-6 bg-white rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-grow flex-col gap-4">
|
||||
// <div className="self-stretch inline-flex justify-start items-center gap-3">
|
||||
// <div className="flex-1">
|
||||
// {children}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
<div className="self-stretch justify-center flex items-center gap-3">
|
||||
{children}
|
||||
</div>
|
||||
// <div className="w-full px-5 pt-5 pb-6 bg-white rounded-2xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-end items-end gap-4">
|
||||
// {/* <div className="self-stretch inline-flex justify-start items-start gap-3">
|
||||
// <div className="justify-start text-zinc-300 text-xl font-medium font-['Inter'] leading-loose">Q</div>
|
||||
// <div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||
// <div className="self-stretch justify-start text-[--Neutrals-NeutralSlate950] text-xl font-semibold font-['Inter'] leading-loose">
|
||||
// {question}
|
||||
// </div>
|
||||
// {description && (
|
||||
// <div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-sm font-normal font-['Inter'] leading-tight">
|
||||
// {description}
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div> */}
|
||||
// <div className="self-stretch h-0 rotate-90 shadow-[0px_1.5px_1.5px_0px_rgba(255,255,255,1.00)] outline outline-1 outline-offset-[-0.50px] border-[--Neutrals-NeutralSlate200]" />
|
||||
// <div className="self-stretch inline-flex justify-start items-center gap-3">
|
||||
// <div className="justify-start text-zinc-300 text-xl font-medium font-['Inter'] leading-loose">A</div>
|
||||
// <div className="flex-1">
|
||||
// {children}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
);
|
||||
};
|
||||
|
||||
// Enhanced input that matches Figma designs
|
||||
interface EnhancedFigmaInputProps {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const EnhancedFigmaInput: React.FC<EnhancedFigmaInputProps> = ({
|
||||
placeholder = "Type your answer....",
|
||||
value = "",
|
||||
onChange,
|
||||
multiline = false,
|
||||
rows = 4,
|
||||
className = ""
|
||||
}) => {
|
||||
const baseClasses = "self-stretch min-h-40 p-5 relative bg-[--Neutrals-NeutralSlate50] rounded-xl inline-flex justify-start items-start gap-2.5 overflow-hidden";
|
||||
const inputClasses = "flex self-stretch w-100 text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal bg-transparent border-none outline-none resize-none";
|
||||
|
||||
if (multiline) {
|
||||
return (
|
||||
<div className={`${baseClasses} ${className}`}>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
className={inputClasses}
|
||||
/>
|
||||
<div className="w-3 h-3 absolute right-[18px] bottom-[18px]">
|
||||
<div className="w-2 h-2 left-[2px] top-[2px] absolute outline outline-1 outline-offset-[-0.50px] outline-[--Neutrals-NeutralSlate500]" />
|
||||
<div className="w-1 h-1 left-[7px] top-[7px] absolute outline outline-1 outline-offset-[-0.50px] outline-[--Neutrals-NeutralSlate500]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClasses} ${className}`}>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedFigmaQuestion;
|
||||
50
src/components/figma/FigmaAlert.tsx
Normal file
50
src/components/figma/FigmaAlert.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
// Figma-based Alert component with proper CSS variables and styling
|
||||
interface FigmaAlertProps {
|
||||
title: string;
|
||||
variant?: 'success' | 'error' | 'warning' | 'info';
|
||||
children?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FigmaAlert: React.FC<FigmaAlertProps> = ({
|
||||
title,
|
||||
variant = 'info',
|
||||
children,
|
||||
onClose,
|
||||
className = ''
|
||||
}) => {
|
||||
const getBorderColor = () => {
|
||||
switch (variant) {
|
||||
case 'success': return 'bg-[--Other-Green]';
|
||||
case 'error': return 'bg-red-500';
|
||||
case 'warning': return 'bg-yellow-500';
|
||||
default: return 'bg-blue-500';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`p-4 relative bg-white rounded-lg shadow-[0px_2px_2px_-1px_rgba(10,13,18,0.04)] shadow-[0px_4px_6px_-2px_rgba(10,13,18,0.03)] shadow-[0px_12px_16px_-4px_rgba(10,13,18,0.08)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-center items-center gap-2.5 overflow-hidden ${className}`}>
|
||||
<div className="w-96 max-w-96 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">
|
||||
{title}
|
||||
{children && <div className="mt-1 text-xs text-[--Neutrals-NeutralSlate600]">{children}</div>}
|
||||
</div>
|
||||
|
||||
{onClose && (
|
||||
<button onClick={onClose} className="flex-shrink-0">
|
||||
<div data-svg-wrapper className="relative">
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6663 5.83325L6.33301 14.1666M6.33301 5.83325L14.6663 14.1666" stroke="var(--Neutrals-NeutralSlate600, #535862)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className={`w-2 h-32 left-[-4px] top-[-41px] absolute ${getBorderColor()}`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FigmaAlert;
|
||||
140
src/components/figma/FigmaInput.tsx
Normal file
140
src/components/figma/FigmaInput.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FigmaInputProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
icon?: React.ReactNode;
|
||||
buttonText?: string;
|
||||
onButtonClick?: () => void;
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export const FigmaInput: React.FC<FigmaInputProps> = ({
|
||||
label = 'Email',
|
||||
placeholder = 'Enter your email',
|
||||
type = 'text',
|
||||
value,
|
||||
onChange,
|
||||
icon,
|
||||
buttonText,
|
||||
onButtonClick,
|
||||
className = '',
|
||||
required = false,
|
||||
showLabel = true
|
||||
}) => {
|
||||
return (
|
||||
<div className={`self-stretch flex flex-col justify-start items-start gap-2 ${className}`}>
|
||||
{showLabel && label && (
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">
|
||||
{label}
|
||||
</div>
|
||||
{required && (
|
||||
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
{icon && (
|
||||
<div data-svg-wrapper className="relative">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight bg-transparent border-none outline-none placeholder:text-Neutrals-NeutralSlate500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{buttonText && (
|
||||
<button
|
||||
onClick={onButtonClick}
|
||||
className="w-32 max-w-32 px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-[--Neutrals-NeutralSlate200] transition-colors"
|
||||
>
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">
|
||||
{buttonText}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Select component for dropdowns
|
||||
interface FigmaSelectProps {
|
||||
label?: string;
|
||||
value?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
options: { value: string; label: string }[];
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const FigmaSelect: React.FC<FigmaSelectProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
className = '',
|
||||
required = false,
|
||||
placeholder = 'Select option'
|
||||
}) => {
|
||||
return (
|
||||
<div className={`self-stretch flex flex-col justify-start items-start gap-2 ${className}`}>
|
||||
{label && (
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-0.5">
|
||||
<div className="justify-start text-[--Neutrals-NeutralSlate900] text-sm font-normal font-['Inter'] leading-tight">
|
||||
{label}
|
||||
</div>
|
||||
{required && (
|
||||
<div className="justify-start text-[--Brand-Orange] text-sm font-medium font-['Inter'] leading-tight">*</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch px-4 py-3.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] inline-flex justify-start items-center gap-2 overflow-hidden">
|
||||
<select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight bg-transparent border-none outline-none appearance-none"
|
||||
>
|
||||
<option value="" className="text-[--Neutrals-NeutralSlate500]">{placeholder}</option>
|
||||
{options.map((option, index) => (
|
||||
<option key={index} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Custom dropdown arrow */}
|
||||
<div className="pointer-events-none">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 6L8 10L12 6" stroke="var(--Neutrals-NeutralSlate500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Email icon component for convenience
|
||||
export const EmailIcon: React.FC = () => (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66675 5.83325L8.47085 10.5961C9.02182 10.9818 9.29731 11.1746 9.59697 11.2493C9.86166 11.3153 10.1385 11.3153 10.4032 11.2493C10.7029 11.1746 10.9783 10.9818 11.5293 10.5961L18.3334 5.83325M5.66675 16.6666H14.3334C15.7335 16.6666 16.4336 16.6666 16.9684 16.3941C17.4388 16.1544 17.8212 15.772 18.0609 15.3016C18.3334 14.7668 18.3334 14.0667 18.3334 12.6666V7.33325C18.3334 5.93312 18.3334 5.23306 18.0609 4.69828C17.8212 4.22787 17.4388 3.84542 16.9684 3.60574C16.4336 3.33325 15.7335 3.33325 14.3334 3.33325H5.66675C4.26662 3.33325 3.56655 3.33325 3.03177 3.60574C2.56137 3.84542 2.17892 4.22787 1.93923 4.69828C1.66675 5.23306 1.66675 5.93312 1.66675 7.33325V12.6666C1.66675 14.0667 1.66675 14.7668 1.93923 15.3016C2.17892 15.772 2.56137 16.1544 3.03177 16.3941C3.56655 16.6666 4.26662 16.6666 5.66675 16.6666Z" stroke="var(--Neutrals-NeutralSlate500, #717680)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default FigmaInput;
|
||||
42
src/components/figma/FigmaMultipleChoice.tsx
Normal file
42
src/components/figma/FigmaMultipleChoice.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FigmaMultipleChoiceProps {
|
||||
options: string[];
|
||||
selectedValue?: string;
|
||||
onSelect?: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FigmaMultipleChoice: React.FC<FigmaMultipleChoiceProps> = ({
|
||||
options,
|
||||
selectedValue,
|
||||
onSelect,
|
||||
className = ""
|
||||
}) => {
|
||||
return (
|
||||
<div className={`self-stretch inline-flex justify-center items-center gap-3 ${className}`}>
|
||||
{options.map((option, index) => {
|
||||
const isSelected = selectedValue === option;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex-1 h-20 relative rounded-[999px] overflow-hidden cursor-pointer transition-colors ${isSelected
|
||||
? 'bg-[--Neutrals-NeutralSlate800]'
|
||||
: 'bg-[--Neutrals-NeutralSlate100] hover:bg-[--Neutrals-NeutralSlate200]'
|
||||
}`}
|
||||
onClick={() => onSelect?.(option)}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-center text-base font-normal font-['Inter'] leading-normal ${isSelected
|
||||
? 'text-[--Neutrals-NeutralSlate0]'
|
||||
: 'text-[--Neutrals-NeutralSlate950]'
|
||||
}`}>
|
||||
{option}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FigmaMultipleChoice;
|
||||
71
src/components/figma/FigmaProgress.tsx
Normal file
71
src/components/figma/FigmaProgress.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ProgressStepProps {
|
||||
number: number;
|
||||
title: string;
|
||||
isActive?: boolean;
|
||||
isCompleted?: boolean;
|
||||
}
|
||||
|
||||
interface FigmaProgressProps {
|
||||
steps: ProgressStepProps[];
|
||||
currentStep?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ProgressStep: React.FC<ProgressStepProps> = ({ number, title, isActive = false, isCompleted = false }) => {
|
||||
const stepClasses = isActive
|
||||
? "p-2 bg-[--Neutrals-NeutralSlate50] rounded-[10px] shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)]"
|
||||
: "p-2 bg-white rounded-[10px]";
|
||||
|
||||
const numberClasses = isActive || isCompleted
|
||||
? "h-5 p-0.5 bg-[--Brand-Orange] rounded-[999px]"
|
||||
: "h-5 p-0.5 bg-[--Neutrals-NeutralSlate0] rounded-[999px] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200]";
|
||||
|
||||
const numberTextClasses = isActive || isCompleted
|
||||
? "w-4 text-center justify-start text-[--Neutrals-NeutralSlate0] text-xs font-medium font-['Inter'] leading-none"
|
||||
: "w-4 text-center justify-start text-[--Neutrals-NeutralSlate600] text-xs font-medium font-['Inter'] leading-none";
|
||||
|
||||
const titleClasses = isActive
|
||||
? "flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-normal font-['Inter'] leading-tight"
|
||||
: "flex-1 justify-start text-[--Neutrals-NeutralSlate600] text-sm font-normal font-['Inter'] leading-tight";
|
||||
|
||||
return (
|
||||
<div className={`self-stretch inline-flex justify-start items-center gap-2.5 overflow-hidden ${stepClasses}`}>
|
||||
<div className={`inline-flex flex-col justify-center items-center gap-0.5 overflow-hidden ${numberClasses}`}>
|
||||
<div className={numberTextClasses}>{number}</div>
|
||||
</div>
|
||||
<div className={titleClasses}>{title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FigmaProgress: React.FC<FigmaProgressProps> = ({ steps, currentStep = 1, className = '' }) => {
|
||||
return (
|
||||
<div className={`self-stretch inline-flex flex-col justify-start items-start gap-2 ${className}`}>
|
||||
{steps.map((step, index) => (
|
||||
<ProgressStep
|
||||
key={index}
|
||||
number={step.number}
|
||||
title={step.title}
|
||||
isActive={step.number === currentStep}
|
||||
isCompleted={step.number < currentStep}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Default onboarding steps as shown in Figma
|
||||
export const defaultOnboardingSteps: ProgressStepProps[] = [
|
||||
{ number: 1, title: "Company Overview & Vision" },
|
||||
{ number: 2, title: "Leadership & Organizational Structure" },
|
||||
{ number: 3, title: "Operations & Execution" },
|
||||
{ number: 4, title: "Culture & Team Health" },
|
||||
{ number: 5, title: "Sales, Marketing & Growth" },
|
||||
{ number: 6, title: "Financial Health & Metrics" },
|
||||
{ number: 7, title: "Innovation & Product/Service Strategy" },
|
||||
{ number: 8, title: "Personal Leadership & Risk" }
|
||||
];
|
||||
|
||||
export default FigmaProgress;
|
||||
278
src/components/figma/FigmaQuestion.tsx
Normal file
278
src/components/figma/FigmaQuestion.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FigmaQuestionProps {
|
||||
questionNumber?: string | number;
|
||||
title: string;
|
||||
description?: string;
|
||||
answer?: string;
|
||||
onAnswerChange?: (value: string) => void;
|
||||
onBack?: () => void;
|
||||
onNext?: () => void;
|
||||
showNavigation?: boolean;
|
||||
nextLabel?: string;
|
||||
backLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FigmaQuestion: React.FC<FigmaQuestionProps> = ({
|
||||
questionNumber = 'Q',
|
||||
title,
|
||||
description,
|
||||
answer = '',
|
||||
onAnswerChange,
|
||||
onBack,
|
||||
onNext,
|
||||
showNavigation = true,
|
||||
nextLabel = 'Next',
|
||||
backLabel = 'Back',
|
||||
className = ''
|
||||
}) => {
|
||||
return (
|
||||
<div className={`w-[600px] px-5 pt-5 pb-6 bg-Other-White rounded-2xl outline outline-1 outline-offset-[-1px] outline-Neutrals-NeutralSlate200 inline-flex flex-col justify-end items-end gap-4 ${className}`}>
|
||||
{/* Question Header */}
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-3">
|
||||
<div className="justify-start text-zinc-300 text-xl font-medium font-['Inter'] leading-loose">
|
||||
{questionNumber}
|
||||
</div>
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate950 text-xl font-semibold font-['Inter'] leading-loose">
|
||||
{title}
|
||||
</div>
|
||||
{description && (
|
||||
<div className="self-stretch justify-start text-Neutrals-NeutralSlate500 text-sm font-normal font-['Inter'] leading-tight">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div data-svg-wrapper>
|
||||
<svg width="563" height="5" viewBox="0 0 563 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_37_3168)">
|
||||
<path d="M1.5 1L561.5 1" stroke="var(--Neutrals-NeutralSlate200, #E9EAEB)" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_37_3168" x="0" y="0.5" width="563" height="4" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feOffset dy="1.5" />
|
||||
<feGaussianBlur stdDeviation="0.75" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_37_3168" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_37_3168" result="shape" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Answer Section */}
|
||||
<div className="self-stretch inline-flex justify-start items-center gap-3">
|
||||
<div className="justify-start text-zinc-300 text-xl font-medium font-['Inter'] leading-loose">A</div>
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
value={answer}
|
||||
onChange={(e) => onAnswerChange?.(e.target.value)}
|
||||
placeholder="Type your answer...."
|
||||
className="w-full bg-transparent outline-none resize-none text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
{showNavigation && (
|
||||
<div className="inline-flex justify-start items-center gap-3">
|
||||
{onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="px-4 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200 transition-colors"
|
||||
>
|
||||
<div data-svg-wrapper className="relative">
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 15L8 10L13 5" stroke="var(--Neutrals-NeutralSlate950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">
|
||||
{backLabel}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{onNext && (
|
||||
<button
|
||||
onClick={onNext}
|
||||
className="px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden hover:bg-orange-600 transition-colors"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">
|
||||
{nextLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div data-svg-wrapper className="relative">
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 15L13 10L8 5" stroke="var(--Neutrals-NeutralSlate0, #FDFDFD)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Progress Bar Component
|
||||
export const FigmaProgressBar: React.FC<{ currentStep: number; totalSteps: number }> = ({ currentStep, totalSteps }) => {
|
||||
return (
|
||||
<div className="p-4 bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
{Array.from({ length: totalSteps }, (_, index) => {
|
||||
const isActive = index < currentStep;
|
||||
const isFirst = index === 0;
|
||||
return (
|
||||
<div key={index}>
|
||||
<svg width={isFirst ? "24" : "4"} height="4" viewBox={`0 0 ${isFirst ? "24" : "4"} 4`} fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width={isFirst ? "24" : "4"} height="4" rx="2" fill={isActive ? "var(--Brand-Orange, #5E48FC)" : "var(--Neutrals-NeutralSlate300, #D5D7DA)"} />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Rating Scale Component (1-10)
|
||||
export const FigmaRatingScale: React.FC<{
|
||||
question: string;
|
||||
leftLabel: string;
|
||||
rightLabel: string;
|
||||
value?: number;
|
||||
onChange: (value: number) => void;
|
||||
scale?: number;
|
||||
}> = ({ question, leftLabel, rightLabel, value, onChange, scale = 10 }) => {
|
||||
return (
|
||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-center items-center gap-12">
|
||||
<div className="flex flex-col justify-center items-center gap-8">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">{question}</div>
|
||||
<div className="inline-flex justify-center items-center gap-3">
|
||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">{leftLabel}</div>
|
||||
{Array.from({ length: scale }, (_, index) => {
|
||||
const number = index + 1;
|
||||
const isSelected = value === number;
|
||||
return (
|
||||
<div
|
||||
key={number}
|
||||
onClick={() => onChange(number)}
|
||||
className={`w-12 h-12 relative rounded-[576.35px] overflow-hidden cursor-pointer transition-colors ${isSelected ? 'bg-Brand-Orange' : 'bg-Neutrals-NeutralSlate100 hover:bg-Neutrals-NeutralSlate200'
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 flex items-center justify-center text-xl font-medium font-['Inter'] leading-7 ${isSelected ? 'text-white' : 'text-Neutrals-NeutralSlate950'
|
||||
}`}>
|
||||
{number}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">{rightLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Text Area Component
|
||||
export const FigmaTextArea: React.FC<{
|
||||
question: string;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}> = ({ question, value, onChange, placeholder = "Type your answer...." }) => {
|
||||
return (
|
||||
<div className="w-[1440px] h-[810px] py-6 relative bg-Neutrals-NeutralSlate0 inline-flex flex-col justify-center items-center gap-9">
|
||||
<div className="w-full max-w-[464px] min-w-[464px] flex flex-col justify-start items-start gap-12">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-8">
|
||||
<div className="self-stretch text-center justify-start text-Neutrals-NeutralSlate950 text-2xl font-medium font-['Neue_Montreal'] leading-normal">{question}</div>
|
||||
<div className="self-stretch min-h-40 p-5 relative bg-Neutrals-NeutralSlate100 rounded-xl inline-flex justify-start items-start gap-2.5">
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="flex-1 bg-transparent text-Neutrals-NeutralSlate950 text-base font-normal font-['Inter'] leading-normal placeholder:text-Neutrals-NeutralSlate500 outline-none resize-none"
|
||||
placeholder={placeholder}
|
||||
rows={6}
|
||||
/>
|
||||
<div className="w-3 h-3 absolute right-5 bottom-5">
|
||||
<div className="w-2 h-2 absolute top-0.5 left-0.5 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
||||
<div className="w-1 h-1 absolute bottom-0 right-0 outline outline-1 outline-offset-[-0.50px] outline-Neutrals-NeutralSlate500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Navigation Buttons Component
|
||||
export const FigmaNavigationButtons: React.FC<{
|
||||
onBack?: () => void;
|
||||
onNext: () => void;
|
||||
onSkip?: () => void;
|
||||
nextDisabled?: boolean;
|
||||
currentStep?: number;
|
||||
totalSteps?: number;
|
||||
}> = ({ onBack, onNext, onSkip, nextDisabled = false, currentStep, totalSteps }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Progress indicator */}
|
||||
{currentStep && totalSteps && (
|
||||
<div className="px-3 py-1.5 left-[24px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden">
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] uppercase leading-none">{currentStep} of {totalSteps}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skip button */}
|
||||
{onSkip && (
|
||||
<div
|
||||
onClick={onSkip}
|
||||
className="px-3 py-1.5 left-[1363px] top-[24px] absolute bg-Neutrals-NeutralSlate100 rounded-[50px] inline-flex justify-center items-center gap-2 overflow-hidden cursor-pointer hover:bg-Neutrals-NeutralSlate200"
|
||||
>
|
||||
<div className="justify-start text-Neutrals-NeutralSlate500 text-sm font-medium font-['Inter'] leading-none">Skip</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress bar */}
|
||||
{currentStep && totalSteps && (
|
||||
<div className="w-[464px] max-w-[464px] min-w-[464px] left-[488px] top-[24px] absolute flex flex-col justify-start items-center gap-4">
|
||||
<FigmaProgressBar currentStep={currentStep} totalSteps={totalSteps} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||
{onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="h-12 px-8 py-3.5 bg-Neutrals-NeutralSlate100 rounded-[999px] flex justify-center items-center gap-1 overflow-hidden hover:bg-Neutrals-NeutralSlate200"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Neutrals-NeutralSlate950 text-sm font-medium font-['Inter'] leading-tight">Back</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={nextDisabled}
|
||||
className="flex-1 h-12 px-4 py-3.5 bg-Brand-Orange rounded-[999px] outline outline-2 outline-offset-[-2px] outline-blue-400 flex justify-center items-center gap-1 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-Other-White text-sm font-medium font-['Inter'] leading-tight">Next</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
253
src/components/figma/Sidebar.tsx
Normal file
253
src/components/figma/Sidebar.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
interface SidebarProps {
|
||||
companyName?: string;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
export default function Sidebar({ companyName = "Zitlac Media", collapsed = false }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 17.5V11.3333C7.5 10.8666 7.5 10.6333 7.59083 10.455C7.67072 10.2982 7.79821 10.1707 7.95501 10.0908C8.13327 9.99999 8.36662 9.99999 8.83333 9.99999H11.1667C11.6334 9.99999 11.8667 9.99999 12.045 10.0908C12.2018 10.1707 12.3293 10.2982 12.4092 10.455C12.5 10.6333 12.5 10.8666 12.5 11.3333V17.5M9.18141 2.30333L3.52949 6.69927C3.15168 6.99312 2.96278 7.14005 2.82669 7.32405C2.70614 7.48704 2.61633 7.67065 2.56169 7.86588C2.5 8.08627 2.5 8.32558 2.5 8.80421V14.8333C2.5 15.7667 2.5 16.2335 2.68166 16.59C2.84144 16.9036 3.09641 17.1585 3.41002 17.3183C3.76654 17.5 4.23325 17.5 5.16667 17.5H14.8333C15.7668 17.5 16.2335 17.5 16.59 17.3183C16.9036 17.1585 17.1586 16.9036 17.3183 16.59C17.5 16.2335 17.5 15.7667 17.5 14.8333V8.80421C17.5 8.32558 17.5 8.08627 17.4383 7.86588C17.3837 7.67065 17.2939 7.48704 17.1733 7.32405C17.0372 7.14005 16.8483 6.99312 16.4705 6.69927L10.8186 2.30333C10.5258 2.07562 10.3794 1.96177 10.2178 1.918C10.0752 1.87938 9.92484 1.87938 9.78221 1.918C9.62057 1.96177 9.47418 2.07562 9.18141 2.30333Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
),
|
||||
label: "Company Wiki",
|
||||
path: "/company-wiki",
|
||||
active: location.pathname === "/company-wiki"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.6663 9.16666H6.66634M8.33301 12.5H6.66634M13.333 5.83332H6.66634M16.6663 5.66666V14.3333C16.6663 15.7335 16.6663 16.4335 16.3939 16.9683C16.1542 17.4387 15.7717 17.8212 15.3013 18.0608C14.7665 18.3333 14.0665 18.3333 12.6663 18.3333H7.33301C5.93288 18.3333 5.23281 18.3333 4.69803 18.0608C4.22763 17.8212 3.84517 17.4387 3.60549 16.9683C3.33301 16.4335 3.33301 15.7335 3.33301 14.3333V5.66666C3.33301 4.26653 3.33301 3.56646 3.60549 3.03168C3.84517 2.56128 4.22763 2.17882 4.69803 1.93914C5.23281 1.66666 5.93288 1.66666 7.33301 1.66666H12.6663C14.0665 1.66666 14.7665 1.66666 15.3013 1.93914C15.7717 2.17882 16.1542 2.56128 16.3939 3.03168C16.6663 3.56646 16.6663 4.26653 16.6663 5.66666Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
),
|
||||
label: "Submissions",
|
||||
path: "/submissions",
|
||||
active: location.pathname === "/submissions"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_914_468)">
|
||||
<path d="M10.0003 1.66666C11.0947 1.66666 12.1783 1.8822 13.1894 2.30099C14.2004 2.71978 15.1191 3.33361 15.8929 4.10744C16.6667 4.88126 17.2805 5.79992 17.6993 6.81097C18.1181 7.82201 18.3337 8.90565 18.3337 10M10.0003 1.66666V9.99999M10.0003 1.66666C5.39795 1.66666 1.66699 5.39762 1.66699 9.99999C1.66699 14.6024 5.39795 18.3333 10.0003 18.3333C14.6027 18.3333 18.3337 14.6024 18.3337 10M10.0003 1.66666C14.6027 1.66666 18.3337 5.39762 18.3337 10M18.3337 10L10.0003 9.99999M18.3337 10C18.3337 11.3151 18.0224 12.6115 17.4254 13.7832C16.8283 14.955 15.9625 15.9688 14.8985 16.7418L10.0003 9.99999" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_914_468">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
label: "Reports",
|
||||
path: "/reports",
|
||||
active: location.pathname === "/reports" || location.pathname === "/"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.4999 9.58333C17.4999 13.4953 14.3285 16.6667 10.4165 16.6667C9.5192 16.6667 8.66086 16.4998 7.87081 16.1954C7.72637 16.1398 7.65415 16.112 7.59671 16.0987C7.54022 16.0857 7.49933 16.0803 7.4414 16.0781C7.3825 16.0758 7.31789 16.0825 7.18865 16.0958L2.92113 16.537C2.51427 16.579 2.31083 16.6001 2.19083 16.5269C2.08631 16.4631 2.01512 16.3566 1.99617 16.2356C1.97441 16.0968 2.07162 15.9168 2.26605 15.557L3.62909 13.034C3.74135 12.8262 3.79747 12.7223 3.82289 12.6225C3.848 12.5238 3.85407 12.4527 3.84604 12.3512C3.83791 12.2484 3.79283 12.1147 3.70266 11.8472C3.46306 11.1363 3.33318 10.375 3.33318 9.58333C3.33318 5.67132 6.5045 2.5 10.4165 2.5C14.3285 2.5 17.4999 5.67132 17.4999 9.58333Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
),
|
||||
label: "Chat",
|
||||
path: "/chat",
|
||||
active: location.pathname === "/chat"
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_914_474)">
|
||||
<path d="M7.57533 7.50001C7.77125 6.94306 8.15795 6.47343 8.66695 6.17428C9.17596 5.87514 9.77441 5.76579 10.3563 5.8656C10.9382 5.96541 11.466 6.26794 11.8462 6.71961C12.2264 7.17128 12.4345 7.74294 12.4337 8.33334C12.4337 10 9.93366 10.8333 9.93366 10.8333M10.0003 14.1667H10.0087M18.3337 10C18.3337 14.6024 14.6027 18.3333 10.0003 18.3333C5.39795 18.3333 1.66699 14.6024 1.66699 10C1.66699 5.39763 5.39795 1.66667 10.0003 1.66667C14.6027 1.66667 18.3337 5.39763 18.3337 10Z" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_914_474">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
label: "Help",
|
||||
path: "/help",
|
||||
active: location.pathname === "/help"
|
||||
}
|
||||
];
|
||||
|
||||
const settingsIcon = (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_914_477)">
|
||||
<path d="M10.0003 12.5C11.381 12.5 12.5003 11.3807 12.5003 10C12.5003 8.61929 11.381 7.5 10.0003 7.5C8.61961 7.5 7.50033 8.61929 7.50033 10C7.50033 11.3807 8.61961 12.5 10.0003 12.5Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M15.6064 12.2727C15.5055 12.5012 15.4755 12.7547 15.52 13.0004C15.5646 13.2462 15.6817 13.473 15.8564 13.6515L15.9018 13.697C16.0427 13.8377 16.1545 14.0048 16.2307 14.1887C16.307 14.3727 16.3462 14.5698 16.3462 14.7689C16.3462 14.9681 16.307 15.1652 16.2307 15.3492C16.1545 15.5331 16.0427 15.7002 15.9018 15.8409C15.7611 15.9818 15.594 16.0935 15.4101 16.1698C15.2261 16.246 15.029 16.2853 14.8299 16.2853C14.6308 16.2853 14.4336 16.246 14.2497 16.1698C14.0657 16.0935 13.8986 15.9818 13.7579 15.8409L13.7124 15.7955C13.5339 15.6208 13.3071 15.5036 13.0614 15.4591C12.8156 15.4145 12.5622 15.4446 12.3337 15.5455C12.1096 15.6415 11.9185 15.8009 11.7839 16.0042C11.6493 16.2074 11.5771 16.4456 11.5761 16.6894V16.8182C11.5761 17.22 11.4165 17.6054 11.1323 17.8896C10.8482 18.1737 10.4628 18.3333 10.0609 18.3333C9.65909 18.3333 9.2737 18.1737 8.98956 17.8896C8.70541 17.6054 8.54578 17.22 8.54578 16.8182V16.75C8.53991 16.4992 8.45875 16.2561 8.31283 16.052C8.16692 15.848 7.963 15.6926 7.7276 15.6061C7.4991 15.5052 7.24563 15.4751 6.99988 15.5197C6.75413 15.5643 6.52736 15.6814 6.34881 15.8561L6.30336 15.9015C6.16264 16.0424 5.99554 16.1541 5.8116 16.2304C5.62766 16.3066 5.4305 16.3459 5.23139 16.3459C5.03227 16.3459 4.83511 16.3066 4.65117 16.2304C4.46724 16.1541 4.30013 16.0424 4.15942 15.9015C4.01854 15.7608 3.90679 15.5937 3.83054 15.4098C3.75429 15.2258 3.71504 15.0287 3.71504 14.8295C3.71504 14.6304 3.75429 14.4333 3.83054 14.2493C3.90679 14.0654 4.01854 13.8983 4.15942 13.7576L4.20487 13.7121C4.37952 13.5336 4.49668 13.3068 4.54124 13.0611C4.5858 12.8153 4.55572 12.5618 4.45487 12.3333C4.35884 12.1093 4.19938 11.9182 3.99613 11.7836C3.79288 11.649 3.55471 11.5767 3.31093 11.5758H3.18214C2.7803 11.5758 2.39492 11.4161 2.11077 11.132C1.82662 10.8478 1.66699 10.4624 1.66699 10.0606C1.66699 9.65876 1.82662 9.27338 2.11077 8.98923C2.39492 8.70509 2.7803 8.54545 3.18214 8.54545H3.25033C3.50108 8.53959 3.74427 8.45842 3.94828 8.31251C4.15229 8.16659 4.30769 7.96268 4.39427 7.72727C4.49511 7.49878 4.52519 7.24531 4.48063 6.99955C4.43607 6.7538 4.31891 6.52703 4.14427 6.34848L4.09881 6.30303C3.95794 6.16231 3.84618 5.99521 3.76993 5.81127C3.69368 5.62734 3.65444 5.43018 3.65444 5.23106C3.65444 5.03195 3.69368 4.83478 3.76993 4.65085C3.84618 4.46691 3.95794 4.29981 4.09881 4.15909C4.23953 4.01822 4.40663 3.90646 4.59057 3.83021C4.7745 3.75396 4.97167 3.71472 5.17078 3.71472C5.36989 3.71472 5.56706 3.75396 5.75099 3.83021C5.93493 3.90646 6.10203 4.01822 6.24275 4.15909L6.2882 4.20455C6.46675 4.37919 6.69352 4.49635 6.93927 4.54091C7.18503 4.58547 7.4385 4.55539 7.66699 4.45455H7.7276C7.95167 4.35851 8.14276 4.19906 8.27737 3.99581C8.41197 3.79256 8.4842 3.55438 8.48517 3.31061V3.18182C8.48517 2.77998 8.64481 2.39459 8.92895 2.11044C9.2131 1.8263 9.59848 1.66667 10.0003 1.66667C10.4022 1.66667 10.7876 1.8263 11.0717 2.11044C11.3558 2.39459 11.5155 2.77998 11.5155 3.18182V3.25C11.5164 3.49378 11.5887 3.73195 11.7233 3.9352C11.8579 4.13845 12.049 4.29791 12.2731 4.39394C12.5016 4.49478 12.755 4.52487 13.0008 4.48031C13.2465 4.43575 13.4733 4.31859 13.6518 4.14394L13.6973 4.09848C13.838 3.95761 14.0051 3.84586 14.1891 3.76961C14.373 3.69336 14.5702 3.65411 14.7693 3.65411C14.9684 3.65411 15.1655 3.69336 15.3495 3.76961C15.5334 3.84586 15.7005 3.95761 15.8412 4.09848C15.9821 4.2392 16.0939 4.40631 16.1701 4.59024C16.2464 4.77418 16.2856 4.97134 16.2856 5.17045C16.2856 5.36957 16.2464 5.56673 16.1701 5.75067C16.0939 5.9346 15.9821 6.10171 15.8412 6.24242L15.7958 6.28788C15.6211 6.46642 15.504 6.69319 15.4594 6.93895C15.4149 7.1847 15.4449 7.43817 15.5458 7.66667V7.72727C15.6418 7.95134 15.8013 8.14244 16.0045 8.27704C16.2078 8.41164 16.4459 8.48388 16.6897 8.48485H16.8185C17.2204 8.48485 17.6057 8.64448 17.8899 8.92863C18.174 9.21277 18.3337 9.59816 18.3337 10C18.3337 10.4018 18.174 10.7872 17.8899 11.0714C17.6057 11.3555 17.2204 11.5152 16.8185 11.5152H16.7503C16.5065 11.5161 16.2684 11.5884 16.0651 11.723C15.8619 11.8576 15.7024 12.0487 15.6064 12.2727Z" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_914_477">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const handleNavClick = (path: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-64 max-w-64 min-w-64 px-3 pt-4 pb-3 bg-[--Neutrals-NeutralSlate0] border-r border-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-between items-center overflow-hidden">
|
||||
{/* Header Section */}
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
||||
{/* Company Selector */}
|
||||
<div className="w-60 pl-2 pr-4 py-2 bg-[--Neutrals-NeutralSlate0] rounded-3xl outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex justify-between items-center overflow-hidden">
|
||||
<div className="flex-1 flex justify-start items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full flex justify-start items-center gap-2.5">
|
||||
<div className="w-8 h-8 relative bg-[--Brand-Orange] rounded-full outline outline-[1.60px] outline-offset-[-1.60px] outline-white/10 overflow-hidden">
|
||||
<div className="left-0 top-0 absolute">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" fill="url(#paint0_linear_731_19280)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_731_19280" x1="16" y1="3.97364e-07" x2="17.3333" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.12" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="left-[8.80px] top-[7.20px] absolute">
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_731_19281)">
|
||||
<path opacity="0.5" fillRule="evenodd" clipRule="evenodd" d="M4.34367 10.6873C4.67023 11.018 4.67022 11.5541 4.34366 11.8848L4.32693 11.9018C4.00036 12.2325 3.47089 12.2325 3.14433 11.9018C2.81777 11.5711 2.81778 11.0349 3.14434 10.7042L3.16107 10.6873C3.48764 10.3566 4.0171 10.3566 4.34367 10.6873Z" fill="url(#paint0_linear_731_19281)" />
|
||||
<path opacity="0.7" fillRule="evenodd" clipRule="evenodd" d="M8.2752 10.9423C8.60118 11.2736 8.60022 11.8097 8.27306 12.1398L5.95673 14.477C5.62957 14.8071 5.1001 14.8061 4.77413 14.4748C4.44815 14.1435 4.44911 13.6074 4.77627 13.2773L7.09261 10.9401C7.41976 10.61 7.94923 10.611 8.2752 10.9423Z" fill="url(#paint1_linear_731_19281)" />
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_731_19281" x="0.398828" y="-0.399988" width="19.2014" height="22.4" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<feMorphology radius="1.2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_731_19281" />
|
||||
<feOffset dy="1.8" />
|
||||
<feGaussianBlur stdDeviation="1.8" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.141176 0 0 0 0 0.141176 0 0 0 0 0.141176 0 0 0 0.1 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_731_19281" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_731_19281" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_731_19281" x1="3.744" y1="10.4393" x2="3.744" y2="12.1498" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_731_19281" x1="6.52467" y1="10.6932" x2="6.52467" y2="14.7239" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.8" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 inline-flex flex-col justify-start items-start gap-0.5">
|
||||
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate950] text-base font-medium font-['Inter'] leading-normal">{companyName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83301 12.5L9.99967 16.6667L14.1663 12.5M5.83301 7.50001L9.99967 3.33334L14.1663 7.50001" stroke="var(--Neutrals-NeutralSlate400, #A4A7AE)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-5">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
||||
{navItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleNavClick(item.path)}
|
||||
className={`w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2 cursor-pointer ${item.active
|
||||
? 'bg-[--Neutrals-NeutralSlate100]'
|
||||
: 'hover:bg-[--Neutrals-NeutralSlate50]'
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
{React.cloneElement(item.icon, {
|
||||
stroke: item.active ? "var(--Brand-Orange, #5E48FC)" : "var(--Neutrals-NeutralSlate400, #A4A7AE)"
|
||||
})}
|
||||
</div>
|
||||
<div className={`justify-start text-sm font-medium font-['Inter'] leading-tight ${item.active
|
||||
? 'text-[--Neutrals-NeutralSlate950]'
|
||||
: 'text-[--Neutrals-NeutralSlate500]'
|
||||
}`}>
|
||||
{item.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-3">
|
||||
{/* Settings */}
|
||||
<div
|
||||
onClick={() => handleNavClick("/settings")}
|
||||
className="w-60 px-4 py-2.5 rounded-[34px] inline-flex justify-start items-center gap-2 cursor-pointer hover:bg-[--Neutrals-NeutralSlate50]"
|
||||
>
|
||||
<div className="relative">
|
||||
{settingsIcon}
|
||||
</div>
|
||||
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate500] text-sm font-medium font-['Inter'] leading-tight">Settings</div>
|
||||
</div>
|
||||
|
||||
{/* Build Report Card */}
|
||||
<div className="self-stretch bg-[--Neutrals-NeutralSlate0] rounded-[20px] shadow-[0px_1px_4px_0px_rgba(14,18,27,0.04)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] flex flex-col justify-start items-start overflow-hidden">
|
||||
<div className="self-stretch h-24 relative">
|
||||
<div className="w-60 p-3 origin-top-left rotate-[-28.34deg] bg-[--Neutrals-NeutralSlate0] rounded-xl shadow-[0px_10px_20px_4px_rgba(14,18,27,0.08)] outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate100]
|
||||
inline-flex flex-col justify-start items-start gap-3 overflow-hidden">
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-2">
|
||||
<div className="self-stretch h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||
<div className="w-20 h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
||||
<div className="w-20 h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||
<div className="self-stretch h-5 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||
</div>
|
||||
<div className="self-stretch flex flex-col justify-start items-start gap-1.5">
|
||||
<div className="w-20 h-2 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||
<div className="self-stretch h-5 bg-[--Neutrals-NeutralSlate100] rounded-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch p-3 flex flex-col justify-start items-start gap-1">
|
||||
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-sm font-semibold font-['Inter'] leading-tight">Build [Company]'s Report</div>
|
||||
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-xs font-normal font-['Inter'] leading-none">Share this form with your team members to capture valuable info about your company to train Auditly.</div>
|
||||
</div>
|
||||
<div className="self-stretch px-3 pb-3 flex flex-col justify-start items-start gap-8">
|
||||
<div className="self-stretch inline-flex justify-start items-start gap-2">
|
||||
<div className="flex-1 px-3 py-1.5 bg-[--Neutrals-NeutralSlate100] rounded-[999px] flex justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">Invite</div>
|
||||
</div>
|
||||
<div>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99967 3.33333V12.6667M3.33301 8H12.6663" stroke="var(--Neutrals-NeutralSlate950, #0A0D12)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 px-3 py-1.5 bg-[--Brand-Orange] rounded-[999px] outline outline-1 outline-offset-[-1px] outline-blue-400 flex justify-center items-center gap-0.5 overflow-hidden">
|
||||
<div>
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.97203 12.2426L8.02922 13.1854C6.72748 14.4872 4.61693 14.4872 3.31518 13.1854C2.01343 11.8837 2.01343 9.77312 3.31518 8.47138L4.25799 7.52857M12.7433 8.47138L13.6861 7.52857C14.9878 6.22682 14.9878 4.11627 13.6861 2.81452C12.3843 1.51277 10.2738 1.51277 8.97203 2.81452L8.02922 3.75733M6.16729 10.3333L10.834 5.66662" stroke="var(--Other-White, white)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="px-1 flex justify-center items-center">
|
||||
<div className="justify-center text-white text-sm font-medium font-['Inter'] leading-tight">Copy</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/components/ui/Alert.tsx
Normal file
35
src/components/ui/Alert.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
type AlertVariant = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
const variantStyles: Record<AlertVariant, string> = {
|
||||
info: 'bg-blue-50 text-blue-800 border-blue-300 dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-700',
|
||||
success: 'bg-green-50 text-green-800 border-green-300 dark:bg-green-900/30 dark:text-green-200 dark:border-green-700',
|
||||
warning: 'bg-amber-50 text-amber-800 border-amber-300 dark:bg-amber-900/30 dark:text-amber-200 dark:border-amber-700',
|
||||
error: 'bg-red-50 text-red-800 border-red-300 dark:bg-red-900/30 dark:text-red-200 dark:border-red-700'
|
||||
};
|
||||
|
||||
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title?: string;
|
||||
variant?: AlertVariant;
|
||||
children?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const Alert: React.FC<AlertProps> = ({ title, variant = 'info', children, icon, compact, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={`border rounded-md ${variantStyles[variant]} ${compact ? 'px-3 py-2 text-sm' : 'px-4 py-3'} flex items-start space-x-3 ${className || ''}`}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <div className="pt-0.5">{icon}</div>}
|
||||
<div className="flex-1">
|
||||
{title && <div className="font-medium mb-0.5">{title}</div>}
|
||||
{children && <div className="leading-snug">{children}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
25
src/components/ui/Breadcrumb.tsx
Normal file
25
src/components/ui/Breadcrumb.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface BreadcrumbItem { label: string; href?: string; }
|
||||
|
||||
export const Breadcrumbs: React.FC<{ items: BreadcrumbItem[]; onNavigate?: (href: string) => void; }> = ({ items, onNavigate }) => (
|
||||
<nav className="text-sm text-[--text-secondary]" aria-label="Breadcrumb">
|
||||
<ol className="flex flex-wrap items-center gap-1">
|
||||
{items.map((item, i) => (
|
||||
<li key={i} className="flex items-center">
|
||||
{item.href ? (
|
||||
<button
|
||||
onClick={() => onNavigate?.(item.href!)}
|
||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||
>{item.label}</button>
|
||||
) : (
|
||||
<span className="text-[--text-primary] font-medium">{item.label}</span>
|
||||
)}
|
||||
{i < items.length - 1 && <span className="mx-2 text-[--border-color]">/</span>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
|
||||
export default Breadcrumbs;
|
||||
1
src/components/ui/Icons.tsx
Normal file
1
src/components/ui/Icons.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { CompanyWikiIcon, SubmissionsIcon, ReportsIcon, ChatIcon, HelpIcon, SettingsIcon, CopyIcon, PlusIcon, ChevronDownIcon, UploadIcon, CheckIcon, WarningIcon, DownloadIcon, MinusIcon, SunIcon, MoonIcon, SystemIcon, SendIcon } from '../UiKit';
|
||||
181
src/components/ui/ImageUpload.tsx
Normal file
181
src/components/ui/ImageUpload.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { StoredImage } from '../../services/imageStorageService';
|
||||
|
||||
interface ImageUploadProps {
|
||||
onImageSelected: (file: File) => void;
|
||||
onImageRemove?: () => void;
|
||||
currentImage?: StoredImage | null;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
className?: string;
|
||||
maxSizeMB?: number;
|
||||
acceptedFormats?: string[];
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
const ImageUpload: React.FC<ImageUploadProps> = ({
|
||||
onImageSelected,
|
||||
onImageRemove,
|
||||
currentImage,
|
||||
loading = false,
|
||||
error,
|
||||
className = '',
|
||||
maxSizeMB = 10,
|
||||
acceptedFormats = ['JPEG', 'PNG', 'GIF', 'WebP'],
|
||||
size = 'medium'
|
||||
}) => {
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const sizeClasses = {
|
||||
small: 'w-12 h-12',
|
||||
medium: 'w-16 h-16',
|
||||
large: 'w-24 h-24'
|
||||
};
|
||||
|
||||
const handleFileSelect = (file: File) => {
|
||||
// Basic validation
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > maxSizeMB * 1024 * 1024) {
|
||||
return;
|
||||
}
|
||||
|
||||
onImageSelected(file);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
handleFileSelect(file);
|
||||
}
|
||||
// Reset input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
handleFileSelect(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (!loading) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (onImageRemove) {
|
||||
onImageRemove();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleInputChange}
|
||||
className="hidden"
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`
|
||||
${sizeClasses[size]} relative rounded-[250px] overflow-hidden
|
||||
cursor-pointer transition-all duration-200
|
||||
${dragOver ? 'ring-2 ring-Brand-Orange ring-opacity-50' : ''}
|
||||
${loading ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-80'}
|
||||
`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{currentImage ? (
|
||||
<>
|
||||
<img
|
||||
src={currentImage.dataUrl}
|
||||
alt="Uploaded"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{!loading && onImageRemove && (
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition-colors"
|
||||
title="Remove image"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
|
||||
{loading ? (
|
||||
<div className="animate-spin w-6 h-6 border-2 border-Brand-Orange border-t-transparent rounded-full" />
|
||||
) : (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-gray-400"
|
||||
>
|
||||
<path
|
||||
d="M12 16L12 8M12 8L8 12M12 8L16 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4 16V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dragOver && (
|
||||
<div className="absolute inset-0 bg-Brand-Orange bg-opacity-20 flex items-center justify-center">
|
||||
<span className="text-Brand-Orange text-xs font-medium">Drop image</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="absolute top-full left-0 mt-1 text-xs text-red-500 whitespace-nowrap">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
49
src/components/ui/Inputs.tsx
Normal file
49
src/components/ui/Inputs.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
interface BaseFieldProps { label?: string; description?: string; error?: string; required?: boolean; children: React.ReactNode; className?: string; }
|
||||
export const Field: React.FC<BaseFieldProps> = ({ label, description, error, required, children, className }) => (
|
||||
<div className={`space-y-2 ${className || ''}`}>
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-[--text-primary] tracking-[-0.14px]">
|
||||
{label} {required && <span className="text-[--status-red]">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
{description && !error && <p className="text-sm text-[--text-secondary] tracking-[-0.14px]">{description}</p>}
|
||||
{error && <p className="text-sm text-[--status-red]">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { invalid?: boolean; }
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, invalid, ...rest }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-3.5 rounded-full border text-sm bg-[--input-bg] text-[--text-primary] placeholder:text-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent] border-[--input-border] transition-all duration-200 ${invalid ? 'border-red-500 focus:ring-red-500' : ''} ${className || ''}`}
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { invalid?: boolean; }
|
||||
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, invalid, ...rest }, ref) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-3.5 rounded-2xl border text-sm resize-vertical bg-[--input-bg] text-[--text-primary] placeholder:text-[--input-placeholder] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent] border-[--input-border] transition-all duration-200 ${invalid ? 'border-red-500 focus:ring-red-500' : ''} ${className || ''}`}
|
||||
{...rest}
|
||||
/>
|
||||
));
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> { invalid?: boolean; }
|
||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(({ className, invalid, children, ...rest }, ref) => (
|
||||
<select
|
||||
ref={ref}
|
||||
className={`w-full px-4 py-3.5 rounded-full border text-sm bg-[--input-bg] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-[--accent] focus:border-[--accent] border-[--input-border] transition-all duration-200 ${invalid ? 'border-red-500 focus:ring-red-500' : ''} ${className || ''}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
));
|
||||
Select.displayName = 'Select';
|
||||
|
||||
export default { Field, Input, Textarea, Select };
|
||||
24
src/components/ui/Progress.tsx
Normal file
24
src/components/ui/Progress.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
export const LinearProgress: React.FC<{ value: number; className?: string; }> = ({ value, className }) => (
|
||||
<div className={`w-full h-2 rounded-full bg-[--background-tertiary] overflow-hidden ${className || ''}`}>
|
||||
<div className="h-full bg-blue-500 transition-all" style={{ width: `${Math.min(100, Math.max(0, value))}%` }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export interface StepProgressProps { current: number; total: number; labels?: string[]; }
|
||||
export const StepProgress: React.FC<StepProgressProps> = ({ current, total, labels }) => (
|
||||
<div>
|
||||
<div className="flex justify-between mb-2">
|
||||
{Array.from({ length: total }).map((_, i) => (
|
||||
<div key={i} className="flex-1 flex flex-col items-center">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold mb-1 ${i <= current ? 'bg-blue-500 text-white' : 'bg-[--background-tertiary] text-[--text-secondary]'}`}>{i + 1}</div>
|
||||
{labels && labels[i] && <span className="text-[10px] text-center px-1 text-[--text-secondary] truncate max-w-[72px]">{labels[i]}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<LinearProgress value={((current + 1) / total) * 100} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default { LinearProgress, StepProgress };
|
||||
8
src/components/ui/Question.tsx
Normal file
8
src/components/ui/Question.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Field } from './Inputs';
|
||||
|
||||
interface QuestionProps { label: string; required?: boolean; description?: string; error?: string; children: React.ReactNode; }
|
||||
export const Question: React.FC<QuestionProps> = ({ label, required, description, error, children }) => (
|
||||
<Field label={label} required={required} description={description} error={error}>{children}</Field>
|
||||
);
|
||||
export default Question;
|
||||
182
src/components/ui/QuestionInput.tsx
Normal file
182
src/components/ui/QuestionInput.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react';
|
||||
import { EmployeeQuestion, EMPLOYEE_QUESTIONS } from '../../employeeQuestions';
|
||||
import { Input, Textarea } from './Inputs';
|
||||
|
||||
interface QuestionInputProps {
|
||||
question: EmployeeQuestion;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
// For yes/no questions with follow-ups, we need access to all answers and ability to set follow-up
|
||||
allAnswers?: Record<string, string>;
|
||||
onFollowupChange?: (questionId: string, value: string) => void;
|
||||
}
|
||||
|
||||
// Helper function to find follow-up question for a given question
|
||||
const findFollowupQuestion = (questionId: string): EmployeeQuestion | null => {
|
||||
return EMPLOYEE_QUESTIONS.find(q => q.followupTo === questionId) || null;
|
||||
};
|
||||
|
||||
export const QuestionInput: React.FC<QuestionInputProps> = ({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
className = '',
|
||||
allAnswers = {},
|
||||
onFollowupChange
|
||||
}) => {
|
||||
const baseInputClasses = "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";
|
||||
|
||||
switch (question.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={question.placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={question.placeholder}
|
||||
className={className}
|
||||
rows={4}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'yesno':
|
||||
const followupQuestion = findFollowupQuestion(question.id);
|
||||
const followupValue = followupQuestion ? allAnswers[followupQuestion.id] || '' : '';
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* Yes/No Radio Buttons */}
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={question.id}
|
||||
value="Yes"
|
||||
checked={value === 'Yes'}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-4 h-4 text-[--accent] border-[--border-color] focus:ring-[--accent]"
|
||||
/>
|
||||
<span className="text-[--text-primary]">Yes</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={question.id}
|
||||
value="No"
|
||||
checked={value === 'No'}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-4 h-4 text-[--accent] border-[--border-color] focus:ring-[--accent]"
|
||||
/>
|
||||
<span className="text-[--text-primary]">No</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Conditional Follow-up Textarea */}
|
||||
{followupQuestion && value === 'Yes' && (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-[--text-primary] tracking-[-0.14px]">
|
||||
{followupQuestion.prompt}
|
||||
</label>
|
||||
<Textarea
|
||||
value={followupValue}
|
||||
onChange={(e) => onFollowupChange?.(followupQuestion.id, e.target.value)}
|
||||
placeholder={followupQuestion.placeholder}
|
||||
rows={3}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'scale':
|
||||
const scaleMin = question.scaleMin || 1;
|
||||
const scaleMax = question.scaleMax || 10;
|
||||
const currentValue = parseInt(value) || scaleMin;
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
<div className="flex w-full justify-between text-sm text-[--text-secondary]">
|
||||
<span>{question.scaleLabels?.min || `${scaleMin}`}</span>
|
||||
<span>{question.scaleLabels?.max || `${scaleMax}`}</span>
|
||||
</div>
|
||||
|
||||
{/* Grid container that aligns slider and numbers */}
|
||||
<div className="grid grid-cols-[auto_1fr_auto] gap-4 items-center">
|
||||
{/* Left label space - dynamically sized */}
|
||||
<div className="text-xs -me-12 text-transparent select-none">
|
||||
{question.scaleLabels?.min || `${scaleMin}`}
|
||||
</div>
|
||||
|
||||
{/* Slider container */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="range"
|
||||
min={scaleMin}
|
||||
max={scaleMax}
|
||||
value={currentValue}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
|
||||
/>
|
||||
{/* Numbers positioned absolutely under the slider */}
|
||||
<div className="flex justify-between absolute -bottom-5 left-0 right-0 text-xs text-[--text-secondary]">
|
||||
{Array.from({ length: scaleMax - scaleMin + 1 }, (_, i) => (
|
||||
<span key={i} className="w-4 text-center">
|
||||
{scaleMin + i}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current value badge */}
|
||||
<div className="w-12 h-8 bg-[--accent] text-white rounded flex items-center justify-center text-sm font-medium">
|
||||
{currentValue}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add some bottom padding to account for the absolute positioned numbers */}
|
||||
<div className="h-4"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={`${baseInputClasses} ${className}`}
|
||||
>
|
||||
<option value="">Select an option...</option>
|
||||
{question.options?.map((option, index) => (
|
||||
<option key={index} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={question.placeholder || "Type your answer here..."}
|
||||
className={`${baseInputClasses} min-h-[100px] resize-vertical ${className}`}
|
||||
rows={4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default QuestionInput;
|
||||
11
src/components/ui/Table.tsx
Normal file
11
src/components/ui/Table.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Table: React.FC<React.TableHTMLAttributes<HTMLTableElement>> = ({ className, children, ...rest }) => (
|
||||
<table className={`w-full text-sm border-separate border-spacing-0 ${className || ''}`} {...rest}>{children}</table>
|
||||
);
|
||||
export const THead: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({ className, children, ...rest }) => (<thead className={`${className || ''}`} {...rest}>{children}</thead>);
|
||||
export const TBody: React.FC<React.HTMLAttributes<HTMLTableSectionElement>> = ({ className, children, ...rest }) => (<tbody className={`${className || ''}`} {...rest}>{children}</tbody>);
|
||||
export const TR: React.FC<React.HTMLAttributes<HTMLTableRowElement>> = ({ className, children, ...rest }) => (<tr className={`hover:bg-[--background-tertiary] transition-colors ${className || ''}`} {...rest}>{children}</tr>);
|
||||
export const TH: React.FC<React.ThHTMLAttributes<HTMLTableHeaderCellElement>> = ({ className, children, ...rest }) => (<th className={`text-left font-medium px-4 py-2 text-[--text-secondary] border-b border-[--border-color] bg-[--background-secondary] first:rounded-tl-md last:rounded-tr-md ${className || ''}`} {...rest}>{children}</th>);
|
||||
export const TD: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = ({ className, children, ...rest }) => (<td className={`px-4 py-2 border-b border-[--border-color] text-[--text-primary] ${className || ''}`} {...rest}>{children}</td>);
|
||||
export default { Table, THead, TBody, TR, TH, TD };
|
||||
8
src/components/ui/index.ts
Normal file
8
src/components/ui/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './Alert';
|
||||
export * from './Breadcrumb';
|
||||
export * from './Icons';
|
||||
export * from './Inputs';
|
||||
export * from './Progress';
|
||||
export * from './Question';
|
||||
export * from './QuestionInput';
|
||||
export * from './Table';
|
||||
Reference in New Issue
Block a user