Skip to content

Commit 4aec862

Browse files
authored
Update UI with more responsive designs (#40)
* [Feature] Add responsive design with card layout for mobile and table for desktop views * [Feature] Enhance brewery details page UI * [Test] Add responsive design tests and update selectors for mobile/desktop views * [Test] Format test files and update pagination component alignment * [Test] Add data-testid attributes for e2e testing * [Feature] Add brewery type filtering and make type labels clickable links * [Feature] Add context-aware brewery type links and pagination for filtered navigation * [Test] Add brewery type filtering tests for state and city levels * [Chore] Format
1 parent 66f769e commit 4aec862

File tree

16 files changed

+794
-220
lines changed

16 files changed

+794
-220
lines changed

src/lib/components/BreweriesTable.svelte

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script lang="ts">
22
import { ExternalLinkIcon, MapPinIcon } from 'lucide-svelte';
3-
import type { Brewery } from '$lib/types';
43
5-
interface Props {
6-
breweries?: Brewery[];
7-
}
8-
9-
let { breweries = [] }: Props = $props();
4+
let {
5+
breweries = [],
6+
context = 'country',
7+
country = '',
8+
state = '',
9+
city = '',
10+
} = $props();
1011
</script>
1112

1213
<table class="min-w-full divide-y divide-gray-300">
@@ -19,7 +20,7 @@
1920
>
2021
<th
2122
scope="col"
22-
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
23+
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900 hidden lg:table-cell"
2324
>Street</th
2425
>
2526
<th
@@ -34,17 +35,17 @@
3435
>
3536
<th
3637
scope="col"
37-
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
38+
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900 hidden lg:table-cell"
3839
>Postal Code</th
3940
>
4041
<th
4142
scope="col"
42-
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
43+
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900 hidden md:table-cell"
4344
>Country</th
4445
>
4546
<th
4647
scope="col"
47-
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
48+
class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900 hidden md:table-cell"
4849
>Type</th
4950
>
5051
<th
@@ -60,11 +61,13 @@
6061
<tr>
6162
<td
6263
class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900 sm:pl-6 max-w-xs truncate"
63-
><a class="text-amber-600 hover:text-amber-900 transition-colors duration-200" href="/b/{brewery.id}"
64-
>{brewery.name}</a
64+
><a
65+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
66+
href="/b/{brewery.id}">{brewery.name}</a
6567
></td
6668
>
67-
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500"
69+
<td
70+
class="whitespace-nowrap px-2 py-2 text-sm text-gray-500 hidden lg:table-cell"
6871
>{brewery.address_1 ?? ''}</td
6972
>
7073
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500"
@@ -81,17 +84,36 @@
8184
>{brewery.state_province}</a
8285
></td
8386
>
84-
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500"
87+
<td
88+
class="whitespace-nowrap px-2 py-2 text-sm text-gray-500 hidden lg:table-cell"
8589
>{brewery.postal_code}</td
8690
>
87-
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500"
91+
<td
92+
class="whitespace-nowrap px-2 py-2 text-sm text-gray-500 hidden md:table-cell"
8893
><a
8994
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
9095
href="/breweries/{brewery.country}">{brewery.country}</a
9196
></td
9297
>
93-
<td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500"
94-
>{brewery.brewery_type}</td
98+
<td class="whitespace-nowrap px-2 py-2 text-sm hidden md:table-cell"
99+
><a
100+
href={(() => {
101+
// Determine the correct URL based on context
102+
switch (context) {
103+
case 'city':
104+
return `/breweries/${brewery.country}/${brewery.state_province}/${brewery.city}?by_type=${brewery.brewery_type}`;
105+
case 'state':
106+
return `/breweries/${brewery.country}/${brewery.state_province}?by_type=${brewery.brewery_type}`;
107+
case 'country':
108+
default:
109+
return `/breweries/${brewery.country}?by_type=${brewery.brewery_type}`;
110+
}
111+
})()}
112+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200 capitalize"
113+
data-testid="brewery-type-link"
114+
>
115+
{brewery.brewery_type}
116+
</a></td
95117
>
96118
<td
97119
class="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"

src/lib/components/BreweryCard.svelte

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script lang="ts">
2+
import { ExternalLinkIcon, MapPinIcon } from 'lucide-svelte';
3+
4+
let { brewery } = $props();
5+
</script>
6+
7+
<div
8+
class="bg-white shadow rounded-lg overflow-hidden border border-gray-200 hover:shadow-md transition-shadow duration-200"
9+
>
10+
<div class="p-4">
11+
<div class="flex justify-between items-start">
12+
<h3 class="text-lg font-medium text-gray-900 truncate">
13+
<a
14+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
15+
href="/b/{brewery.id}"
16+
>
17+
{brewery.name}
18+
</a>
19+
</h3>
20+
<div class="flex space-x-2">
21+
{#if brewery.latitude && brewery.longitude}
22+
<a
23+
href="https://www.google.com/maps/search/?api=1&query={brewery.latitude},{brewery.longitude}"
24+
target="_blank"
25+
rel="noreferrer"
26+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
27+
aria-label="View on map"
28+
>
29+
<MapPinIcon size={20} />
30+
</a>
31+
{/if}
32+
{#if brewery.website_url}
33+
<a
34+
href={brewery.website_url}
35+
target="_blank"
36+
rel="noreferrer"
37+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
38+
aria-label="Visit website"
39+
>
40+
<ExternalLinkIcon size={20} />
41+
</a>
42+
{/if}
43+
</div>
44+
</div>
45+
46+
<div class="mt-2 text-sm text-gray-600">
47+
<p class="truncate">{brewery.address_1 ?? ''}</p>
48+
<div class="flex flex-wrap gap-x-1">
49+
<a
50+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
51+
href="/breweries/{brewery.country}/{brewery.state_province}/{brewery.city}"
52+
>
53+
{brewery.city}
54+
</a>
55+
<span>,</span>
56+
<a
57+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
58+
href="/breweries/{brewery.country}/{brewery.state_province}"
59+
>
60+
{brewery.state_province}
61+
</a>
62+
<span>{brewery.postal_code}</span>
63+
</div>
64+
<div class="mt-1 flex justify-between">
65+
<a
66+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200"
67+
href="/breweries/{brewery.country}"
68+
>
69+
{brewery.country}
70+
</a>
71+
<a
72+
href={`/breweries/${brewery.country}?by_type=${brewery.brewery_type}`}
73+
class="text-amber-600 hover:text-amber-900 transition-colors duration-200 capitalize"
74+
data-testid="brewery-type-link"
75+
>
76+
{brewery.brewery_type}
77+
</a>
78+
</div>
79+
</div>
80+
</div>
81+
</div>

src/lib/components/Pagination.svelte

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,54 @@
11
<script lang="ts">
2-
import type { Metadata } from '$lib/types';
3-
4-
interface Props {
5-
meta: Metadata;
6-
country: string;
7-
state?: string | undefined;
8-
city?: string | undefined;
9-
}
10-
112
let {
123
meta,
134
country,
145
state = undefined,
15-
city = undefined
16-
}: Props = $props();
6+
city = undefined,
7+
context = 'country',
8+
breweryType = undefined,
9+
} = $props();
1710
1811
let page = $derived(+meta.page);
12+
13+
const getPageUrl = (targetPage: number): string => {
14+
let baseUrl = `/breweries/${country}`;
15+
16+
if (context === 'state' || context === 'city') {
17+
baseUrl += `/${state}`;
18+
}
19+
20+
if (context === 'city') {
21+
baseUrl += `/${city}`;
22+
}
23+
24+
baseUrl += `/${targetPage}`;
25+
26+
if (breweryType) {
27+
baseUrl += `?by_type=${breweryType}`;
28+
}
29+
30+
return baseUrl;
31+
};
1932
</script>
2033

21-
<ul class="mt-4 text-sm flex gap-4">
34+
<div
35+
class="mt-4 flex flex-wrap items-center justify-center sm:justify-end gap-3"
36+
>
2237
{#if page > 1}
23-
<li>
24-
<a
25-
class="px-4 py-2 border border-amber-300 rounded-md text-amber-700 hover:bg-amber-50 hover:text-amber-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 transition-colors duration-200 shadow-sm hover:shadow transition-shadow duration-200"
26-
href="/breweries/{country}{state ? `/${state}` : ''}{city
27-
? `/${city}`
28-
: ''}/{page - 1}">Previous</a
29-
>
30-
</li>
38+
<a
39+
class="px-5 py-3 border border-amber-300 rounded-md text-amber-700 hover:bg-amber-50 hover:text-amber-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 transition-colors duration-200 shadow-sm hover:shadow transition-shadow duration-200 text-sm font-medium min-w-[100px] text-center"
40+
href={getPageUrl(page - 1)}>Previous</a
41+
>
3142
{/if}
43+
44+
<span class="text-sm text-gray-600 px-2">
45+
Page {page} of {Math.ceil(+meta.total / +meta.per_page)}
46+
</span>
47+
3248
{#if page < +meta.total / +meta.per_page}
33-
<li>
34-
<a
35-
class="px-4 py-2 border border-amber-300 rounded-md text-amber-700 hover:bg-amber-50 hover:text-amber-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 transition-colors duration-200 shadow-sm hover:shadow transition-shadow duration-200"
36-
href="/breweries/{country}{state ? `/${state}` : ''}{city
37-
? `/${city}`
38-
: ''}/{page + 1}">Next</a
39-
>
40-
</li>
49+
<a
50+
class="px-5 py-3 border border-amber-300 rounded-md text-amber-700 hover:bg-amber-50 hover:text-amber-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500 transition-colors duration-200 shadow-sm hover:shadow transition-shadow duration-200 text-sm font-medium min-w-[100px] text-center"
51+
href={getPageUrl(page + 1)}>Next</a
52+
>
4153
{/if}
42-
</ul>
54+
</div>

0 commit comments

Comments
 (0)