Repository navigation
[Parked · Feature] Materials storefront search, Carousell-style filters, rich PDP & Hot/Legit ranking #488
Copy link
Copy link
Open
Labels
On HoldExtra attention is neededExtra attention is neededenhancementNew feature or requestNew feature or request
Description
Header
- Phase: Parked / On Hold — not V1. Do not treat as urgent vs [Urgent · V1] Unified account + roles + child sub-profiles + hours ledger #459 / #461A / desk/fee. Unpark only after CEO says so and Notes Shop escrow path (Notes shop, manual cashier, no off-platform contact #203 / # Issue D7 — Order, receipt, and dispute #269 / # Issue D8 — Shop rules #270 / [Feature / Notes Shop] Escrow Hold & Anti-Scam (Seller-Funded Refunds) #429) is in daily use, and Pack Issue 1 (listing builder) fields exist to filter on.
- Must NOT block: Direct tutor profile URLs, 1-on-1 enquiry, Cap.57 matching, or current
/notes/$slug+ home notes row behaviour for legacy notes. - Owner: Locate during impl (storefront:
src/features/materials/materials-directory.tsx,material-card.tsx,home-notes-row.tsx,notes.$slug/ courses routes, listing queries). - Depends on / related:
- Pack Issue 1 (this Materials Marketplace Pack) — structured
broad_category,included_formats, variants, upgrade links. - #415 — same
broad_categorypills on shop cards. - #392 — Big Cover + expandable card; do not rebuild chrome from scratch.
- Money / unlock: #203, #269, #270, #429 — 20% materials fee; FPS hold; seller-funded refunds.
- Different: #474 memberships ≠ one-shot SKUs.
- Supersede/extend thin #267 D5, #271 D9 (comment after create).
- Pack Issue 1 (this Materials Marketplace Pack) — structured
- Architecture: Extend existing directory/query patterns on
digital_materials. No new search microservice. Prefer changingmaterials-directory/ RPCs over greenfield.
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:
- Filters are curriculum/subject-centric — not Carousell-style format × category × price × level.
- Cards/PDP do not explain multi-variant packages or upgrade ladders (Issue 1 fields).
- No deterministic Hot & Legit ranking (views / likes / sales / Bayesian rating / cold-start) — quality buries under recency noise.
- 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
- Visitor opens Materials / Notes storefront (home notes row, directory page — exact route locate during impl; known:
/notes/$slug,/courses/,/courses/$courseId). - Visitor applies filters, changes sort, opens PDP, likes/saves, or starts Buy/Unlock (existing cashier).
Step-by-step flow
- Browse: Sticky search (
Search notes, video courses, mock papers, fitness guides…/ 繁中 equivalent). Horizontal [Feature · V1] Extracurriculars — hybrid tags, portfolio, ecosystem #415 industry pills: All + sixbroad_categorycodes (short label Talks & Community OK). - 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). - 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, priceFrom HK$if multi-variant elseHK$. - 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. - 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
- Touch:
materials-directory.tsx,material-card.tsx,listings.functions.ts,queries.ts,home-notes-row.tsx(keep folders/courses off home notes row per Shop UI (Big Cover + Expandable Card), Topic Folder Bundles, Video Courses & General Courses #392 leak fix),notes.$slug.tsx,courses/*, like/save storage locate during impl. - Ranking constants + pseudo-code:
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 whensort=hot(4) paginate last. - Indexes:
(status, min_price_hkd), GINincluded_formats,broad_category, optionalhot_scorematerialized column refreshed on batch. - New tables only if necessary:
material_likes/ view dedupe store — justify in PR; prefer additive counters ondigital_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
- Listing builder / variants schema write path → Issue 1.
- Seller Studio charts / promo copy generator → Issue 3.
- Paid boost / sponsored auction; vector search; native DRM; SF API.
- [Parked · Epic] Creator-Monetized Study Vehicles — Gated Vaults · AI Mock Builder · Hybrid Tuition Bridge #474 gated memberships.
Test plan
- 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.
- Default Hot: A ahead of B (Bayesian); C above D (cold start).
- Filter Format=Video → only
video_course_packinincluded_formats. - PDP desktop+mobile: switch tiers; click next upgrade; verify sticky CTA on mobile.
- Refresh PDP 20× → views +1 only.
Open questions
- Exact storefront route unification (
/notesvs/coursesvs single/shop) — CEO preference when unparking? - Buyer reviews: use existing admin_star_rating only, or ship buyer review table in this issue?
- “Verified seller” signal source (which tutor verification flag) — locate during impl / confirm.
- Guest like/save: require login, or cookie-only save list?
Metadata
Metadata
Assignees
Labels
On HoldExtra attention is neededExtra attention is neededenhancementNew feature or requestNew feature or request
Activity
owlstonbusiness-svg commentedon Oct 27, 2026
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.
owlstonbusiness-svg commentedon Oct 27, 2026
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.
owlstonbusiness-svg commentedon Oct 31, 2026
Cross-lock from #553 (CEO 2026-10-31) — Carousel stays; tags additive
broad_categorychips remain the structured discovery layer when this issue unparks.