Refactor authentication and account management with modular UI components (#242)

This commit is contained in:
2026-08-23 01:30:58 -04:00
committed by GitHub
parent c514eb9814
commit 1621313daa
109 changed files with 14743 additions and 10792 deletions

View File

@@ -0,0 +1,55 @@
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
interface ApiResponse<T> {
message: string;
payload: T;
}
export interface BreweryLocation {
breweryPostLocationId: string;
breweryPostId: string;
cityId: string;
addressLine1: string;
addressLine2: string | null;
postalCode: string;
}
export interface Brewery {
breweryPostId: string;
postedById: string;
breweryName: string;
description: string;
createdAt: string;
updatedAt: string | null;
location: BreweryLocation | null;
}
export const getBreweries = async (limit?: number, offset?: number): Promise<Brewery[]> => {
const params = new URLSearchParams();
if (limit !== undefined) params.set('limit', String(limit));
if (offset !== undefined) params.set('offset', String(offset));
const res = await fetch(`${API_BASE_URL}/api/brewery?${params}`);
if (!res.ok) {
throw new Error(`Failed to load breweries (${res.status})`);
}
const data: ApiResponse<Brewery[]> = await res.json();
return data.payload;
};
export const getBreweryById = async (id: string): Promise<Brewery | null> => {
const res = await fetch(`${API_BASE_URL}/api/brewery/${encodeURIComponent(id)}`);
if (res.status === 404) {
return null;
}
if (!res.ok) {
throw new Error(`Failed to load brewery (${res.status})`);
}
const data: ApiResponse<Brewery> = await res.json();
return data.payload;
};

View File

@@ -0,0 +1,26 @@
import { Link } from 'react-router';
import type { Brewery } from '../breweries.server';
interface BreweryCardProps {
brewery: Brewery;
}
const BreweryCard = ({ brewery }: BreweryCardProps) => (
<Link
to={`/breweries/${brewery.breweryPostId}`}
className="card bg-base-100 shadow hover:shadow-lg transition-shadow"
>
<div className="card-body">
<h2 className="card-title">{brewery.breweryName}</h2>
<p className="text-base-content/70 line-clamp-3">{brewery.description}</p>
{brewery.location && (
<p className="text-sm text-base-content/50 mt-2">
{brewery.location.addressLine1}
{brewery.location.addressLine2 ? `, ${brewery.location.addressLine2}` : ''}
</p>
)}
</div>
</Link>
);
export default BreweryCard;

View File

@@ -0,0 +1,39 @@
import BreweryCard from '../components/BreweryCard';
import { getBreweries } from '../breweries.server';
import type { Route } from './+types/breweries';
export const meta = ({}: Route.MetaArgs) => {
return [{ title: 'Breweries | The Biergarten App' }];
};
export const loader = async () => {
const breweries = await getBreweries();
return { breweries };
};
const Breweries = ({ loaderData }: Route.ComponentProps) => {
const { breweries } = loaderData;
return (
<div className="min-h-screen bg-base-200">
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-4">Breweries</h1>
<p className="text-base-content/70 mb-6">Discover our partner breweries.</p>
{breweries.length === 0 ? (
<div className="alert alert-info alert-soft">
<span>No breweries have been posted yet.</span>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{breweries.map((brewery) => (
<BreweryCard key={brewery.breweryPostId} brewery={brewery} />
))}
</div>
)}
</div>
</div>
);
};
export default Breweries;

View File

@@ -0,0 +1,59 @@
import { data, Link } from 'react-router';
import { getBreweryById } from '../breweries.server';
import type { Route } from './+types/brewery-detail';
export const loader = async ({ params }: Route.LoaderArgs) => {
const brewery = await getBreweryById(params.id);
if (!brewery) {
throw data('Brewery not found.', { status: 404 });
}
return { brewery };
};
const BreweryDetail = ({ loaderData }: Route.ComponentProps) => {
const { brewery } = loaderData;
return (
<div className="min-h-screen bg-base-200">
<div className="container mx-auto p-4 max-w-2xl">
<Link
to="/breweries"
className="link link-hover text-sm text-base-content/60 mb-4 inline-block"
>
&larr; Back to Breweries
</Link>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h1 className="card-title text-3xl">{brewery.breweryName}</h1>
<p className="text-base-content/70 whitespace-pre-line">
{brewery.description}
</p>
{brewery.location && (
<div className="bg-base-200 rounded-box p-4 mt-4">
<p className="text-xs font-semibold uppercase tracking-widest text-base-content/50 mb-2">
Location
</p>
<p>{brewery.location.addressLine1}</p>
{brewery.location.addressLine2 && (
<p>{brewery.location.addressLine2}</p>
)}
<p>{brewery.location.postalCode}</p>
</div>
)}
<p className="text-xs text-base-content/40 mt-4">
Posted {new Date(brewery.createdAt).toLocaleDateString()}
{brewery.updatedAt &&
` · Updated ${new Date(brewery.updatedAt).toLocaleDateString()}`}
</p>
</div>
</div>
</div>
</div>
);
};
export default BreweryDetail;