Skip to content

[Parked · Feature] Materials storefront search, Carousell-style filters, rich PDP & Hot/Legit ranking #488

Open

Description

@owlstonbusiness-svg

Header


Problem

As supply grows beyond academic PDFs into speech, fitness, music, STEM, talks + formats (video, cloud vaults, exercise packs, physical), live discovery is too thin:

  1. Filters are curriculum/subject-centric — not Carousell-style format × category × price × level.
  2. Cards/PDP do not explain multi-variant packages or upgrade ladders (Issue 1 fields).
  3. No deterministic Hot & Legit ranking (views / likes / sales / Bayesian rating / cold-start) — quality buries under recency noise.
  4. Code path: materials-directory.tsx, material-card.tsx, listings.functions.ts, queries.ts, notes.$slug.tsx, courses.* — exact ranking job location: locate during impl.

Live counters already include at least sold_count (lifecycle migration); views/likes may need additive tables — justify if new.


CEO / product lock

Ship when unparked Park forever-for-now
Faceted search: keyword + #415 category pills + format / level / language / price / verified Vector / LLM semantic search
Sort: Hot & Legit (default), Top Sold, Newest, Price asc/desc Personalized ML collaborative filtering
Hot score: sales / likes / views / Bayesian rating / legitimacy / cold-start; exclude unpublished before slice Paid CPC sponsored slots inside organic results
Rich PDP: gallery, specs, variant selector, upgrade ladder, seller card In-app live chat (use existing checkout / WA desk flow)
Reuse #392 Big Cover + expandable card patterns Parallel card design system
Cloud vaults filterable as format; video still YT/Vimeo only Drive as video host

Exact behaviour

Triggers

  1. Visitor opens Materials / Notes storefront (home notes row, directory page — exact route locate during impl; known: /notes/$slug, /courses/, /courses/$courseId).
  2. Visitor applies filters, changes sort, opens PDP, likes/saves, or starts Buy/Unlock (existing cashier).

Step-by-step flow

  1. Browse: Sticky search (Search notes, video courses, mock papers, fitness guides… / 繁中 equivalent). Horizontal [Feature · V1] Extracurriculars — hybrid tags, portfolio, ecosystem #415 industry pills: All + six broad_category codes (short label Talks & Community OK).
  2. Filters (Carousell-style chips): Format (included_formats), Level, Language (EN / 繁中 / Cantonese / Mandarin), Price HKD range, Trust (verified seller, has sample). Multi-select within a facet = OR; across facets = AND. Persist in URL query (?broad_category=debate_languages&formats=video_course_pack&sort=hot).
  3. Cards: Cover (Shop UI (Big Cover + Expandable Card), Topic Folder Bundles, Video Courses & General Courses #392), HOT/NEW badge, primary format pill (+N), title 2-line clamp, seller + verified, ★ rating (n) · sold · ♥ likes, price From HK$ if multi-variant else HK$.
  4. PDP: Gallery + What’s Inside specs; sticky buy box with variant radio cards; dynamic what-you-get; Upgrade ladder when prerequisite/next set; Like/Save; Share; primary Buy/Unlock → existing Notes shop, manual cashier, no off-platform contact #203/# Issue D7 — Order, receipt, and dispute #269 rail for selected variant_id.
  5. Events: PDP view increments deduped (max 1 per user/session fingerprint per listing per 6 hours). Like toggles save row + likes_count.

Data in / out / side effects

  • In: q, broad_category, skills_tags[], formats[], skill_level[], languages[], min_price, max_price, verified_only, sort (hot|top_sold|newest|price_asc|price_desc), page/cursor, limit.
  • Out: paginated public cards + total + facet counts; PDP public DTO without delivery payloads.
  • Side effects: view/like counters; hot_score refresh on metrics update + periodic batch (e.g. 15 min — locate job runner during impl).

MUST / MUST NOT

  • MUST filter status = published (and not deleted/suspended) before sort slice / pagination.
  • MUST match listing if any selected format ∈ included_formats.
  • MUST dedupe views (no refresh farming).
  • MUST Bayesian-smooth ratings so 1×5.0★ cannot beat many 4.8★ + sales.
  • MUST NOT require academic score verification to appear in Hot; verified identity may apply legitimacy multiplier only.
  • MUST NOT leak private delivery URLs on PDP for non-buyers.
  • MUST NOT insert paid boost slots into organic Hot.

UI / look

Storefront wireframe

+-----------------------------------------------------------------------------------+
| [🔍 Search video courses, DSE/IB mocks, speech guides, workout plans…          ] |
| Categories (#415): (• All) (Academics) (Debate & Lang) (Music & Arts)             |
|   (Sports) (STEM) (Talks & Community)                                             |
| Filters: [Format ▾] [Level ▾] [Price HK$ ▾] [✓ Verified]                          |
| Pills: [📄 Notes] [📝 Exercise] [🎬 Video] [📁 Cloud] [⚡ Guide] [📦 Physical]   |
|-----------------------------------------------------------------------------------|
| Showing N materials                          Sort: [ 🔥 Hot & Legit ▾ ]           |
| +-------------+ +-------------+ +-------------+                                   |
| | HOT | Video | | NEW | Paper | | Guide       |                                   |
| | title…      | | title…      | | title…      |                                   |
| | ★ · sold ♥  | | …           | | …           |                                   |
| | From HK$280 | | HK$190      | | From HK$150 |                                   |
| +-------------+ +-------------+ +-------------+                                   |
+-----------------------------------------------------------------------------------+

PDP wireframe

+-----------------------------------------+-----------------------------------------+
| Gallery / sample / teaser               | Format pills · Title                    |
| What's Inside specs                     | ★ · sold · views                        |
| Upgrade ladder: L1 → [YOU] → L3         | Variant radios (Tier 1/2/3)             |
| Seller card → 1-on-1 (if public tutor)  | [ Buy / Unlock Tier X (HK$…) ] [♥] [Share]|
+-----------------------------------------+-----------------------------------------+

States

State Copy / UI
Empty filters No materials match your exact filters / 找不到符合篩選條件的教材 + Clear All + top 4 Hot in parent category
Loading 6 skeleton cards (stable aspect — avoid CLS)
Error Could not load materials. Please retry. / 無法載入教材,請重試。
Mobile PDP Sticky bottom bar: Tier 2 · HK$480 [Unlock →]

Copy EN + 繁中

Element EN 繁中
Sort hot 🔥 Hot & Legit (Recommended) 🔥 熱門及高評分推薦
Sort sold Top Sold 最高銷量
Sort new Newest Arrivals 最新上架
Sort price↑ Price: Low to High 價格:由低至高
Upgrade title Step-by-Step Learning Upgrade Path 循序漸進學習升級路線
Multi price From HK$ HK$ 起
Cross-sell Also available for 1-on-1 Tutoring → 同時提供 1對1 私人補習/指導 →

Logic & edge cases

Case Expected
Cold start (<14d, rich preview ≥2 assets + description length gate) Decaying NEW_LISTING_BOOST so new complete listings get discovery
Low supply (<4 in category) Show matches, then divider Popular Materials Across MatchMax / 全站熱門教材
Multi-facet AND across facets; OR within format/level multi-select
View spam hash(listing_id + user_id|session) ≤1 / 6h
Unverified seller Can rank via sales/reviews; verified → ×1.12 legitimacy only
Unpublished in middle of result build Excluded before slice — never appear mid-page
Broken upgrade neighbor Omit node; no 404 crash on PDP

Implementation checklist

export const RANKING_WEIGHTS = {
  VERIFIED_SALE_POINTS: 25.0,
  LIKE_SAVE_POINTS: 4.0,
  UNIQUE_VIEW_30D_POINTS: 0.25,
  BAYESIAN_PRIOR_MEAN: 4.2,
  BAYESIAN_MIN_REVIEWS: 5,
  NEW_LISTING_BOOST_DAYS: 14,
  NEW_LISTING_MAX_BOOST: 18.0,
} as const;

export function computeMaterialHotScore(item: {
  sales_30d: number;
  sales_total: number;
  likes_count: number;
  views_30d: number;
  rating_avg: number;
  rating_count: number;
  has_rich_preview: boolean;
  is_seller_verified: boolean;
  age_in_days: number;
}): number {
  const velocitySales = item.sales_30d * RANKING_WEIGHTS.VERIFIED_SALE_POINTS;
  const lifetimeSales = Math.log10(1 + item.sales_total) * 15.0;
  const likeScore = item.likes_count * RANKING_WEIGHTS.LIKE_SAVE_POINTS;
  const viewScore = Math.min(item.views_30d, 2000) * RANKING_WEIGHTS.UNIQUE_VIEW_30D_POINTS;
  const raw = velocitySales + lifetimeSales + likeScore + viewScore;

  const m = RANKING_WEIGHTS.BAYESIAN_MIN_REVIEWS;
  const C = RANKING_WEIGHTS.BAYESIAN_PRIOR_MEAN;
  const bayesian =
    (item.rating_count / (item.rating_count + m)) * item.rating_avg +
    (m / (item.rating_count + m)) * C;
  const qualityMultiplier = Math.max(0.4, bayesian / 4.2);

  let legitimacy = 1.0;
  if (item.has_rich_preview) legitimacy += 0.15;
  if (item.is_seller_verified) legitimacy += 0.12;

  let cold = 0;
  if (item.age_in_days < RANKING_WEIGHTS.NEW_LISTING_BOOST_DAYS && item.has_rich_preview) {
    const ratio =
      (RANKING_WEIGHTS.NEW_LISTING_BOOST_DAYS - item.age_in_days) /
      RANKING_WEIGHTS.NEW_LISTING_BOOST_DAYS;
    cold = RANKING_WEIGHTS.NEW_LISTING_MAX_BOOST * ratio;
  }

  const ageFactor = Math.pow(1 + Math.max(0, item.age_in_days - 7) / 30, 0.25);
  return Number((((raw * qualityMultiplier * legitimacy) / ageFactor) + cold).toFixed(4));
}
  • Query order: (1) published + not deleted (2) facets including included_formats && selected (3) order by hot_score DESC, sales_total DESC, created_at DESC when sort=hot (4) paginate last.
  • Indexes: (status, min_price_hkd), GIN included_formats, broad_category, optional hot_score materialized column refreshed on batch.
  • New tables only if necessary: material_likes / view dedupe store — justify in PR; prefer additive counters on digital_materials (views_count, likes_count, hot_score) if absent.

Acceptance criteria

  • Filters by [Feature · V1] Extracurriculars — hybrid tags, portfolio, ecosystem #415 broad_category, format, level, language, price, verified work; URL shareable.
  • Multi-format OR within format facet; AND across facets.
  • Default Hot & Legit uses computeMaterialHotScore; cold-start boost for rich new listings (<14d).
  • Unpublished excluded before slice.
  • PDP variant switch updates price + bullets; upgrade ladder navigates when links valid.
  • PDP refresh spam ≤1 view / 6h / session.
  • Public responses never include delivery payloads.
  • Buy CTA uses existing 20% materials cashier — no parallel checkout.

Out of scope


Test plan

  1. Seed 4 listings: (A) video+cloud high sales/likes/rating; (B) exercise pack 1×5.0★ few sales; (C) new rich speech guide 1d old 0 sales; (D) sparse 90d old.
  2. Default Hot: A ahead of B (Bayesian); C above D (cold start).
  3. Filter Format=Video → only video_course_pack in included_formats.
  4. PDP desktop+mobile: switch tiers; click next upgrade; verify sticky CTA on mobile.
  5. Refresh PDP 20× → views +1 only.

Open questions

  1. Exact storefront route unification (/notes vs /courses vs single /shop) — CEO preference when unparking?
  2. Buyer reviews: use existing admin_star_rating only, or ship buyer review table in this issue?
  3. “Verified seller” signal source (which tutor verification flag) — locate during impl / confirm.
  4. Guest like/save: require login, or cookie-only save list?

Activity

  1. owlstonbusiness-svg commented on Oct 27, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Materials Marketplace Pack (Parked) — Phase B (Buyer / discovery)

    Sibling issues:

    Depends on #487 fields for filters/variants/upgrade ladder. Hot & Legit ranking lives here. Supersedes/extends thin D5/D9 (#267/#271) when unparked.

  2. owlstonbusiness-svg commented on Oct 27, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Cross-link (not required work). Parked Make Offer CTA / PDP behaviour for materials: #490. Storefront search/Hot ranking here stays separate; offer UI is opt-in per listing when #490 unparks.

  3. owlstonbusiness-svg commented on Oct 31, 2026

    @owlstonbusiness-svg
    CollaboratorAuthor

    Cross-lock from #553 (CEO 2026-10-31) — Carousel stays; tags additive

    • Parked Carousell-style storefront filters / broad_category chips remain the structured discovery layer when this issue unparks.
    • User-generated tags on notes/materials (SoT on #543 + search #553) are additive, not a replacement for carousel filters.
    • HYBRID locked: keep admin taxonomy for faceting; free tags for long-tail. REJECT tags-only and REJECT carousel-only.
    • When unparking, wire storefront search to both category facets and exact tag chips (same normalize rules as [Feature · V1] Global Search Bar: free-text across Tutors, Cases, Notes, Centres, Users & Posts #553).
    • Fee / Cap.57 SoT unchanged. Issue remains Parked until CEO unpark.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    On HoldExtra attention is neededenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions