Key Frontend Components
Job Components
JobSwiper
File: components/jobs/JobSwiper.tsx
TikTok-style vertical swipe interface for browsing jobs.
Features
- Full-screen vertical swipe
- Touch, mouse wheel, and keyboard navigation
- Animated transitions with Framer Motion
- Progress indicator
- First-time tutorial
Props
interface JobSwiperProps {
job: Job;
currentIndex: number;
totalJobs: number;
isLoading: boolean;
onSave: () => void;
onNext: () => void;
onPrev: () => void;
onRefresh: () => void;
}Touch Handling
const minSwipeDistance = 50;
const onTouchStart = (e: React.TouchEvent) => {
setTouchStart(e.targetTouches[0].clientY);
};
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isUpSwipe = distance > minSwipeDistance;
const isDownSwipe = distance < -minSwipeDistance;
if (isUpSwipe) handleNext();
if (isDownSwipe) handlePrev();
};Body Scroll Lock
useEffect(() => {
// Lock body scroll when swiper is mounted
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.touchAction = 'none';
return () => {
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.touchAction = '';
};
}, []);JobCard
File: components/jobs/JobCard.tsx
Full-screen job card displayed in the swiper.
Features
- Company logo and info header
- Salary with currency
- Job type and experience level badges
- Expandable description
- Requirements and benefits lists
- Action buttons (Save, Apply, Share)
Layout
<div className="h-full bg-gradient-to-b from-gray-900 to-black text-white">
{/* Header: Company info */}
<div className="p-4 flex items-center gap-3">
<img src={job.employer?.logo} className="w-12 h-12 rounded-full" />
<div>
<h2 className="font-bold text-lg">{job.title}</h2>
<p className="text-gray-300">{job.company}</p>
</div>
</div>
{/* Salary */}
<div className="text-2xl font-bold text-orange-500">
{formatCurrency(job.salary, job.currency)}/month
</div>
{/* Badges */}
<div className="flex gap-2">
<Badge>{job.jobType.replace('_', ' ')}</Badge>
<Badge>{job.experienceLevel}</Badge>
<Badge>{job.location}</Badge>
</div>
{/* Description */}
<ExpandableDescription content={job.description} />
{/* Action buttons - sticky bottom */}
<div className="fixed bottom-0 left-0 right-0 p-4 flex gap-3">
<Button onClick={onSave} variant="outline">
<Heart /> Save
</Button>
<Button onClick={onApply} variant="primary" className="flex-1">
Apply Now
</Button>
</div>
</div>SwipeTutorial
File: components/jobs/SwipeTutorial.tsx
First-time user tutorial for swipe interface.
Content
- Swipe up for next job
- Swipe down for previous
- Tap to expand details
- Heart to save
- Apply button to apply
Message Components
ChatView
File: components/messages/ChatView.tsx
Full chat interface for conversations.
Props
interface ChatViewProps {
conversation: ConversationWithMessages;
currentUserId: string;
typingUsers: TypingUser[];
isLoadingMessages: boolean;
isSending: boolean;
hasMoreMessages: boolean;
onBack: () => void;
onSend: (content: string, images?: string[], replyToId?: string) => void;
onLoadMore: () => void;
onDelete: (messageId: string) => void;
onTyping: (isTyping: boolean) => void;
onTogglePin: () => void;
onToggleMute: () => void;
onArchive: () => void;
}Features
- Message list with infinite scroll
- Real-time typing indicators
- Online status display
- Pin/mute/archive actions
- Reply to messages
- Image attachments
MessageBubble
File: components/messages/MessageBubble.tsx
Individual message bubble with actions.
Variants
- Own message: Aligned right, colored bubble
- Other's message: Aligned left, gray bubble
- System message: Centered, muted style
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={cn(
'max-w-[75%] rounded-2xl p-3',
isOwn ? 'bg-orange-500 text-white ml-auto' : 'bg-gray-100 text-gray-900'
)}
>
{message.replyTo && <ReplyPreview message={message.replyTo} />}
{message.images?.length > 0 && <ImageGrid images={message.images} />}
<p>{message.content}</p>
<div className="flex items-center gap-1 text-xs opacity-70">
<span>{formatTime(message.createdAt)}</span>
{isOwn && <StatusIcon status={message.status} />}
</div>
</motion.div>MessageInput
File: components/messages/MessageInput.tsx
Composer for sending messages.
Features
- Text input with auto-resize
- Image attachment button
- Reply preview with cancel
- Send button with loading state
- Typing indicator trigger
<div className="border-t bg-white p-3">
{replyTo && (
<div className="flex items-center gap-2 mb-2 p-2 bg-gray-100 rounded">
<span>Replying to {replyTo.sender.name}</span>
<button onClick={onCancelReply}><X /></button>
</div>
)}
<div className="flex items-end gap-2">
<button onClick={openImagePicker}><Image /></button>
<textarea
ref={inputRef}
value={message}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
className="flex-1 resize-none"
/>
<button
onClick={handleSend}
disabled={!message.trim() || isSending}
>
{isSending ? <Loader2 className="animate-spin" /> : <Send />}
</button>
</div>
</div>Okia Components
OkiaInterviewPage
File: components/okia/OkiaInterviewPage.tsx
Full-screen AI interview interface.
Props
interface OkiaInterviewPageProps {
sessionToken: string;
}States
loading- Fetching sessionready- Session loaded, ready to startin_progress- Interview activepaused- Interview pausedcompleted- Interview finished, showing report
Layout
<div className="h-screen flex flex-col bg-gradient-to-br from-purple-900 to-indigo-900">
{/* Header with timer */}
<header className="p-4 flex justify-between items-center">
<div className="flex items-center gap-2">
<OkiaLogo />
<span>AI Interview</span>
</div>
{status === 'in_progress' && <Timer timeRemaining={timeRemaining} />}
</header>
{/* Chat area */}
<div className="flex-1 overflow-y-auto p-4">
<AnimatePresence>
{messages.map(msg => (
<InterviewMessage key={msg.id} message={msg} />
))}
</AnimatePresence>
{isTyping && <OkiaTypingIndicator />}
</div>
{/* Quick response suggestions */}
{currentQuestion?.suggestions && (
<div className="flex gap-2 p-2 overflow-x-auto">
{currentQuestion.suggestions.map(s => (
<SuggestionChip key={s} onClick={() => sendMessage(s)}>{s}</SuggestionChip>
))}
</div>
)}
{/* Input */}
<InterviewInput onSend={sendMessage} disabled={status !== 'in_progress'} />
</div>UI Components
Button
File: components/ui/Button.tsx
Reusable button with variants.
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
isLoading,
children,
disabled,
...props
}) => {
const variants = {
primary: 'bg-orange-500 hover:bg-orange-600 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900',
outline: 'border-2 border-orange-500 text-orange-500 hover:bg-orange-50',
ghost: 'text-gray-600 hover:bg-gray-100',
danger: 'bg-red-500 hover:bg-red-600 text-white',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={cn(
'rounded-lg font-medium transition-colors disabled:opacity-50',
variants[variant],
sizes[size]
)}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? <Loader2 className="animate-spin" /> : children}
</button>
);
};Modal
File: components/ui/Modal.tsx
Animated modal dialog.
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
size = 'md',
}) => {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-40"
onClick={onClose}
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className={cn(
'fixed z-50 bg-white rounded-xl shadow-xl',
'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
sizes[size]
)}
>
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold">{title}</h2>
<button onClick={onClose}><X /></button>
</div>
<div className="p-4">
{children}
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};Auth Components
LoginModal
File: components/auth/LoginModal.tsx
Multi-step auth modal with phone verification.
Tabs
- Login - Phone + password
- Register - Phone verification → Complete profile
Phone Verification Flow
- Enter phone number
- Receive SMS code
- Enter 6-digit code
- Get verification token
- Complete registration with token
const [step, setStep] = useState<'phone' | 'verify' | 'complete'>('phone');
const [verificationToken, setVerificationToken] = useState<string | null>(null);
// Step 1: Send verification code
const handleSendCode = async () => {
await api.post('/auth/send-verification-code', { phone, userType: 'user' });
setStep('verify');
};
// Step 2: Verify code
const handleVerifyCode = async () => {
const result = await api.post('/auth/verify-code', { phone, code, userType: 'user' });
setVerificationToken(result.data.verificationToken);
setStep('complete');
};
// Step 3: Complete registration
const handleRegister = async (formData) => {
await api.post('/auth/register/user', {
verificationToken,
...formData,
});
// Auto-login after registration
};