Get started with FacePing's face recognition API using Next.js
First, create a new Next.js project with TypeScript:
npx create-next-app@latest faceping-demo --typescript
cd faceping-demo
npm install
Create a .env.local
file in your project root:
NEXT_PUBLIC_FACEPING_API_URL=https://api.faceping.ai
FACEPING_API_KEY=your_api_key_here
Create a utility file for API calls (utils/faceping.ts
):
// utils/faceping.ts
export const FACEPING_API_URL = process.env.NEXT_PUBLIC_FACEPING_API_URL;
export async function createGroup(groupName: string) {
try {
const response = await fetch(`${FACEPING_API_URL}/groups/${groupName}`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to create group');
}
return await response.json();
} catch (error) {
console.error('Error creating group:', error);
throw error;
}
}
export async function uploadFace(groupName: string, file: File) {
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch(
`${FACEPING_API_URL}/groups/${groupName}/faces`,
{
method: 'POST',
body: formData
}
);
if (!response.ok) {
throw new Error('Failed to upload face');
}
return await response.json();
} catch (error) {
console.error('Error uploading face:', error);
throw error;
}
}
export async function searchFaces(groupName: string, file: File) {
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch(
`${FACEPING_API_URL}/groups/${groupName}/search`,
{
method: 'POST',
body: formData
}
);
if (!response.ok) {
throw new Error('Failed to search faces');
}
return await response.json();
} catch (error) {
console.error('Error searching faces:', error);
throw error;
}
}
Create a component for face group management (components/FaceGroup.tsx
):
// components/FaceGroup.tsx
'use client';
import { useState } from 'react';
import { createGroup } from '../utils/faceping';
export default function FaceGroup() {
const [groupName, setGroupName] = useState('');
const [status, setStatus] = useState('');
const handleCreateGroup = async () => {
try {
await createGroup(groupName);
setStatus('Group created successfully');
setGroupName('');
} catch (error) {
setStatus('Error creating group');
}
};
return (
<div className="space-y-4">
<input
type="text"
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
placeholder="Enter group name"
className="w-full p-2 border rounded"
/>
<button
onClick={handleCreateGroup}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Create Group
</button>
{status && <p className="mt-2 text-sm">{status}</p>}
</div>
);
}
Create a component for face upload (components/FaceUpload.tsx
):
// components/FaceUpload.tsx
'use client';
import { useState, useRef } from 'react';
import { uploadFace } from '../utils/faceping';
export default function FaceUpload() {
const [groupName, setGroupName] = useState('');
const [status, setStatus] = useState('');
const fileRef = useRef<HTMLInputElement>(null);
const handleUpload = async () => {
const file = fileRef.current?.files?.[0];
if (!file) return;
try {
await uploadFace(groupName, file);
setStatus('Face uploaded successfully');
setGroupName('');
if (fileRef.current) fileRef.current.value = '';
} catch (error) {
setStatus('Error uploading face');
}
};
return (
<div className="space-y-4">
<input
type="text"
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
placeholder="Enter group name"
className="w-full p-2 border rounded"
/>
<input
type="file"
ref={fileRef}
accept="image/*"
className="w-full"
/>
<button
onClick={handleUpload}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Upload Face
</button>
{status && <p className="mt-2 text-sm">{status}</p>}
</div>
);
}
Create a component for face search (components/FaceSearch.tsx
):
// components/FaceSearch.tsx
'use client';
import { useState, useRef } from 'react';
import { searchFaces } from '../utils/faceping';
interface MatchResult {
score: number;
}
export default function FaceSearch() {
const [groupName, setGroupName] = useState('');
const [status, setStatus] = useState('');
const [results, setResults] = useState<MatchResult[]>([]);
const fileRef = useRef<HTMLInputElement>(null);
const handleSearch = async () => {
const file = fileRef.current?.files?.[0];
if (!file) return;
try {
const data = await searchFaces(groupName, file);
setResults(data.matches || []);
setStatus('Search completed');
} catch (error) {
setStatus('Error searching faces');
setResults([]);
}
};
return (
<div className="space-y-4">
<input
type="text"
value={groupName}
onChange={(e) => setGroupName(e.target.value)}
placeholder="Enter group name"
className="w-full p-2 border rounded"
/>
<input
type="file"
ref={fileRef}
accept="image/*"
className="w-full"
/>
<button
onClick={handleSearch}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Search Faces
</button>
{status && <p className="mt-2 text-sm">{status}</p>}
{results.length > 0 && (
<div className="mt-4 space-y-2">
{results.map((match, index) => (
<div key={index} className="p-2 bg-gray-50 rounded">
Match Score: {match.score}
</div>
))}
</div>
)}
</div>
);
}
Update your main page (app/page.tsx
):
// app/page.tsx
export default function Home() {
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">FacePing Demo</h1>
<div className="space-y-8">
<section>
<h2 className="text-xl font-semibold mb-4">Create Face Group</h2>
<FaceGroup />
</section>
<section>
<h2 className="text-xl font-semibold mb-4">Upload Face</h2>
<FaceUpload />
</section>
<section>
<h2 className="text-xl font-semibold mb-4">Search Faces</h2>
<FaceSearch />
</section>
</div>
</main>
);
}
Create a custom error handling utility (utils/error.ts
):
// utils/error.ts
export class APIError extends Error {
constructor(
message: string,
public status?: number,
public code?: string
) {
super(message);
this.name = 'APIError';
}
}
export function handleAPIError(error: unknown): APIError {
if (error instanceof APIError) {
return error;
}
if (error instanceof Error) {
return new APIError(error.message);
}
return new APIError('An unknown error occurred');
}
Create a loading component (components/LoadingSpinner.tsx
):
// components/LoadingSpinner.tsx
export default function LoadingSpinner() {
return (
<div className="flex justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
</div>
);
}
Update the FaceSearch component to include loading state:
// components/FaceSearch.tsx
'use client';
import { useState, useRef } from 'react';
import { searchFaces } from '../utils/faceping';
import LoadingSpinner from './LoadingSpinner';
export default function FaceSearch() {
const [groupName, setGroupName] = useState('');
const [status, setStatus] = useState('');
const [results, setResults] = useState<MatchResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fileRef = useRef<HTMLInputElement>(null);
const handleSearch = async () => {
const file = fileRef.current?.files?.[0];
if (!file) return;
setIsLoading(true);
try {
const data = await searchFaces(groupName, file);
setResults(data.matches || []);
setStatus('Search completed');
} catch (error) {
setStatus('Error searching faces');
setResults([]);
} finally {
setIsLoading(false);
}
};
return (
<div className="space-y-4">
{/* ... existing inputs ... */}
<button
onClick={handleSearch}
disabled={isLoading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{isLoading ? <LoadingSpinner /> : 'Search Faces'}
</button>
{/* ... existing results display ... */}
</div>
);
}
Make sure to set up your environment variables properly. Create a .env.local
file in your project root:
NEXT_PUBLIC_FACEPING_API_URL=https://api.faceping.ai
FACEPING_API_KEY=your_api_key_here
# Add this to your .gitignore
.env*.local