feat: / shows current/next trip.

This commit is contained in:
Felix Zett 2025-08-30 21:14:23 +02:00
parent 3186a9d549
commit 6e6c23b6ad
4 changed files with 108 additions and 54 deletions

View file

@ -1,4 +1,4 @@
from datetime import date
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from uuid import UUID from uuid import UUID
@ -129,3 +129,16 @@ def reconfigure_trip(trip_id: UUID, payload: TripUpdate, db: Session = Depends(g
"deleted_checked_trip_item_ids": deleted_checked, "deleted_checked_trip_item_ids": deleted_checked,
"created_trip_item_ids": created_ids, "created_trip_item_ids": created_ids,
} }
@router.get("/next-id", response_model=UUID)
def get_next_trip_id(db: Session = Depends(get_db)):
today = date.today()
trip = (
db.query(models.Trip)
.filter(models.Trip.start_date >= today)
.order_by(models.Trip.start_date.asc())
.first()
)
if not trip:
raise HTTPException(status_code=404, detail="No upcoming trip found")
return trip.id

View file

@ -1,11 +1,27 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useParams } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate } from "react-router-dom";
import { getSeed, getTrips, getTripItems, toggleTripItem } from "./api"; import { getSeed, getTrips, getNextTripId } from "./api";
import ItemsPage from "./pages/ItemsPage"; import ItemsPage from "./pages/ItemsPage";
import TripChecklist from "./pages/TripChecklist"; import TripChecklist from "./pages/TripChecklist";
function NextTripRedirect({ trips }: { trips: any[] }) {
const [nextTripId, setNextTripId] = React.useState<string | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
getNextTripId()
.then(setNextTripId)
.catch(() => setError("Kein anstehender Trip gefunden"));
}, []);
if (error) return <div>{error}</div>;
if (!nextTripId) return <div>Lade...</div>;
return <Navigate to={`/trips/${nextTripId}`} replace />;
}
export default function App() { export default function App() {
const [trips, setTrips] = useState<any[]>([]); const [trips, setTrips] = useState<any[]>([]);
const navigate = useNavigate();
async function loadTrips() { async function loadTrips() {
const data = await getTrips(); const data = await getTrips();
@ -16,59 +32,75 @@ export default function App() {
loadTrips(); loadTrips();
}, []); }, []);
async function goToCurrentTrip() {
try {
const id = await getNextTripId();
navigate(`/trips/${id}`);
} catch {
alert("Kein anstehender Trip gefunden");
}
}
return ( return (
<Router> <div className="p-4 max-w-2xl mx-auto">
<div className="p-4 max-w-2xl mx-auto"> <h1 className="text-2xl font-bold mb-4">Packlist</h1>
<h1 className="text-2xl font-bold mb-4">Packlist</h1>
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">
<Link to="/trips"> <button
<button className="bg-blue-500 text-white px-4 py-2 rounded"> className="bg-yellow-500 text-white px-4 py-2 rounded"
Alle Trips onClick={goToCurrentTrip}
</button> >
</Link> Zum aktuellen Trip
<Link to="/items"> </button>
<button className="bg-green-500 text-white px-4 py-2 rounded"> <Link to="/trips">
Alle Items <button className="bg-blue-500 text-white px-4 py-2 rounded">
</button> Alle Trips
</Link>
<button
className="bg-gray-300 text-gray-800 px-4 py-2 rounded"
onClick={async () => {
await getSeed();
await loadTrips();
}}
>
Seed-Daten erzeugen
</button> </button>
</div> </Link>
<Link to="/items">
<Routes> <button className="bg-green-500 text-white px-4 py-2 rounded">
<Route Alle Items
path="/trips" </button>
element={ </Link>
<ul> <button
{trips.map((trip) => ( className="bg-gray-300 text-gray-800 px-4 py-2 rounded"
<li key={trip.id} className="mb-2"> onClick={async () => {
<Link await getSeed();
to={`/trips/${trip.id}`} await loadTrips();
className="text-blue-600 underline" }}
> >
{trip.name} <span className="text-gray-500">({trip.start_date} {trip.end_date})</span> Seed-Daten erzeugen
</Link> </button>
</li>
))}
</ul>
}
/>
<Route
path="/trips/:id"
element={<TripChecklist trips={trips} />}
/>
<Route path="/items" element={<ItemsPage />} />
<Route path="/" element={<Navigate to="/trips" />} />
</Routes>
</div> </div>
</Router>
<Routes>
<Route
path="/trips"
element={
<ul>
{trips.map((trip) => (
<li key={trip.id} className="mb-2">
<Link
to={`/trips/${trip.id}`}
className="text-blue-600 underline"
>
{trip.name} <span className="text-gray-500">({trip.start_date} {trip.end_date})</span>
</Link>
</li>
))}
</ul>
}
/>
<Route
path="/trips/:id"
element={<TripChecklist trips={trips} />}
/>
<Route path="/items" element={<ItemsPage />} />
<Route path="/" element={<NextTripRedirect trips={trips} />} />
</Routes>
</div>
); );
} }
// Wichtig: Der <App />-Inhalt muss innerhalb von <Router> verwendet werden!
// In main.tsx ist das bereits der Fall.

View file

@ -82,3 +82,9 @@ export async function createItem(name: string, tags: string[]): Promise<Item> {
if (!res.ok) throw new Error("Failed to create item"); if (!res.ok) throw new Error("Failed to create item");
return res.json(); return res.json();
} }
export async function getNextTripId(): Promise<string> {
const res = await fetch(`${API_BASE}/trips/next-id`);
if (!res.ok) throw new Error("No upcoming trip found");
return res.json();
}

View file

@ -3,13 +3,16 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import './index.css'; import './index.css';
import { BrowserRouter } from "react-router-dom";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<App /> <BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
</React.StrictMode> </React.StrictMode>
); );