Skip to content

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 ​

typescript
interface JobSwiperProps {
  job: Job;
  currentIndex: number;
  totalJobs: number;
  isLoading: boolean;
  onSave: () => void;
  onNext: () => void;
  onPrev: () => void;
  onRefresh: () => void;
}

Touch Handling ​

typescript
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 ​

typescript
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 ​

tsx
<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 ​

  1. Swipe up for next job
  2. Swipe down for previous
  3. Tap to expand details
  4. Heart to save
  5. Apply button to apply

Message Components ​

ChatView ​

File: components/messages/ChatView.tsx

Full chat interface for conversations.

Props ​

typescript
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
tsx
<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
tsx
<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 ​

typescript
interface OkiaInterviewPageProps {
  sessionToken: string;
}

States ​

  • loading - Fetching session
  • ready - Session loaded, ready to start
  • in_progress - Interview active
  • paused - Interview paused
  • completed - Interview finished, showing report

Layout ​

tsx
<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.

typescript
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>
  );
};

File: components/ui/Modal.tsx

Animated modal dialog.

tsx
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 ​

  1. Enter phone number
  2. Receive SMS code
  3. Enter 6-digit code
  4. Get verification token
  5. Complete registration with token
tsx
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
};

One chat. Everything done.