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
3 changes: 1 addition & 2 deletions apps/extension/src/test-extension-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2816,8 +2816,7 @@ async function runTests() {
? radarMod
: (radarMod as unknown as { default: typeof radarMod }).default
) as typeof radarMod;
const plausibilityMod =
await import('../../web/src/lib/offer-plausibility.js');
const plausibilityMod = await import('@repo/shared-data/rules');
const { implausiblyLowOfferIds, implausiblyLowIndexes } = (
typeof plausibilityMod.implausiblyLowOfferIds === 'function'
? plausibilityMod
Expand Down
8 changes: 8 additions & 0 deletions apps/web/e2e/reflow-contrast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const PHONE_PAGES = [
'/smartphones/iphone-16-pro-max-256gb',
'/merchants',
'/compare',
// Longer sort labels pushed the catalog sideways in en and cs
// (DOS/Rate#1082).
'/en/smartphones',
'/cs/smartphones',
// Czech copy is the longest; the merchant rating line did not wrap
// (DOS/Rate#1084).
'/cs/leaderboard',
'/cs/merchants/excluded-review-store',
];

for (const path of PHONE_PAGES) {
Expand Down
24 changes: 24 additions & 0 deletions apps/web/e2e/selector-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,27 @@ test('the compare category switcher exposes its state', async ({ page }) => {
const group = page.getByRole('group', { name: 'Chọn danh mục đối chiếu' });
await expect(group.getByRole('button', { pressed: true })).toHaveCount(1);
});

// Forced colours drop the colours that mark the pressed button; it must still
// look different from the others (DOS/Rate#1086).
for (const [path, name] of [
['/leaderboard', 'Chọn ngành hàng'],
['/compare', 'Chọn danh mục đối chiếu'],
] as const) {
test(`the pressed category stands out in forced colors on ${path}`, async ({
page,
}) => {
await page.emulateMedia({ forcedColors: 'active' });
expect((await page.goto(path))?.status()).toBe(200);
const group = page.getByRole('group', { name });
const look = (pressed: boolean) =>
group
.getByRole('button', { pressed })
.first()
.evaluate((el) => getComputedStyle(el).backgroundColor);
await expect(group.getByRole('button', { pressed: false })).not.toHaveCount(
0
);
expect(await look(true)).not.toBe(await look(false));
});
}
2 changes: 1 addition & 1 deletion apps/web/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@
"rank3Label": "3. místo",
"fullTableTitle": "Kompletní žebříček kategorie",
"verifiedReviewsSuffix": "ověřených recenzí",
"blockedSpamCount": "{count, plural, one {Zablokována # falešná/spamová recenze} few {Zablokovány # falešné/spamové recenze} other {Zablokováno # falešných/spamových recenzí}}",
"blockedSpamCount": "{count, plural, one {Zablokována # falešná či spamová recenze} few {Zablokovány # falešné či spamové recenze} other {Zablokováno # falešných či spamových recenzí}}",
"storesTrackedSuffix": "obchodů",
"viewAuditDetailBtn": "Recenze a 10 skupin parametrů →",
"inspectBtn": "Ověřit cenu",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"lint": "eslint . --max-warnings 0",
"preview": "next build && next start",
"start": "next start",
"test": "pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/core-rate-architecture.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/write-paths.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/logger.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/review-display.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/concurrency.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/catalog-listing.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/discount-verdict-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-detail-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-competitors.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/analyzer-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sold-count.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/content-lang.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/image-url.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/client-messages.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/seo.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/offer-plausibility.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sitemap-lastmod.test.ts",
"test": "pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/server/rate/__tests__/core-rate-architecture.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/app/api/v1/rate/__tests__/write-paths.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/logger.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/review-display.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/concurrency.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/catalog-listing.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/discount-verdict-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-detail-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/product-competitors.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/analyzer-copy.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sold-count.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/content-lang.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/image-url.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/client-messages.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/seo.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/offer-plausibility.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/sitemap-lastmod.test.ts && pnpm --filter @repo/crawlers exec tsx --tsconfig ../web/tsconfig.test.json ../web/src/lib/__tests__/leaderboard-badge-copy.test.ts",
"typecheck": "tsc --noEmit"
},
"browserslist": [
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/app/[locale]/merchants/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ export default async function MerchantProfilePage({
className="d-flex align-items-center small flex-wrap gap-3"
style={{ color: '#94a3b8' }}
>
<span className="d-inline-flex align-items-center gap-1">
{/* Items wrap: "Hodnocení tržiště: Neaktualizováno" is wider
than a 320px header column (DOS/Rate#1084). */}
<span className="d-inline-flex align-items-center flex-wrap gap-1">
<BsStarFill className="text-warning" />
{t('platformRatingLabel')}{' '}
<strong className="font-monospace text-white">
Expand All @@ -291,7 +293,7 @@ export default async function MerchantProfilePage({
: t('notUpdatedYet')}
</strong>
</span>
<span className="d-inline-flex align-items-center gap-1">
<span className="d-inline-flex align-items-center flex-wrap gap-1">
<BsPeopleFill className="text-info" />
{t('followersLabel')}{' '}
<strong className="font-monospace text-white">
Expand All @@ -300,7 +302,7 @@ export default async function MerchantProfilePage({
: '-'}
</strong>
</span>
<span className="d-inline-flex align-items-center gap-1">
<span className="d-inline-flex align-items-center flex-wrap gap-1">
<BsBoxSeam className="text-success" />
{t('trackedModelsLabel')}{' '}
<strong className="font-monospace text-white">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/[locale]/smartphones/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import ProductValueCalculator from '@/components/leaderboard/ProductValueCalcula
import ProductOffersExplorer from '@/components/leaderboard/ProductOffersExplorer';
import ProductSpecsExportToolbar from '@/components/leaderboard/ProductSpecsExportToolbar';
import ClientMessages from '@/components/providers/ClientMessages';
import { plausibleMinPrice } from '@/lib/offer-plausibility';
import { plausibleMinPrice } from '@repo/shared-data/rules';
import {
firstImageUrl,
usableImageUrls,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/[locale]/smartphones/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ export async function CategoryCatalogContent({

<div className="col-xl-5 col-lg-5 col-md-5 col-sm-12">
<div className="text-md-end">
<div className="dropdown d-inline-flex p-0">
<div className="dropdown d-inline-flex mw-100 p-0">
<a
href="#shortfilter"
className="dropdown-toggle toogleDrops px-3 py-2"
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/app/style/scss/_custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1522,3 +1522,24 @@ body .input-group > .input-group-text {
margin-right: 0 !important;
}
}

// Forced colours replace the background and text colours that mark a pressed
// toggle (btn-danger vs btn-outline-dark), so the selected category looked
// like the others in Windows High Contrast. Draw every pressed control with
// the system highlight instead (DOS/Rate#1086).
// `html` outranks Bootstrap's !important utilities (bg-white, text-dark) on
// children such as count badges.
@media (forced-colors: active) {
html [aria-pressed='true'] {
forced-color-adjust: none;
background-color: Highlight !important;
border-color: Highlight !important;
color: HighlightText !important;

* {
background-color: transparent !important;
border-color: HighlightText !important;
color: HighlightText !important;
}
}
}
5 changes: 5 additions & 0 deletions apps/web/src/app/style/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6423,6 +6423,11 @@ a.mousewheel.center {
border-radius: 50px;
transition: all ease 0.4s;
color: var(--bs-dark);
// Long labels ("ŘAZENÍ: VÝCHOZÍ (NEDÁVNO AKTUALIZOVANÉ)") wrap instead of
// pushing a 320px page sideways; Bootstrap's toggle is nowrap
// (DOS/Rate#1082).
max-width: 100%;
white-space: normal;

&:hover {
background-color: var(--bs-primary-bg-subtle);
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/elementary/LocaleSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,10 @@ export function LocaleSwitcher({
style={{
height: compact ? '38px' : '48px',
// The compact switcher sits in the phone header, which has to fit
// 320px (DOS/Rate#809).
padding: compact ? '0 8px' : '0 14px',
// 320px (DOS/Rate#809) with the widest label: "EN" and "CS" are
// ~5px wider than "VI" and pushed the menu toggle's tap area off
// screen (DOS/Rate#1082).
padding: compact ? '0 5px' : '0 14px',
backgroundColor: isDarkHeader
? 'rgba(255, 255, 255, 0.12)'
: 'var(--bs-card-bg, #ffffff)',
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/inspect/DeepInspectWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { SAMPLE_AUDIT_URLS } from '@/lib/inspect-samples';
import {
implausiblyLowOfferIds,
plausibleOffers,
} from '@/lib/offer-plausibility';
} from '@repo/shared-data/rules';
import { productImageUrl } from '@/lib/rate-domain-core';

const INSPECT_CONTROL_CHARS_RE = /\p{Cc}+/gu;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
type RadarSignal,
} from '@/lib/deal-radar';
import { escapeCsvCell } from '@/lib/csv-safe';
import { implausiblyLowOfferIds } from '@/lib/offer-plausibility';
import { implausiblyLowOfferIds } from '@repo/shared-data/rules';
import { contentLang } from '@/lib/content-lang';
import ContentText from '@/components/elementary/ContentText';

Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/lib/__tests__/leaderboard-badge-copy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

// The leaderboard's blocked-spam badge is about 114px wide on a 320px phone
// (~12 characters at its font size). "falešných/spamových" had no line-break
// opportunity (a slash only allows a break before digits), so the badge
// pushed /cs/leaderboard 16px sideways (DOS/Rate#1084). The E2E seed has no
// ignored reviews, so this guards the copy directly.
const MAX_UNBREAKABLE_CHARS = 12;

for (const locale of ['vi', 'en', 'cs'] as const) {
const file = join(__dirname, '..', '..', '..', 'locales', `${locale}.json`);
const message: string = JSON.parse(readFileSync(file, 'utf8')).leaderboard
.blockedSpamCount;
assert.ok(message, `${locale}: missing leaderboard.blockedSpamCount`);

// Keep the text of every plural branch; drop ICU syntax and placeholders.
const text = message
.replace(/\{count\}/g, '0')
.replace(/\{count, plural,/, '')
.replace(/\b(?:zero|one|two|few|many|other)\s*\{/g, ' ')
.replace(/[{}#]/g, ' ');

for (const word of text.split(/\s+/).filter(Boolean)) {
assert.ok(
word.length <= MAX_UNBREAKABLE_CHARS,
`${locale}: "${word}" cannot wrap inside the leaderboard badge`
);
}
}

console.log('leaderboard-badge-copy: ok');
2 changes: 1 addition & 1 deletion apps/web/src/lib/__tests__/offer-plausibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
isImplausiblyLowPrice,
plausibleMinPrice,
plausibleOffers,
} from '../offer-plausibility';
} from '@repo/shared-data/rules';

function main() {
console.warn('🧪 Offer plausibility suite');
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/rate-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createClient } from '@supabase/supabase-js';
import type { Database, MasterProduct } from '@repo/shared-data';
import { createLogger } from '@/lib/logger';
import { isPendingReview } from '@/lib/review-display';
import { plausibleOffers } from '@/lib/offer-plausibility';
import { plausibleOffers } from '@repo/shared-data/rules';
import {
type ReviewFlagCategory,
type ReviewItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9699,7 +9699,7 @@ async function runCoreArchitectureSuite() {
// 107. Issue #900: Offer-Plausibility, Product-Competitors & Copy/SEO/Sitemap Helpers Bounds
{
const plausMod =
require('../../../lib/offer-plausibility') as typeof import('../../../lib/offer-plausibility');
require('@repo/shared-data/rules') as typeof import('@repo/shared-data/rules');
const compMod =
require('../../../lib/product-competitors') as typeof import('../../../lib/product-competitors');
const verdictCopyMod =
Expand Down Expand Up @@ -11462,7 +11462,7 @@ async function runCoreArchitectureSuite() {
const {
implausiblyLowOfferIds: implausiblyLowOfferIds983,
plausibleOffers: plausibleOffers983,
} = await import('../../../lib/offer-plausibility');
} = await import('@repo/shared-data/rules');
const {
buildKeySpecLine: buildKeySpecLine983,
rankRelatedCompetitors: rankRelatedCompetitors983,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/server/rate/api-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'server-only';
import { NextResponse } from 'next/server';
import { plausibleMinPrice, plausibleOffers } from '@/lib/offer-plausibility';
import { plausibleMinPrice, plausibleOffers } from '@repo/shared-data/rules';
import {
MAX_VALID_VND_PRICE,
normalizeOfferOriginalPrice,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/server/rate/deals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
getRadarSignal,
type RadarSignal,
} from '../../lib/deal-radar';
import { plausibleOffers } from '../../lib/offer-plausibility';
import { plausibleOffers } from '@repo/shared-data/rules';
import {
RATE_CACHE_TAGS,
categoryCacheTag,
Expand Down
14 changes: 3 additions & 11 deletions apps/web/src/server/rate/extension.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'server-only';
import {
isImplausiblyLowPrice,
minimumListingPriceVnd,
plausibleOffers,
} from '../../lib/offer-plausibility';
} from '@repo/shared-data/rules';
import type { Database } from '@repo/shared-data';
import {
MAX_VALID_VND_PRICE,
Expand Down Expand Up @@ -269,11 +270,6 @@ const ACCESSORY_KEYWORDS_VI_EN = [
'camera lens protector',
];

const MINIMUM_DEVICE_PRICE_FLOOR_VND = 1_200_000;
const MINIMUM_EARBUDS_PRICE_FLOOR_VND = 400_000;
const EARBUDS_TITLE_REGEX =
/\b(?:airpods|earpods|buds|freebuds|soundpeats|wf-|wh-|tws|tai[\s_-]*nghe|earbuds|in[\s_-]*ear)\b/i;

function isDomainOrSubdomain(host: string, domain: string): boolean {
return host === domain || host.endsWith(`.${domain}`);
}
Expand Down Expand Up @@ -458,11 +454,7 @@ export function detectAccessoryOrJunk(params: {
}
}

const isEarbuds =
EARBUDS_TITLE_REGEX.test(titleLower) || EARBUDS_TITLE_REGEX.test(urlLower);
const minFloor = isEarbuds
? MINIMUM_EARBUDS_PRICE_FLOOR_VND
: MINIMUM_DEVICE_PRICE_FLOOR_VND;
const minFloor = minimumListingPriceVnd([titleLower, urlLower]);

if (
typeof safeParams.price === 'number' &&
Expand Down
5 changes: 5 additions & 0 deletions packages/shared-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./rules": {
"types": "./dist/rules/index.d.ts",
"import": "./dist/rules/index.js",
"require": "./dist/rules/index.js"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js",
Expand Down
Loading
Loading