src/lib/answers.js +49 −0
@@ -0,0 +1,49 @@
+// Five plain-language answers per country, read straight off the segment scores.
+// Scores are in the order of segments.yaml: possession, cultivation, enforcement,
+// sharing, consumption, access. See segments.yaml for what each level means.
+
+export const ANSWER_LABEL = { yes: "Yes", limited: "Limited", no: "No" };
+
+const pick = (score, steps) => steps.find(([min]) => score >= min).slice(1);
+
+export const QUESTIONS = [
+ { id: "carry", q: "Can I carry it?", seg: 0, steps: [
+ [5, "yes", "Legal up to a limit"],
+ [3, "limited", "Not a crime, but you can be fined"],
+ [0, "no", "It is a crime"],
+ ] },
+ { id: "grow", q: "Can I grow it?", seg: 1, steps: [
+ [4, "yes", "A few plants at home are legal"],
+ [2, "limited", "A few plants are tolerated"],
+ [0, "no", "It is a crime"],
+ ] },
+ { id: "buy", q: "Can I buy it?", seg: 5, steps: [
+ [6, "yes", "In licensed shops"],
+ [4, "yes", "Through clubs or state outlets"],
+ [3, "limited", "Tolerated shops or clubs, or on prescription"],
+ [1, "limited", "On prescription only"],
+ [0, "no", "There is no legal source"],
+ ] },
+ { id: "public", q: "Can I use it in public?", seg: 4, steps: [
+ [7, "yes", "Wherever smoking is allowed"],
+ [5, "limited", "In some places, with many restrictions"],
+ [3, "no", "In private only"],
+ [0, "no", "Use itself is an offence"],
+ ] },
+ // Not a yes/no question, so each step carries its own short label.
+ { id: "caught", q: "What if I'm caught?", seg: 2, steps: [
+ [6, "yes", "If you stay within the limits", "Nothing"],
+ [4, "limited", "Or a referral to treatment", "A fine"],
+ [2, "limited", "Usually a fine; prison is possible", "Fine or prison"],
+ [1, "no", "Users are routinely jailed", "Prison"],
+ [0, "no", "Long sentences or worse", "Severe punishment"],
+ ] },
+];
+
+// `s` is the array of six segment scores of a country, state or province.
+export const answersFor = (s) =>
+ QUESTIONS.map(({ id, q, seg, steps }) => {
+ const [answer, text, label = ANSWER_LABEL[answer]] = pick(s[seg], steps);
+ return { id, q, answer, text, label };
+ });
+
src/pages/can-i.astro +86 −0
@@ -0,0 +1,86 @@
+---
+// Five plain-language answers for every country, side by side.
+import Base from "../layouts/Base.astro";
+import { places, regions } from "../lib/countries.js";
+import { answersFor, QUESTIONS, ANSWER_LABEL } from "../lib/answers.js";
+
+const base = import.meta.env.BASE_URL;
+const rows = [...places]
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((c) => ({ c, answers: answersFor(c.s) }));
+---
+<Base title="Can I?" description="Can I carry, grow, buy or use cannabis in public, and what happens if I'm caught? Five plain answers for every country.">
+ <main class="cani-page">
+ <section class="hero hero-sm">
+ <p class="eyebrow">Can I?</p>
+ <h1>Five plain answers for every country</h1>
+ <p class="lede">Can you carry it, grow it, buy it or use it in public, and what happens if you are caught? The answers are read from each country's scores, so they are a rough guide, not legal advice. Open a country for the details.</p>
+ </section>
+
+ <div class="filters cani-filters">
+ <input id="caniSearch" type="search" placeholder="Search country…" aria-label="Search country">
+ <select id="caniRegion" aria-label="Region">
+ <option value="">All regions</option>
+ {regions.map((r) => <option value={r}>{r}</option>)}
+ </select>
+ <div class="legend cani-legend">
+ {Object.entries(ANSWER_LABEL).map(([k, label]) => <span><i class={`sw a-${k}`}></i>{label}</span>)}
+ </div>
+ </div>
+
+ <div class="card table-card">
+ <div class="table-scroll">
+ <table class="cani" id="caniTable">
+ <thead>
+ <tr>
+ <th scope="col">Country</th>
+ {QUESTIONS.map((x) => <th scope="col">{x.q}</th>)}
+ </tr>
+ </thead>
+ <tbody>
+ {rows.map(({ c, answers }) => (
+ <tr data-name={c.name.toLowerCase()} data-region={c.region}>
+ <th scope="row"><a href={`${base}country/${c.slug}/`}><span class="flag">{c.flag}</span> {c.name}</a></th>
+ {answers.map((x) => (
+ <td class={`a-${x.answer}`} title={x.text}>
+ <b>{x.label}</b>
+ <small>{x.text}</small>
+ </td>
+ ))}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <p class="table-updated" id="caniCount">{rows.length} countries and territories</p>
+ </main>
+
+ <script>
+ const search = document.getElementById("caniSearch");
+ const region = document.getElementById("caniRegion");
+ const rows = [...document.querySelectorAll("#caniTable tbody tr")];
+ const count = document.getElementById("caniCount");
+ const apply = () => {
+ const q = search.value.trim().toLowerCase();
+ let shown = 0;
+ for (const tr of rows) {
+ const ok = tr.dataset.name.includes(q) && (!region.value || tr.dataset.region === region.value);
+ tr.hidden = !ok;
+ if (ok) shown++;
+ }
+ count.textContent = `${shown} of ${rows.length} countries and territories`;
+ };
+ // The filter bar and the table's header row stay in view while scrolling;
+ // they need to know how tall the bars above them are.
+ const measure = () => {
+ const root = document.documentElement.style;
+ root.setProperty("--hdr", `${document.querySelector(".topbar").offsetHeight}px`);
+ root.setProperty("--bar", `${document.querySelector(".cani-filters").offsetHeight}px`);
+ };
+ measure();
+ addEventListener("resize", measure);
+ search.addEventListener("input", apply);
+ region.addEventListener("change", apply);
+ </script>
+</Base>