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