Fix organization setup flow: redirect to onboarding for incomplete setup
This commit is contained in:
35
components/ui/Alert.tsx
Normal file
35
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
components/ui/Breadcrumb.tsx
Normal file
25
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
components/ui/Icons.tsx
Normal file
1
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';
|
||||
49
components/ui/Inputs.tsx
Normal file
49
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
components/ui/Progress.tsx
Normal file
24
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
components/ui/Question.tsx
Normal file
8
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
components/ui/QuestionInput.tsx
Normal file
182
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
components/ui/Table.tsx
Normal file
11
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
components/ui/index.ts
Normal file
8
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