mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
Handle fetch fail in client app, fix data binding in location data (#243)
* Improve error handling for server rendered routes * Fix data binding issue for country/state codes (use alias in sql)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { data } from 'react-router';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -24,32 +26,49 @@ export interface Brewery {
|
||||
location: BreweryLocation | null;
|
||||
}
|
||||
|
||||
const fetchApi = async (path: string): Promise<Response> => {
|
||||
try {
|
||||
return await fetch(`${API_BASE_URL}${path}`);
|
||||
} catch {
|
||||
throw data('The brewery service is unreachable right now. Please try again in a moment.', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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}`);
|
||||
const res = await fetchApi(`/api/brewery?${params}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load breweries (${res.status})`);
|
||||
throw data(`Failed to load breweries (${res.status}).`, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
});
|
||||
}
|
||||
|
||||
const data: ApiResponse<Brewery[]> = await res.json();
|
||||
return data.payload;
|
||||
const body: ApiResponse<Brewery[]> = await res.json();
|
||||
return body.payload;
|
||||
};
|
||||
|
||||
export const getBreweryById = async (id: string): Promise<Brewery | null> => {
|
||||
const res = await fetch(`${API_BASE_URL}/api/brewery/${encodeURIComponent(id)}`);
|
||||
const res = await fetchApi(`/api/brewery/${encodeURIComponent(id)}`);
|
||||
|
||||
if (res.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load brewery (${res.status})`);
|
||||
throw data(`Failed to load brewery (${res.status}).`, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
});
|
||||
}
|
||||
|
||||
const data: ApiResponse<Brewery> = await res.json();
|
||||
return data.payload;
|
||||
const body: ApiResponse<Brewery> = await res.json();
|
||||
return body.payload;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import RouteErrorState from '../../../components/ui/error/RouteErrorState';
|
||||
import BreweryCard from '../components/BreweryCard';
|
||||
import { getBreweries } from '../breweries.server';
|
||||
import type { Route } from './+types/breweries';
|
||||
@@ -37,3 +38,7 @@ const Breweries = ({ loaderData }: Route.ComponentProps) => {
|
||||
};
|
||||
|
||||
export default Breweries;
|
||||
|
||||
export const ErrorBoundary = ({ error }: Route.ErrorBoundaryProps) => (
|
||||
<RouteErrorState error={error} />
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { data, Link } from 'react-router';
|
||||
import RouteErrorState from '../../../components/ui/error/RouteErrorState';
|
||||
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 });
|
||||
throw data('Brewery not found.', { status: 404, statusText: 'Not Found' });
|
||||
}
|
||||
|
||||
return { brewery };
|
||||
@@ -57,3 +58,7 @@ const BreweryDetail = ({ loaderData }: Route.ComponentProps) => {
|
||||
};
|
||||
|
||||
export default BreweryDetail;
|
||||
|
||||
export const ErrorBoundary = ({ error }: Route.ErrorBoundaryProps) => (
|
||||
<RouteErrorState error={error} />
|
||||
);
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
biergartenThemes,
|
||||
defaultThemeName,
|
||||
isBiergartenTheme,
|
||||
type ThemeName,
|
||||
themeStorageKey,
|
||||
} from '../themes';
|
||||
import { useRouteLoaderData } from 'react-router';
|
||||
import type { loader as rootLoader } from '../../../root';
|
||||
import { biergartenThemes, defaultThemeName, themeCookieName, type ThemeName } from '../themes';
|
||||
|
||||
const applyTheme = (theme: ThemeName) => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(themeStorageKey, theme);
|
||||
document.cookie = `${themeCookieName}=${theme}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
||||
};
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const [selectedTheme, setSelectedTheme] = useState<ThemeName>(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return defaultThemeName;
|
||||
}
|
||||
|
||||
const savedTheme = localStorage.getItem(themeStorageKey);
|
||||
return isBiergartenTheme(savedTheme) ? savedTheme : defaultThemeName;
|
||||
});
|
||||
const rootTheme = useRouteLoaderData<typeof rootLoader>('root')?.theme ?? defaultThemeName;
|
||||
const [selectedTheme, setSelectedTheme] = useState<ThemeName>(rootTheme);
|
||||
|
||||
const activeTheme =
|
||||
biergartenThemes.find((theme) => theme.value === selectedTheme) ?? biergartenThemes[0];
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface ThemeOption {
|
||||
}
|
||||
|
||||
export const defaultThemeName: ThemeName = 'biergarten-lager';
|
||||
export const themeStorageKey = 'biergarten-theme';
|
||||
export const themeCookieName = 'biergarten-theme';
|
||||
|
||||
export const biergartenThemes: ThemeOption[] = [
|
||||
{
|
||||
@@ -36,3 +36,9 @@ export const biergartenThemes: ThemeOption[] = [
|
||||
export const isBiergartenTheme = (value: string | null | undefined): value is ThemeName => {
|
||||
return biergartenThemes.some((theme) => theme.value === value);
|
||||
};
|
||||
|
||||
export const parseThemeCookie = (cookieHeader: string | null): ThemeName => {
|
||||
const match = cookieHeader?.match(new RegExp(`(?:^|; )${themeCookieName}=([^;]*)`));
|
||||
const value = match ? decodeURIComponent(match[1]) : null;
|
||||
return isBiergartenTheme(value) ? value : defaultThemeName;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user