Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions __tests__/api/events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,66 @@ describe('GET /api/events', () => {
error: expect.stringContaining('Rate limit exceeded')
}));
});

describe('segmentId filtering', () => {
it('should forward a single segmentId to Ticketmaster', async () => {
nock.cleanAll();
nock('https://app.ticketmaster.com')
.get('/discovery/v2/events.json')
.query(params => params.segmentId === 'KZFzniwnSyZfZ7v7nJ' && params.city === 'Denver')
.reply(200, mockEvents);

const request = new NextRequest('http://localhost:3000/api/events?city=Denver&segmentId=KZFzniwnSyZfZ7v7nJ');
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data._embedded.events).toHaveLength(2);
});

it('should forward comma-separated segmentIds to Ticketmaster', async () => {
nock.cleanAll();
nock('https://app.ticketmaster.com')
.get('/discovery/v2/events.json')
.query(params => params.segmentId === 'KZFzniwnSyZfZ7v7nJ,KZFzniwnSyZfZ7v7na' && params.city === 'Denver')
.reply(200, mockEvents);

const request = new NextRequest('http://localhost:3000/api/events?city=Denver&segmentId=KZFzniwnSyZfZ7v7nJ,KZFzniwnSyZfZ7v7na');
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data._embedded.events).toHaveLength(2);
});

it('should not include segmentId when not provided', async () => {
nock.cleanAll();
nock('https://app.ticketmaster.com')
.get('/discovery/v2/events.json')
.query(params => !params.segmentId && params.city === 'Denver')
.reply(200, mockEvents);

const request = new NextRequest('http://localhost:3000/api/events?city=Denver');
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data._embedded.events).toHaveLength(2);
});

it('should forward segmentId with keyword search', async () => {
nock.cleanAll();
nock('https://app.ticketmaster.com')
.get('/discovery/v2/events.json')
.query(params => params.segmentId === 'KZFzniwnSyZfZ7v7nE' && params.keyword === 'Taylor Swift')
.reply(200, mockEvents);

const request = new NextRequest('http://localhost:3000/api/events?keyword=Taylor+Swift&segmentId=KZFzniwnSyZfZ7v7nE');
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data._embedded.events).toHaveLength(2);
});
});
});
5 changes: 5 additions & 0 deletions app/api/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export async function GET(request: NextRequest) {
sort: 'date,asc'
};

const segmentId = searchParams.get('segmentId');
if (segmentId) {
params.segmentId = segmentId;
}

if (city) {
params.city = city;
params.radius = RADIUS;
Expand Down
55 changes: 31 additions & 24 deletions app/components/AttractionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,40 +27,47 @@ interface AttractionListProps {
const AttractionList: FC<AttractionListProps> = ({ attractions, onSelect }) => {
if (!attractions.length) {
return (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
No attractions found
<div className="text-center py-16 text-surface-400 dark:text-surface-500">
<svg className="mx-auto h-12 w-12 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
<p className="text-sm font-medium">No attractions found</p>
<p className="text-xs mt-1">Try a different search term</p>
</div>
);
}

return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{attractions.map(attraction => (
<div
key={attraction.id}
className="bg-white dark:bg-gray-800 shadow rounded-lg p-4 cursor-pointer hover:shadow-lg transition-shadow"
className="bg-white dark:bg-surface-900 rounded-xl overflow-hidden shadow-sm cursor-pointer
hover:shadow-md hover:scale-[1.02] transition-all"
onClick={() => onSelect(attraction.id)}
>
<div className="flex items-start space-x-4">
{attraction.images?.[0] && (
<img
src={attraction.images[0].url}
alt={attraction.name}
className="w-24 h-24 object-cover rounded-lg"
/>
)}
<div className="flex-grow">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{attraction.name}
</h3>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{attraction.type.charAt(0).toUpperCase() + attraction.type.slice(1)}
</div>
{attraction.classifications?.[0]?.segment && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{attraction.classifications[0].segment.name}
</div>
)}
{attraction.images?.[0] ? (
<img
src={attraction.images[0].url}
alt={attraction.name}
className="w-full h-32 object-cover"
/>
) : (
<div className="w-full h-32 bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<svg className="h-10 w-10 text-surface-300 dark:text-surface-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
</div>
)}
<div className="p-3">
<h3 className="text-sm font-semibold text-surface-900 dark:text-white line-clamp-2">
{attraction.name}
</h3>
<div className="text-xs text-surface-500 dark:text-surface-400 mt-1">
{attraction.classifications?.[0]?.segment?.name ||
(attraction.type.charAt(0).toUpperCase() + attraction.type.slice(1))}
</div>
</div>
</div>
Expand Down
Loading
Loading