Skip to content
Open
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
16 changes: 8 additions & 8 deletions content/blog/compare-headphones-singapore-2026.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
---
slug: "compare-headphones-singapore-2026"
title: "Sony WH-1000XM5 Price Singapore (2026) — S$349 Cheapest"
description: "Cheapest Sony WH-1000XM5 in Singapore is S$349 on Shopee SG; Lazada S$359, Amazon S$369. Compare 2026 prices."
title: "Buy Sony WH-1000XM5 Singapore — Cheapest Price S$349 (2026)"
description: "Buy Sony WH-1000XM5 in Singapore from S$349 on Shopee SG. Compare 2026 prices across Lazada, Amazon, Challenger, Courts and Harvey Norman — live headphone deals."
author: "BuyWhere Team"
publishedAt: "2026-06-19"
lastUpdatedAt: "2026-07-10"
lastUpdatedAt: "2026-07-29"
tags: ["headphones", "singapore", "pricing", "comparison", "audio"]
jsonLd: >
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Article",
"headline": "Sony WH-1000XM5 Price Singapore (2026) — From S$349",
"description": "Cheapest Sony WH-1000XM5 in Singapore is S$349 on Shopee SG; Lazada S$359, Amazon S$369. Compare 2026 prices.",
"headline": "Buy Sony WH-1000XM5 Singapore — Cheapest Price S$349 (2026)",
"description": "Buy Sony WH-1000XM5 in Singapore from S$349 on Shopee SG. Compare 2026 prices across Lazada, Amazon, Challenger, Courts and Harvey Norman — live headphone deals.",
"datePublished": "2026-06-19",
"dateModified": "2026-07-10",
"dateModified": "2026-07-29",
"author": { "@type": "Organization", "name": "BuyWhere Team", "url": "https://buywhere.ai" },
"publisher": {
"@type": "Organization",
Expand Down Expand Up @@ -78,7 +78,7 @@ jsonLd: >
}
---

# Sony WH-1000XM5 Price Singapore (2026) — From S$349
# Buy Sony WH-1000XM5 Singapore — Cheapest Price S$349 (2026)

The top wireless headphones in Singapore — Sony WH-1000XM5, Apple AirPods Max, and Bose QuietComfort Ultra — are available from 15+ merchants with significant price variation. BuyWhere tracks real-time pricing across all Singapore merchants, showing price differences of SGD 50–150 between the cheapest and most expensive retailers for the same product.

Expand Down Expand Up @@ -176,4 +176,4 @@ Developers can use BuyWhere's API to build price monitoring agents. The `get_dea

*Data powered by BuyWhere — the definitive product catalog for AI agents. Compare prices across 50+ Singapore merchants at [buywhere.ai](https://buywhere.ai).*

*Prices last verified: 2026-06-13. This page is refreshed monthly; for live multi-merchant data, query the [BuyWhere compare hub](https://buywhere.ai/compare) or read the [cross-merchant price comparison guide](/blog/compare-product-prices-singapore-2026). See also the [Cheapest iPhone in Singapore](/blog/cheapest-iphone-singapore-2026) and the [Best Laptop Deals in Singapore](/blog/best-laptop-deals-singapore) for adjacent category refreshes.*
*Prices last verified: 2026-07-29. This page is refreshed monthly; for live multi-merchant data, query the [BuyWhere compare hub](https://buywhere.ai/compare) or read the [cross-merchant price comparison guide](/blog/compare-product-prices-singapore-2026). See also the [Cheapest iPhone in Singapore](/blog/cheapest-iphone-singapore-2026) and the [Best Laptop Deals in Singapore](/blog/best-laptop-deals-singapore) for adjacent category refreshes.*
86 changes: 86 additions & 0 deletions scripts/mobile-fold-check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { chromium } from '@playwright/test';

const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:52000';
const SEARCH_PATH = '/search?q=wireless%20headphones&country=us';
const MAX_FIRST_CARD_TOP = 600;

const products = Array.from({ length: 8 }, (_, index) => ({
id: index + 1,
name: `Wireless headphones ${index + 1}`,
price_amount: 99 + index,
price_currency: 'USD',
merchant_name: 'Example Shop',
image_url: `https://example.com/headphones-${index + 1}.jpg`,
click_url: `https://example.com/products/${index + 1}`,
brand: 'Example',
category: 'Headphones',
}));

async function preparePage(browser, viewport) {
const context = await browser.newContext({ viewport });
const page = await context.newPage();

await page.route('**/api/products/search?**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: products, total: products.length, has_more: false }),
});
});

await page.goto(`${BASE_URL}${SEARCH_PATH}`, { waitUntil: 'networkidle' });
await page.getByTestId('search-product-card').first().waitFor();
return { context, page };
}

function assert(condition, message) {
if (!condition) throw new Error(message);
}

const browser = await chromium.launch({ headless: true });

try {
const mobile = await preparePage(browser, { width: 390, height: 844 });
const mobileSummary = mobile.page.getByTestId('search-mobile-summary');
const refine = mobile.page.getByTestId('search-refine');
const editor = mobile.page.getByTestId('search-editor');
const desktopHero = mobile.page.getByTestId('search-desktop-hero');
const desktopToolbar = mobile.page.getByTestId('search-desktop-toolbar');
const firstCard = mobile.page.getByTestId('search-product-card').first();
const firstCardRect = await firstCard.boundingBox();

assert(await mobileSummary.isVisible(), 'Mobile search summary must be visible');
assert(!(await editor.isVisible()), 'Mobile search editor must be collapsed by default');
assert(!(await desktopHero.isVisible()), 'Desktop hero must be hidden on mobile');
assert(!(await desktopToolbar.isVisible()), 'Desktop results toolbar must be hidden on mobile');
assert(firstCardRect, 'First product card must render');
assert(
firstCardRect.y <= MAX_FIRST_CARD_TOP,
`First product card starts at ${firstCardRect.y}px; expected <= ${MAX_FIRST_CARD_TOP}px`,
);

await refine.locator('summary').click();
assert(await editor.isVisible(), 'Refine search must reveal the editor');
assert(await mobile.page.getByLabel('Search products').isVisible(), 'Search input must remain accessible');
assert(await mobile.page.getByLabel('Country selector').isVisible(), 'Country selector must remain accessible');

const mobileSnapshot = {
viewport: { width: 390, height: 844 },
firstCardTop: firstCardRect.y,
visibleCardPixels: 844 - firstCardRect.y,
summaryText: await mobileSummary.innerText(),
refineOpen: await refine.evaluate((element) => element.open),
};
await mobile.context.close();

const desktop = await preparePage(browser, { width: 1280, height: 900 });
assert(await desktop.page.getByTestId('search-desktop-hero').isVisible(), 'Desktop hero must remain visible');
assert(await desktop.page.getByTestId('search-editor').isVisible(), 'Desktop search editor must remain visible');
assert(await desktop.page.getByTestId('search-desktop-toolbar').isVisible(), 'Desktop results toolbar must remain visible');
assert(!(await desktop.page.getByTestId('search-mobile-summary').isVisible()), 'Mobile summary must be hidden on desktop');
await desktop.context.close();

console.log(JSON.stringify({ passed: true, mobile: mobileSnapshot, desktop: { width: 1280, height: 900 } }, null, 2));
} finally {
await browser.close();
}
88 changes: 66 additions & 22 deletions src/app/search/SearchResultsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ function SearchProgressIndicator({ startedAt }: { startedAt: number }) {
function SearchCard({ product }: { product: SearchCardProduct }) {
return (
<a
data-testid="search-product-card"
href={product.href}
target="_blank"
rel="noopener noreferrer"
Expand Down Expand Up @@ -404,6 +405,21 @@ export default function SearchResultsClient({
const lastRequestKeyRef = useRef<string | null>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const searchFieldRef = useRef<HTMLLabelElement>(null);
const searchRefineRef = useRef<HTMLDetailsElement>(null);

useEffect(() => {
const mediaQuery = window.matchMedia('(min-width: 640px)');
const syncRefineDisclosure = (matches: boolean) => {
if (searchRefineRef.current) {
searchRefineRef.current.open = matches;
}
};
const handleViewportChange = (event: MediaQueryListEvent) => syncRefineDisclosure(event.matches);

syncRefineDisclosure(mediaQuery.matches);
mediaQuery.addEventListener('change', handleViewportChange);
return () => mediaQuery.removeEventListener('change', handleViewportChange);
}, []);

const persistSearchHistory = useCallback((searchTerm: string) => {
setSearchHistory((currentHistory) => {
Expand Down Expand Up @@ -659,8 +675,8 @@ export default function SearchResultsClient({

<main id="main-content" className="flex-1">
<section className="border-b border-amber-100 bg-[radial-gradient(circle_at_top,_rgba(245,158,11,0.22),_rgba(255,247,237,0.85)_38%,_rgba(255,255,255,1)_80%)]">
<div className={`mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 ${hasActiveSearch ? 'py-5 lg:py-6' : 'py-10 lg:py-14'}`}>
<div className="max-w-3xl">
<div className={`mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 ${hasActiveSearch ? 'py-3 sm:py-5 lg:py-6' : 'py-10 lg:py-14'}`}>
<div className={`max-w-3xl ${hasActiveSearch ? 'hidden sm:block' : ''}`} data-testid="search-desktop-hero">
<p className="text-sm font-semibold uppercase tracking-[0.22em] text-amber-700">Product search</p>
<h1 className={`${hasActiveSearch ? 'mt-2 text-3xl sm:text-4xl' : 'mt-3 text-4xl sm:text-5xl'} font-semibold tracking-tight text-slate-950`}>
{query.trim() ? `Search results for "${query.trim()}"` : "Find live catalog results without leaving BuyWhere"}
Expand All @@ -670,10 +686,30 @@ export default function SearchResultsClient({
</p>
</div>

<div className={`${hasActiveSearch ? 'mt-5 rounded-[28px] p-3 md:p-4' : 'mt-8 rounded-[32px] p-4 md:p-6'} border border-white/80 bg-white/80 shadow-[0_24px_80px_-48px_rgba(15,23,42,0.55)] backdrop-blur`}>
{isNavigating && showSearchPrompt ? <SearchInputSkeleton /> : null}
{hasActiveSearch ? (
<div className="sm:hidden" data-testid="search-mobile-summary">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-amber-700">{activeCountry.label}</p>
<h1 className="mt-1 truncate text-xl font-semibold tracking-tight text-slate-950">
{loadingInitial ? `Searching for “${debouncedQuery}”` : `${total.toLocaleString()} results for “${debouncedQuery}”`}
</h1>
</div>
) : null}

<details
ref={searchRefineRef}
className={`${hasActiveSearch ? 'group mt-3 sm:mt-5' : ''} search-refine`}
data-testid="search-refine"
>
{hasActiveSearch ? (
<summary className="flex min-h-11 cursor-pointer list-none items-center justify-between rounded-2xl border border-amber-200 bg-white/90 px-4 text-sm font-semibold text-slate-800 shadow-sm marker:content-none sm:hidden [&::-webkit-details-marker]:hidden">
Refine search
<span className="text-amber-700 transition-transform group-open:rotate-180" aria-hidden="true">⌄</span>
</summary>
) : null}
<div className={`${hasActiveSearch ? 'mt-3 rounded-[24px] p-3 sm:mt-5 sm:rounded-[28px] md:p-4' : 'mt-8 rounded-[32px] p-4 md:p-6'} border border-white/80 bg-white/80 shadow-[0_24px_80px_-48px_rgba(15,23,42,0.55)] backdrop-blur`} data-testid="search-editor">
{isNavigating && showSearchPrompt ? <SearchInputSkeleton /> : null}

<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]">
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_220px]">
<label ref={searchFieldRef} className="relative block">
<span className="mb-2 block text-sm font-medium text-slate-700">Search query</span>
<Search className="pointer-events-none absolute left-4 top-[3.2rem] h-5 w-5 text-slate-600" aria-hidden="true" />
Expand Down Expand Up @@ -816,24 +852,32 @@ export default function SearchResultsClient({
</label>
</div>

<div className="mt-4 flex flex-wrap items-center gap-2 text-sm text-slate-500">
<span>Suggested:</span>
{SUGGESTED_SEARCHES.map((suggestion) => (
<button
key={suggestion}
type="button"
onClick={() => runSearch(suggestion)}
className="rounded-full border border-amber-200 bg-amber-50 px-3 py-1.5 font-medium text-amber-800 transition hover:border-amber-300 hover:bg-amber-100"
>
{suggestion}
</button>
))}
<div className={`${hasActiveSearch ? 'hidden sm:flex' : 'flex'} mt-4 flex-wrap items-center gap-2 text-sm text-slate-500`}>
<span>Suggested:</span>
{SUGGESTED_SEARCHES.map((suggestion) => (
<button
key={suggestion}
type="button"
onClick={() => runSearch(suggestion)}
className="rounded-full border border-amber-200 bg-amber-50 px-3 py-1.5 font-medium text-amber-800 transition hover:border-amber-300 hover:bg-amber-100"
>
{suggestion}
</button>
))}
</div>
</div>
</div>
</details>
<style jsx>{`
@media (max-width: 639px) {
.search-refine:not([open]) > div {
display: none;
}
}
`}</style>
</div>
</section>

<section className="mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8 lg:py-10">
<section className={`mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 ${hasActiveSearch ? 'py-4 sm:py-8 lg:py-10' : 'py-8 lg:py-10'}`}>
{showSearchPrompt ? (
<div className="rounded-[28px] border border-dashed border-slate-300 bg-white/90 p-8 text-center shadow-sm">
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-600">Start browsing</p>
Expand All @@ -851,8 +895,8 @@ export default function SearchResultsClient({
) : null}

{!showSearchPrompt && !error ? (
<div className="space-y-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-4 sm:space-y-6">
<div className="hidden sm:flex sm:items-end sm:justify-between" data-testid="search-desktop-toolbar">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-amber-700">
{activeCountry.label}
Expand Down Expand Up @@ -945,7 +989,7 @@ export default function SearchResultsClient({

{!loadingInitial && products.length > 0 ? (
<>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4" data-testid="search-product-grid">
{products.map((product) => (
<SearchCard key={product.id} product={product} />
))}
Expand Down