Exactly as described
Verified purchaseOla N.
Arrived in two days, well packed.
3 people found this helpful
Reviews sorted by a select (newest, highest, most helpful), filtered by stars with counts, shown a page at a time.
3 reviews
Arrived in two days, well packed.
Does the job. The lid is a bit loose.
The photos make it look twice the size.
No votes yet.
It needs theme.qwe, as every component does, and avatar.qwe and review_card.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/avatar.qwe$ curl -O https://quillolang.org/components/ui/review_card.qwe$ curl -O https://quillolang.org/components/ui/review_list.qweimport ui/theme.qwe use theme_cssuse ui/review_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>The code of the preview above.
let review_list_demo_reviews = [ { id: "a1", rating: 5, title: "Exactly as described", name: "Ola N.", date: "2026-10-02", verified: true, helpful: 3, text: "Arrived in two days, well packed." }, { id: "a2", rating: 2, title: "Smaller than it looks", name: "Jun K.", date: "2026-08-21", helpful: 9, text: "The photos make it look twice the size." }, { id: "a3", rating: 4, title: "Good value", name: "Marta R.", date: "2026-09-14", verified: true, helpful: 12, text: "Does the job. The lid is a bit loose." }, { id: "a4", rating: 5, title: "Second one I bought", name: "Sam T.", date: "2026-07-01", helpful: 1, text: "Gave the first to my sister." }, { id: "a5", rating: 3, name: "Anonymous", date: "2026-06-11", helpful: 0, text: "Fine." }, { id: "a6", rating: 4, title: "Quiet", name: "Lea B.", date: "2026-05-30", verified: true, helpful: 5, text: "Quieter than the old one." }, { id: "a7", rating: 1, title: "Broke in a week", name: "Ivo P.", date: "2026-04-18", helpful: 20, text: "The hinge snapped. Support replaced it, to be fair." },]component ReviewListDemoLive() { state mine = [] state log = "No votes yet." <div.demo-stack> <ReviewList id="rl-live" reviews={take(review_list_demo_reviews, 3)} voted={mine} on_helpful={(rid) => { mine = contains(mine, rid) ? mine | filter((x) => x != rid) : push(mine, rid) log = `Voted: ${len(mine) == 0 ? "none" : join(mine, ", ")}.` }}/> <p.demo-note>{log}</p> </div>}view ReviewListDemo() = <div.demo-stack> <form method="post" action="/reviews/helpful"> <ReviewList id="rl-all" reviews={review_list_demo_reviews} limit={4}/> </form> <ReviewListDemoLive/> </div>// ReviewList: reviews of one thing, sorted by a select (newest, oldest, highest,// lowest, most helpful) and filtered by stars with a row of buttons that count// them, shown a page at a time with a "Show more" button.//// <ReviewList id="reviews" reviews={reviews} limit={5}/>// <ReviewList reviews={reviews} voted={mine} on_helpful={(id) => { mine = push(mine, id) }}/>//// reviews: a list of maps as ReviewCard takes them ({ id, rating, title, text, name,// src?, date, verified?, helpful?, images?, response? }); each is drawn by ReviewCard// (review_card.qwe, which this file needs, with avatar.qwe).// sort: "newest" (default), "oldest", "highest", "lowest" or "helpful" to start with.// stars: 1 to 5 to start filtered to that many; null shows all. limit: how many to// show at first, the rest behind "Show N more" (0, the default, shows every one).// voted: the ids this reader found helpful (their buttons show pressed).// on_helpful(id): inside one of your components, called with the review's id when// Helpful is clicked; without it the button submits `helpful` = id to a form// around the list. label: the list's name for screen readers ("Reviews").// empty: what it says with no reviews to show. id: a prefix for its ids; each review's card is `review-<id>-<review id>`.import ui/review_card.qwe use ReviewCardimport ui/theme.qwe use ui_clslet review_list_css = css`.ui-review-list { display: grid; gap: 12px; min-width: 0; font: 14px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-review-list-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }.ui-review-list-filter { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; padding: 0; list-style: none; }.ui-review-list-star { display: inline-flex; align-items: center; gap: 4px; height: 30px; padding: 0 10px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-review-list-star:hover { border-color: var(--ui-strong); }.ui-review-list-star:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-review-list-star[aria-pressed="true"] { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-review-list-star:disabled { opacity: .5; cursor: not-allowed; }.ui-review-list-star svg { width: 12px; height: 12px; color: var(--ui-warning); }.ui-review-list-star small { color: var(--ui-muted); font-weight: 400; }.ui-review-list-star[aria-pressed="true"] small { color: inherit; }.ui-review-list-sort { display: inline-flex; align-items: center; gap: 8px; color: var(--ui-muted); font-size: 13px; }.ui-review-list-sort select { height: 32px; padding: 0 28px 0 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface) no-repeat right 9px center / 10px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6' fill='none' stroke='%23888' stroke-width='1.5'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E"); color: var(--ui-fg); font: 13px/1 var(--ui-font); appearance: none;}.ui-review-list-sort select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-review-list-count { color: var(--ui-muted); font-size: 13px; }.ui-review-list-items { display: grid; }.ui-review-list-items > .ui-review-list-item:last-child { border-bottom: 0; }.ui-review-list-empty { padding: 24px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm); color: var(--ui-muted); text-align: center; }.ui-review-list-more { justify-self: center; height: 34px; padding: 0 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-review-list-more:hover { border-color: var(--ui-strong); }.ui-review-list-more:focus-visible { outline: none; box-shadow: var(--ui-focus); }`// The reviews in an order: newest, oldest, highest, lowest or helpful.fn review_list_sorted(reviews: list, by: str) -> list = match by { "oldest" => sort_by(reviews, (r) => str(r.date ?? "")) "highest" => reverse(sort_by(reviews, (r) => r.rating ?? 0)) "lowest" => sort_by(reviews, (r) => r.rating ?? 0) "helpful" => reverse(sort_by(reviews, (r) => r.helpful ?? 0)) _ => reverse(sort_by(reviews, (r) => str(r.date ?? ""))) }component ReviewList(reviews: list = [], sort: str = "newest", stars: int? = null, limit: int = 0, voted: list = [], on_helpful = null, label: str = "Reviews", empty: str = "No reviews yet.", id: str = "reviews", class: str? = null) { style global review_list_css state by = sort state only = stars state shown = limit let counts = for s in [5, 4, 3, 2, 1] => { stars: s, n: count(reviews, (r) => floor(r.rating ?? 0) == s) } let kept = review_list_sorted(only == null ? reviews : reviews | filter((r) => floor(r.rating ?? 0) == only), by) let page = shown > 0 && len(kept) > shown ? take(kept, shown) : kept let left = len(kept) - len(page) <section class={ui_cls(["ui-review-list", class])} id={id} aria-label={label}> <div.ui-review-list-bar> <ul.ui-review-list-filter aria-label="Filter by stars"> <li><button.ui-review-list-star type="button" aria-pressed={only == null ? "true" : "false"} @click={() => { only = null }}> All <small>{len(reviews)}</small></button></li> {for c in counts => <li key={c.stars}> <button.ui-review-list-star type="button" aria-pressed={only == c.stars ? "true" : "false"} disabled={c.n == 0} aria-label={`${c.stars} stars, ${c.n}`} @click={() => { only = only == c.stars ? null : c.stars }}> {c.stars}<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6L2.5 9.4l6.6-.8Z"/></svg> <small>{c.n}</small> </button> </li>} </ul> <label.ui-review-list-sort>Sort by <select name="sort" @change={(e) => { by = e.value }}> <option value="newest" selected={by == "newest"}>Newest</option> <option value="oldest" selected={by == "oldest"}>Oldest</option> <option value="highest" selected={by == "highest"}>Highest rated</option> <option value="lowest" selected={by == "lowest"}>Lowest rated</option> <option value="helpful" selected={by == "helpful"}>Most helpful</option> </select> </label> </div> <p.ui-review-list-count role="status"> {only == null ? `${len(reviews)} ${len(reviews) == 1 ? "review" : "reviews"}` : `${len(kept)} of ${len(reviews)} reviews with ${only} ${only == 1 ? "star" : "stars"}`} </p> {len(kept) == 0 ? <div.ui-review-list-empty>{empty}</div> : <div.ui-review-list-items> {for r in page => <ReviewCard key={r.id} {...r} id={`${id}-${r.id}`} voted={contains(voted, r.id)} class="ui-review-list-item" on_helpful={on_helpful != null ? () => { on_helpful(r.id) } : null}/>} </div>} {left > 0 && <button.ui-review-list-more type="button" @click={() => { shown = shown + limit }}> Show {left > limit ? limit : left} more </button>} </section>}On ReviewList: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.