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.