feat(fotograf-de-scraper): initial setup with backend and frontend scaffold [32788f42]

This commit is contained in:
2026-03-20 13:28:53 +00:00
parent b8eae846a5
commit 62ae7fe69e
7 changed files with 819 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
import { useState, useEffect } from 'react';
import './App.css';
interface Job {
id: string;
name: string;
url: string;
status: string;
date: string;
shooting_type: string;
}
function App() {
const [accountType, setAccountType] = useState('kiga'); // Default to kindergarten
const [jobs, setJobs] = useState<Job[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8001';
const fetchJobs = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${API_BASE_URL}/api/jobs?account_type=${accountType}`);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.detail || 'Fehler beim Abrufen der Aufträge');
}
const data: Job[] = await response.json();
setJobs(data);
} catch (err: any) {
setError(err.message);
console.error("Failed to fetch jobs:", err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchJobs();
}, [accountType]); // Refetch when accountType changes
return (
<div className="min-h-screen bg-gray-100 p-4">
<div className="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">Fotograf.de Auftragsübersicht</h1>
<div className="mb-4">
<label htmlFor="accountType" className="block text-sm font-medium text-gray-700">Account auswählen:</label>
<select
id="accountType"
value={accountType}
onChange={(e) => setAccountType(e.target.value)}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option value="kiga">Kindergarten</option>
<option value="schule">Schule</option>
</select>
</div>
<button
onClick={fetchJobs}
disabled={isLoading}
className="mb-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{isLoading ? 'Lade Aufträge...' : 'Aufträge neu laden'}
</button>
{error && <p className="text-red-600 mb-4">Fehler: {error}</p>}
{jobs.length === 0 && !isLoading && !error && (
<p className="text-gray-500">Keine Aufträge gefunden.</p>
)}
{jobs.length > 0 && (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Datum</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Typ</th>
<th scope="col" className="relative px-6 py-3"><span className="sr-only">Aktionen</span></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{jobs.map((job) => (
<tr key={job.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{job.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.status}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.date}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{job.shooting_type}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href={job.url} target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:text-indigo-900">Details</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
export default App;