Back to Docs

Examples

Complete code examples for common integration patterns.

Complete Hiring Portal

Build a full-featured hiring portal with interview scheduling, candidate management, and results review.

// pages/interviews/index.tsx
import { InterviewProvider, InterviewList, useInterviews, useProject } from '@codiris/interview-sdk/react';

function HiringPortal() {
  return (
    <InterviewProvider apiKey={process.env.NEXT_PUBLIC_INTERVIEW_API_KEY!}>
      <CandidatesDashboard />
    </InterviewProvider>
  );
}

function CandidatesDashboard() {
  const projectId = 'your-project-id';
  const { project, loading: projectLoading } = useProject(projectId);
  const { interviews, loading: interviewsLoading, refetch } = useInterviews(projectId, {
    status: ['completed', 'in_progress'],
    pageSize: 50,
  });

  if (projectLoading || interviewsLoading) return <Loading />;

  return (
    <div className="p-8">
      <header className="mb-8">
        <h1 className="text-2xl font-bold">{project?.name}</h1>
        <p className="text-gray-600">
          {interviews?.total || 0} candidates interviewed
        </p>
      </header>

      {/* Stats */}
      <div className="grid grid-cols-4 gap-4 mb-8">
        <StatCard
          label="Total"
          value={interviews?.total || 0}
        />
        <StatCard
          label="Completed"
          value={interviews?.items.filter(i => i.status === 'completed').length || 0}
        />
        <StatCard
          label="In Progress"
          value={interviews?.items.filter(i => i.status === 'in_progress').length || 0}
        />
        <StatCard
          label="Avg Rating"
          value={calculateAvgRating(interviews?.items || [])}
        />
      </div>

      {/* Interview List */}
      <InterviewList
        interviews={interviews?.items || []}
        loading={interviewsLoading}
        emptyMessage="No candidates yet. Share your interview link!"
        onInterviewClick={(interview) => {
          window.location.href = `/interviews/${interview.id}`;
        }}
      />
    </div>
  );
}

In-App Feedback Widget

Add a floating feedback widget to your SaaS application.

// components/FeedbackWidget.tsx
import { InterviewProvider, InterviewWidget } from '@codiris/interview-sdk/react';

export function FeedbackWidget({ userId, userEmail }: { userId: string; userEmail: string }) {
  return (
    <InterviewProvider apiKey={process.env.NEXT_PUBLIC_INTERVIEW_API_KEY!}>
      <InterviewWidget
        projectId="feedback-project-id"
        position="bottom-right"
        triggerType="button"
        buttonText="Give Feedback"
        buttonStyle={{
          backgroundColor: '#6366f1',
          textColor: '#ffffff',
          borderRadius: '50px',
        }}
        // Pass user context
        customVariables={{
          userId,
          userEmail,
          page: window.location.pathname,
        }}
        onComplete={(interview) => {
          // Track in analytics
          analytics.track('Feedback Submitted', {
            interviewId: interview.id,
            userId,
          });

          // Show thank you message
          toast.success('Thanks for your feedback!');
        }}
      />
    </InterviewProvider>
  );
}

// Usage in your app layout
function AppLayout({ children }) {
  const { user } = useAuth();

  return (
    <div>
      {children}
      {user && (
        <FeedbackWidget userId={user.id} userEmail={user.email} />
      )}
    </div>
  );
}

User Research Dashboard

Build a research insights dashboard with theme analysis and quote extraction.

// pages/research/[projectId].tsx
import {
  InterviewProvider,
  useProject,
  useInterviews,
  useInterviewResults,
  TranscriptViewer
} from '@codiris/interview-sdk/react';
import { useState } from 'react';

function ResearchDashboard({ projectId }: { projectId: string }) {
  const { project } = useProject(projectId);
  const { interviews } = useInterviews(projectId, { status: ['completed'] });
  const [selectedInterview, setSelectedInterview] = useState<string | null>(null);

  // Aggregate themes from all completed interviews
  const allThemes = useMemo(() => {
    const themeMap = new Map<string, { count: number; quotes: string[] }>();

    interviews?.items.forEach(interview => {
      interview.results?.themes?.forEach(theme => {
        const existing = themeMap.get(theme.name) || { count: 0, quotes: [] };
        themeMap.set(theme.name, {
          count: existing.count + theme.frequency,
          quotes: [...existing.quotes, ...theme.quotes.slice(0, 2)],
        });
      });
    });

    return Array.from(themeMap.entries())
      .map(([name, data]) => ({ name, ...data }))
      .sort((a, b) => b.count - a.count);
  }, [interviews]);

  return (
    <div className="grid grid-cols-3 gap-8 p-8">
      {/* Themes Panel */}
      <div className="col-span-1">
        <h2 className="text-xl font-bold mb-4">Themes</h2>
        {allThemes.map(theme => (
          <div key={theme.name} className="p-4 bg-gray-100 rounded-lg mb-3">
            <div className="flex justify-between items-center mb-2">
              <span className="font-medium">{theme.name}</span>
              <span className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
                {theme.count} mentions
              </span>
            </div>
            <div className="text-sm text-gray-600 space-y-1">
              {theme.quotes.slice(0, 2).map((quote, i) => (
                <p key={i} className="italic">"{quote}"</p>
              ))}
            </div>
          </div>
        ))}
      </div>

      {/* Interviews List */}
      <div className="col-span-1">
        <h2 className="text-xl font-bold mb-4">Interviews ({interviews?.total})</h2>
        {interviews?.items.map(interview => (
          <div
            key={interview.id}
            onClick={() => setSelectedInterview(interview.id)}
            className={`p-4 border rounded-lg mb-3 cursor-pointer ${
              selectedInterview === interview.id ? 'border-blue-500 bg-blue-50' : ''
            }`}
          >
            <p className="font-medium">{interview.candidateName || 'Anonymous'}</p>
            <p className="text-sm text-gray-500">
              {new Date(interview.createdAt).toLocaleDateString()}
            </p>
          </div>
        ))}
      </div>

      {/* Transcript Panel */}
      <div className="col-span-1">
        {selectedInterview && (
          <InterviewDetails interviewId={selectedInterview} />
        )}
      </div>
    </div>
  );
}

function InterviewDetails({ interviewId }: { interviewId: string }) {
  const { interview, loading } = useInterview(interviewId);
  const { results } = useInterviewResults(interviewId);

  if (loading) return <Loading />;

  return (
    <div>
      <h2 className="text-xl font-bold mb-4">Interview Details</h2>

      {/* Results Summary */}
      {results && (
        <div className="bg-gray-50 p-4 rounded-lg mb-4">
          <h3 className="font-medium mb-2">Key Insights</h3>
          <ul className="list-disc list-inside text-sm text-gray-600">
            {results.painPoints?.map((point, i) => (
              <li key={i}>{point}</li>
            ))}
          </ul>
        </div>
      )}

      {/* Transcript */}
      <h3 className="font-medium mb-2">Transcript</h3>
      <TranscriptViewer
        transcript={interview?.transcript || []}
        showTimestamps
        style={{ maxHeight: '400px' }}
      />
    </div>
  );
}

Email Invitation System

Send personalized interview invitations to candidates.

// lib/interview-invitations.ts
import { InterviewClient } from '@codiris/interview-sdk';

const client = new InterviewClient({
  apiKey: process.env.INTERVIEW_API_KEY!,
});

interface InviteCandidate {
  email: string;
  name: string;
  position?: string;
}

export async function sendInterviewInvitations(
  projectId: string,
  candidates: InviteCandidate[]
) {
  const results = [];

  for (const candidate of candidates) {
    try {
      // Create a unique token for this candidate
      const { data: token } = await client.createToken(projectId, {
        inviteeEmail: candidate.email,
        inviteeName: candidate.name,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
      });

      // Get the interview URL with the token
      const interviewUrl = client.getInterviewUrl(projectId, {
        token: token.token,
        candidateName: candidate.name,
        candidateEmail: candidate.email,
      });

      // Send email via your email provider (e.g., Resend, SendGrid)
      await sendEmail({
        to: candidate.email,
        subject: `Interview Invitation - ${candidate.position || 'Open Position'}`,
        html: `
          <h1>Hi ${candidate.name},</h1>
          <p>You've been invited to complete an interview for the ${candidate.position || 'open position'}.</p>
          <p>
            <a href="${interviewUrl}" style="
              display: inline-block;
              padding: 12px 24px;
              background: #6366f1;
              color: white;
              text-decoration: none;
              border-radius: 8px;
            ">
              Start Interview
            </a>
          </p>
          <p>This link expires in 7 days.</p>
        `,
      });

      results.push({ candidate, success: true, tokenId: token.id });
    } catch (error) {
      results.push({ candidate, success: false, error: error.message });
    }
  }

  return results;
}

// API route to send invitations
// pages/api/send-invitations.ts
export async function POST(req: Request) {
  const { projectId, candidates } = await req.json();

  const results = await sendInterviewInvitations(projectId, candidates);

  const successful = results.filter(r => r.success).length;
  const failed = results.filter(r => !r.success).length;

  return Response.json({
    message: `Sent ${successful} invitations, ${failed} failed`,
    results,
  });
}

Webhook Integration

Handle interview completion webhooks to trigger workflows.

// pages/api/webhooks/interview.ts
import { InterviewClient } from '@codiris/interview-sdk';
import crypto from 'crypto';

const client = new InterviewClient({
  apiKey: process.env.INTERVIEW_API_KEY!,
});

export async function POST(req: Request) {
  // Verify webhook signature
  const signature = req.headers.get('x-codiris-signature');
  const body = await req.text();

  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(body)
    .digest('hex');

  if (signature !== expectedSignature) {
    return Response.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.type) {
    case 'interview.completed': {
      const { interviewId, projectId, candidateEmail } = event.data;

      // Fetch full interview data
      const { data: interview } = await client.getInterview(interviewId);
      const { data: results } = await client.getResults(interviewId);

      // Update your ATS or CRM
      await updateATS({
        candidateEmail,
        interviewId,
        status: 'completed',
        score: results?.overallScore,
        recommendation: results?.recommendation,
      });

      // Notify hiring manager
      await sendSlackNotification({
        channel: '#hiring',
        text: `New interview completed for ${interview.candidateName}\nScore: ${results?.overallScore}/100\nRecommendation: ${results?.recommendation}`,
      });

      // If high score, auto-advance candidate
      if (results?.overallScore >= 80) {
        await scheduleNextRound(candidateEmail);
      }

      break;
    }

    case 'interview.started': {
      // Track in analytics
      await analytics.track('Interview Started', event.data);
      break;
    }

    case 'token.expired': {
      // Send reminder email
      const { tokenId, inviteeEmail } = event.data;
      await sendReminderEmail(inviteeEmail);
      break;
    }
  }

  return Response.json({ received: true });
}

Export to Brainboard

Create visual Brainboards from interview insights.

// components/ExportToBrainboard.tsx
import { useCreateBoardFromInterview } from '@codiris/interview-sdk/react';

export function ExportToBrainboard({ interviewId }: { interviewId: string }) {
  const { createBoard, loading, error, boardUrl } = useCreateBoardFromInterview();

  const handleExport = async () => {
    const result = await createBoard(interviewId, 'Interview Insights');
    if (result.success) {
      // Open in new tab
      window.open(result.boardUrl, '_blank');
    }
  };

  if (boardUrl) {
    return (
      <a
        href={boardUrl}
        target="_blank"
        className="text-blue-600 hover:underline"
      >
        View Brainboard →
      </a>
    );
  }

  return (
    <button
      onClick={handleExport}
      disabled={loading}
      className="px-4 py-2 bg-purple-600 text-white rounded-lg disabled:opacity-50"
    >
      {loading ? 'Creating...' : 'Export to Brainboard'}
    </button>
  );
}

// Server-side export with themes
// lib/export-to-brainboard.ts
import { InterviewClient } from '@codiris/interview-sdk';
import { BrainboardClient } from 'codiris-brainboard-sdk';

const interviewClient = new InterviewClient({ apiKey: process.env.INTERVIEW_API_KEY! });
const brainboardClient = new BrainboardClient({ apiKey: process.env.BRAINBOARD_API_KEY! });

export async function createInsightsBoard(projectId: string) {
  // Get all completed interviews
  const { data: interviews } = await interviewClient.listInterviews(projectId, {
    status: ['completed'],
    pageSize: 100,
  });

  // Create board
  const { data: board } = await brainboardClient.createBoard({
    name: `Research Insights - ${new Date().toLocaleDateString()}`,
  });

  // Add title
  await brainboardClient.createObject(board.id, {
    type: 'text',
    x: 100,
    y: 50,
    text: 'User Research Insights',
    fontSize: 48,
    fontWeight: 'bold',
  });

  // Add interview count
  await brainboardClient.createObject(board.id, {
    type: 'sticky',
    x: 100,
    y: 150,
    width: 150,
    height: 100,
    text: `${interviews.total} Interviews\nCompleted`,
    fill: '#e0f2fe',
  });

  // Add themes from all interviews
  let x = 100;
  const allThemes = aggregateThemes(interviews.items);

  for (const theme of allThemes.slice(0, 6)) {
    await brainboardClient.createObject(board.id, {
      type: 'sticky',
      x,
      y: 300,
      width: 200,
      height: 200,
      text: `${theme.name}\n\n${theme.count} mentions\n\n"${theme.quotes[0] || ''}"`,
      fill: '#fef3c7',
    });
    x += 220;
  }

  return board;
}