Changelog c4021c7
expand federated countries
7 files of code, design or docs +294 −21 lines
Code, design and documents 7 files
src/components/CountryArticle.astro +70 −15
@@ -4,9 +4,16 @@ import { SEGMENTS, CONTEXT, REC_LABEL, MED_LABEL } from "../data/data.js"; import { DETAILS } from "../data/details.js"; -import { countries, segAvg, segmentPoints, segmentMax } from "../lib/countries.js"; +import { countries, segAvg, segmentPoints, segmentMax, subunitsOf } from "../lib/countries.js"; import { tierOf, tierColor } from "../lib/tiers.js"; -const { c } = Astro.props; -const d = DETAILS[c.id]; +// `sub` is optional: one state, province or territory of `c`. Without it the national view is shown. +const { c, sub } = Astro.props; +const base = import.meta.env.BASE_URL; +const group = subunitsOf(c.id); +const v = sub ?? c; // whichever entity's scores are on display +const d = sub ? null : DETAILS[c.id]; +const countryUrl = `${base}country/${c.slug}/`; +const unitUrl = (x) => `${countryUrl}${x.id}/`; +const byName = group ? [...group.units].sort((a, b) => a.name.localeCompare(b.name)) : []; const r = 42; const circ = 2 * Math.PI * r; @@ -17,6 +24,6 @@ const circ = 2 * Math.PI * r; <span class="d-flag">{c.flag}</span> <div> - <h1>{c.name}</h1> - <p class="sub">{c.region}</p> + <h1>{v.name}</h1> + <p class="sub">{sub ? `${group.label} · ${c.name}` : c.region}</p> </div> </div> @@ -24,24 +31,50 @@ const circ = 2 * Math.PI * r; <svg class="ring" width="104" height="104" viewBox="0 0 104 104" aria-hidden="true"> <circle cx="52" cy="52" r={r} fill="none" stroke="var(--border)" stroke-width="9" /> - <circle cx="52" cy="52" r={r} fill="none" stroke={tierColor(c.raw)} stroke-width="9" stroke-linecap="round" - stroke-dasharray={`${(c.raw / 100) * circ} ${circ}`} transform="rotate(-90 52 52)" /> - <text x="52" y="62" text-anchor="middle">{c.score}</text> + <circle cx="52" cy="52" r={r} fill="none" stroke={tierColor(v.raw)} stroke-width="9" stroke-linecap="round" + stroke-dasharray={`${(v.raw / 100) * circ} ${circ}`} transform="rotate(-90 52 52)" /> + <text x="52" y="62" text-anchor="middle">{v.score}</text> </svg> <div> - <div class="tier">{tierOf(c.raw).name}</div> - <p class="sub">#{c.rank} of {countries.length} worldwide<br>#{c.regionRank} of {c.regionCount} in {c.region}</p> + <div class="tier">{tierOf(v.raw).name}</div> + {sub + ? <p class="sub">#{sub.rank} of {group.units.length} in {c.name}<br>would rank #{sub.worldRank} among countries</p> + : <p class="sub">#{c.rank} of {countries.length} worldwide<br>#{c.regionRank} of {c.regionCount} in {c.region}</p>} </div> </div> <div class="d-chips"> - <span class={`chip s-${c.rec}`}>Recreational: {REC_LABEL[c.rec]}</span> - <span class={`chip s-${c.med}`}>Medical: {MED_LABEL[c.med]}</span> + <span class={`chip s-${v.rec}`}>Recreational: {REC_LABEL[v.rec]}</span> + <span class={`chip s-${v.med}`}>Medical: {MED_LABEL[v.med]}</span> </div> - <p class="d-note">{c.note}</p> + <p class="d-note">{v.note}</p> + {group && ( + <label class="field sub-select"> + <span>{group.label}</span> + <select data-subunit-select aria-label={`Choose a ${group.label.toLowerCase()} of ${c.name}`}> + <option value={countryUrl} selected={!sub}>All of {c.name} (national estimate)</option> + {byName.map((x) => <option value={unitUrl(x)} selected={sub?.id === x.id}>{x.name} · {x.score}</option>)} + </select> + </label> + )} </div> <div class="c-main"> <h2 class="d-h">Score breakdown <span>▏world average</span></h2> - <p class="d-hint">Select a segment to show or hide the exact rules and sources.{d && ` Reviewed ${d.reviewed}.`}</p> - {SEGMENTS.map((s, i) => { + {sub + ? <p class="d-hint">{group.intro} Segment-by-segment explanations are written at national level — <a href={countryUrl} data-subunit>see {c.name}</a>.</p> + : <p class="d-hint">Select a segment to show or hide the exact rules and sources.{d && ` Reviewed ${d.reviewed}.`}</p>} + {sub && SEGMENTS.map((s, i) => ( + <div class="segrow"> + <div class="segrow-top"> + <span>{s.name} <small>· {s.weight}%</small></span> + <b>{sub.s[i]}/10 <small class="vs">{c.name}: {c.s[i]}</small></b> + </div> + <div class="bar" title={`World average ${segAvg[i].toFixed(1)}/10`}> + <i style={`width:${sub.s[i] * 10}%;background:${tierColor(sub.s[i] * 10)}`}></i> + <span class="avg-tick" style={`left:calc(${segAvg[i] * 10}% - 1px)`}></span> + </div> + <div class="pts">adds {segmentPoints(i, sub.s[i]).toFixed(1)} of {segmentMax(i).toFixed(0)} points</div> + </div> + ))} + {!sub && SEGMENTS.map((s, i) => { const seg = d?.seg[s.detail]; return ( @@ -72,4 +105,26 @@ const circ = 2 * Math.PI * r; })} + {group && ( + <section class="c-units"> + <h2 class="d-h">{sub ? `Other ${group.plural}` : `By ${group.label.toLowerCase()}`} <span>{group.units.length} scored</span></h2> + {!sub && <p class="d-hint">{group.intro}</p>} + <ol class="units"> + {group.units.map((x) => ( + <li class={sub?.id === x.id ? "current" : undefined}> + <a href={unitUrl(x)} data-subunit> + <span class="u-rank">{x.rank}</span> + <span class="u-name">{x.name}</span> + <span class="u-score">{x.score}</span> + <span class="bar"><i style={`width:${x.raw}%;background:${tierColor(x.raw)}`}></i></span> + </a> + </li> + ))} + </ol> + <ul class="srcs"> + {group.sources.map(([label, url]) => <li><a href={url} target="_blank" rel="noopener noreferrer">{label}</a></li>)} + </ul> + </section> + )} + {d && ( <>
src/data/subunits.js +122 −0
@@ -0,0 +1,122 @@ +/* Cannabis Freedom Index — scores below national level + * + * For federations where the rules differ sharply between states, provinces or territories. + * Each unit is scored on the same six segments and levels as countries (see SEGMENTS in data.js): + * [possession, cultivation, enforcement, sharing, consumption, access] + * + * The home-page ranking and map still use the single national estimate in data.js. + * Units have a short note rather than a full segment-by-segment write-up. + * Scores are editorial estimates, not legal advice. + */ + +const u = (id, name, s, rec, med, note) => ({ id, name, s, rec, med, note }); + +export const SUBUNITS = { + US: { + label: "State", + plural: "states", + intro: "Federal law still prohibits non-medical cannabis everywhere in the United States; these scores describe state law. Counties and cities can add their own rules, and state law does not apply on federal land.", + sources: [ + ["NORML — State-by-state laws", "https://norml.org/laws/"], + ["National Conference of State Legislatures — State cannabis laws", "https://www.ncsl.org/health/state-medical-cannabis-laws"], + ], + units: [ + u("al", "Alabama", [1, 0, 1, 0, 1, 0], "illegal", "limited", "Possession for personal use is a misdemeanour with up to a year in jail; a second offence is a felony. A medical law passed in 2021 but products have been held up by licensing disputes."), + u("ak", "Alaska", [5, 4, 6, 5, 4, 6], "legal", "yes", "Adults 21+ may hold 1 oz and grow six plants (three mature). Licensed shops since 2016, and some may offer on-site consumption."), + u("az", "Arizona", [5, 4, 6, 5, 3, 6], "legal", "yes", "1 oz (5 g of concentrate) and six plants per adult, twelve per household. Licensed shops since 2021."), + u("ar", "Arkansas", [1, 1, 2, 1, 1, 3], "illegal", "yes", "Under 4 oz is a misdemeanour with up to a year in jail. Medical cannabis is sold in dispensaries to card holders."), + u("ca", "California", [5, 4, 6, 5, 4, 6], "legal", "yes", "28.5 g of flower (8 g of concentrate) and six plants per household. Shops, delivery and licensed consumption lounges; many cities still ban sales."), + u("co", "Colorado", [5, 4, 6, 5, 4, 6], "legal", "yes", "2 oz and six plants (three flowering). The first state with licensed shops, in 2014; hospitality venues are licensed in some cities."), + u("ct", "Connecticut", [6, 4, 6, 5, 3, 6], "legal", "yes", "1.5 oz on you and 5 oz locked at home; six plants (three mature), twelve per household. Licensed shops since 2023."), + u("de", "Delaware", [5, 1, 6, 5, 3, 6], "legal", "yes", "1 oz is legal and shops opened in 2025, but growing at home remains a crime."), + u("dc", "District of Columbia", [5, 4, 6, 5, 3, 4], "legal", "yes", "2 oz, six plants (three mature) and gifts of up to 1 oz are legal. Congress blocks licensed adult-use sales, so access runs through self-certified medical dispensaries."), + u("fl", "Florida", [1, 1, 2, 1, 1, 3], "illegal", "yes", "20 g or less is a misdemeanour with up to a year in jail. A large medical programme operates; a 2024 legalisation measure won 56% but needed 60%."), + u("ga", "Georgia", [1, 1, 2, 1, 1, 1], "illegal", "limited", "1 oz or less is a misdemeanour with up to a year in jail, though Atlanta and several cities have cut it to a small fine. Only low-THC oil is allowed medically."), + u("hi", "Hawaii", [2, 1, 3, 1, 1, 3], "decrim", "yes", "Up to 3 g is a $130 violation; more is a misdemeanour. Medical cannabis is dispensed to card holders, who may also grow."), + u("id", "Idaho", [1, 0, 1, 0, 1, 0], "illegal", "no", "No medical programme and no decriminalisation: under 3 oz is a misdemeanour with up to a year in jail and a mandatory minimum fine."), + u("il", "Illinois", [5, 2, 6, 5, 3, 6], "legal", "yes", "Residents may hold 30 g. Only medical patients may grow (five plants); for others a few plants is a $200 civil fine. Licensed shops since 2020."), + u("in", "Indiana", [1, 0, 2, 0, 1, 0], "illegal", "no", "Possession is a misdemeanour with up to 180 days in jail. No medical programme."), + u("ia", "Iowa", [1, 0, 2, 0, 1, 1], "illegal", "limited", "A first offence carries up to six months in jail. A narrow medical programme caps THC per patient."), + u("ks", "Kansas", [1, 0, 1, 0, 1, 0], "illegal", "no", "No medical programme. A first possession offence is a misdemeanour with up to six months in jail; later offences can be felonies."), + u("ky", "Kentucky", [1, 1, 2, 1, 1, 2], "illegal", "yes", "Up to 8 oz is a misdemeanour capped at 45 days in jail. A medical programme began in 2025; smoking flower is not allowed under it."), + u("la", "Louisiana", [3, 1, 4, 1, 2, 3], "decrim", "yes", "Up to 14 g is a $100 fine with no jail. Medical cannabis, including flower, is sold through licensed pharmacies."), + u("me", "Maine", [5, 4, 6, 5, 3, 6], "legal", "yes", "2.5 oz and three mature plants per adult. Licensed shops since 2020, alongside a long-standing medical caregiver market."), + u("md", "Maryland", [5, 3, 6, 5, 3, 6], "legal", "yes", "1.5 oz and two plants per household. Licensed shops since July 2023."), + u("ma", "Massachusetts", [6, 4, 6, 5, 3, 6], "legal", "yes", "1 oz in public and 10 oz at home; six plants per adult, twelve per household. Licensed shops since 2018."), + u("mi", "Michigan", [6, 5, 6, 5, 4, 6], "legal", "yes", "2.5 oz on you, 10 oz at home and twelve plants per household — the most generous home-grow limit in the country. Shops and licensed lounges."), + u("mn", "Minnesota", [6, 4, 6, 5, 3, 6], "legal", "yes", "2 oz in public and 2 lb at home; eight plants (four mature). Legal since 2023, with licensed shops opening from 2025."), + u("ms", "Mississippi", [3, 1, 4, 1, 2, 3], "decrim", "yes", "A first offence with up to 30 g is a $100–250 fine with no jail. Medical dispensaries serve card holders."), + u("mo", "Missouri", [6, 3, 6, 5, 3, 6], "legal", "yes", "3 oz is legal. Growing six flowering plants requires a state registration card. Licensed shops since 2023."), + u("mt", "Montana", [5, 3, 6, 5, 3, 6], "legal", "yes", "1 oz and two mature plants per adult, four per household. Shops operate only in counties that voted for legalisation."), + u("ne", "Nebraska", [3, 1, 4, 1, 2, 1], "decrim", "limited", "A first offence with up to 1 oz is a $300 infraction. Voters approved medical cannabis in 2024; the programme is still being set up."), + u("nv", "Nevada", [5, 2, 6, 5, 4, 6], "legal", "yes", "2.5 oz is legal. Home growing is allowed only if you live more than 25 miles from a shop. Licensed consumption lounges operate in Las Vegas."), + u("nh", "New Hampshire", [3, 1, 4, 1, 2, 3], "decrim", "yes", "Up to ¾ oz is a $100 violation. The only New England state without legal sales; a therapeutic programme serves patients."), + u("nj", "New Jersey", [6, 1, 6, 5, 3, 6], "legal", "yes", "Up to 6 oz is legal and shops have operated since 2022, but growing even one plant at home remains a serious crime."), + u("nm", "New Mexico", [6, 4, 6, 5, 4, 6], "legal", "yes", "2 oz in public and no limit at home; six mature plants per adult, twelve per household. Shops and licensed consumption areas."), + u("ny", "New York", [6, 4, 6, 5, 6, 6], "legal", "yes", "3 oz on you and 5 lb at home; six plants per adult. Uniquely, smoking cannabis is allowed almost anywhere tobacco smoking is."), + u("nc", "North Carolina", [2, 1, 3, 1, 1, 0], "decrim", "no", "Up to ½ oz is a misdemeanour punished by a fine of up to $200 with no jail. No medical programme; the only legal shop is on Cherokee tribal land."), + u("nd", "North Dakota", [2, 1, 3, 1, 1, 3], "decrim", "yes", "Up to ½ oz is an infraction with a fine. Medical cannabis is available; voters rejected legalisation in 2018, 2022 and 2024."), + u("oh", "Ohio", [5, 4, 6, 5, 3, 6], "legal", "yes", "2.5 oz and six plants per adult, twelve per household. Licensed shops since August 2024; lawmakers have since tightened rules on public use."), + u("ok", "Oklahoma", [1, 1, 2, 1, 1, 3], "illegal", "yes", "Without a card, possession is a misdemeanour. But the medical programme has no list of qualifying conditions and is among the easiest to join; patients may grow six mature plants."), + u("or", "Oregon", [6, 4, 6, 5, 3, 6], "legal", "yes", "2 oz in public and 8 oz at home; four plants per household. Licensed shops since 2015."), + u("pa", "Pennsylvania", [1, 1, 2, 1, 1, 3], "illegal", "yes", "30 g or less is a misdemeanour with up to 30 days in jail, cut to a small fine in Philadelphia, Pittsburgh and other cities. A large medical programme operates."), + u("ri", "Rhode Island", [6, 4, 6, 5, 3, 6], "legal", "yes", "1 oz in public and 10 oz at home; three mature plants. Licensed shops since December 2022."), + u("sc", "South Carolina", [1, 0, 2, 0, 1, 0], "illegal", "no", "1 oz or less is a misdemeanour with up to 30 days in jail. No medical programme."), + u("sd", "South Dakota", [1, 1, 2, 1, 0, 3], "illegal", "yes", "Up to 2 oz is a misdemeanour with up to a year in jail, and ingesting cannabis is itself an offence. Medical cannabis is available to card holders."), + u("tn", "Tennessee", [1, 0, 1, 0, 1, 0], "illegal", "no", "½ oz or less is a misdemeanour with up to a year in jail. Only low-THC oil is permitted for a few conditions."), + u("tx", "Texas", [1, 0, 2, 0, 1, 1], "illegal", "limited", "2 oz or less is a misdemeanour with up to 180 days in jail; several cities no longer enforce it. A restricted low-THC medical programme was widened in 2025."), + u("ut", "Utah", [1, 1, 2, 1, 1, 3], "illegal", "yes", "Under 1 oz is a misdemeanour with up to six months in jail. Medical cannabis is sold through licensed pharmacies; smoking it is not allowed."), + u("vt", "Vermont", [5, 4, 6, 5, 3, 6], "legal", "yes", "1 oz and two mature plus four immature plants per household. The first state to legalise through its legislature; shops since 2022."), + u("va", "Virginia", [5, 4, 6, 5, 3, 3], "legal", "yes", "1 oz and four plants per household have been legal since 2021, but adult-use shops were repeatedly vetoed. Medical dispensaries are the only legal place to buy."), + u("wa", "Washington", [5, 1, 6, 4, 3, 6], "legal", "yes", "1 oz is legal and shops have operated since 2014, but only medical patients may grow; for everyone else it is a felony."), + u("wv", "West Virginia", [1, 1, 2, 1, 1, 3], "illegal", "yes", "Possession carries 90 days to six months in jail, with conditional discharge for a first offence. Medical dispensaries serve card holders; no flower smoking."), + u("wi", "Wisconsin", [1, 0, 2, 0, 1, 0], "illegal", "no", "A first offence is a misdemeanour with up to six months in jail; a second is a felony. No medical programme, though Madison and Milwaukee impose only small fines."), + u("wy", "Wyoming", [1, 0, 1, 0, 0, 0], "illegal", "no", "Up to 3 oz is a misdemeanour with up to a year in jail, and being under the influence is a separate offence. No medical programme."), + ], + }, + + CA: { + label: "Province or territory", + plural: "provinces and territories", + intro: "The federal Cannabis Act applies everywhere in Canada. Provinces and territories set the minimum age, where you may consume, how shops work, and whether you may grow at home.", + sources: [ + ["Department of Justice Canada — Cannabis legalization and regulation", "https://www.justice.gc.ca/eng/cj-jp/cannabis/"], + ["Health Canada — Cannabis laws and regulations", "https://www.canada.ca/en/health-canada/services/drugs-medication/cannabis/laws-regulations.html"], + ], + units: [ + u("ab", "Alberta", [6, 4, 6, 5, 6, 6], "legal", "yes", "Minimum age 18, the lowest in Canada. Private shops; consumption allowed where tobacco is, subject to city by-laws."), + u("bc", "British Columbia", [6, 4, 6, 5, 6, 6], "legal", "yes", "Age 19. Public and private shops; smoking generally allowed where tobacco is, except around children."), + u("mb", "Manitoba", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Private shops. Home growing was banned until May 2025; public consumption is prohibited."), + u("nb", "New Brunswick", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Government-run Cannabis NB plus some private shops; consumption only in private."), + u("nl", "Newfoundland and Labrador", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Private licensed shops; consumption only in private residences."), + u("ns", "Nova Scotia", [6, 4, 6, 5, 5, 6], "legal", "yes", "Age 19. Sold through the provincial liquor corporation; smoking follows tobacco rules."), + u("nt", "Northwest Territories", [6, 4, 6, 5, 4, 6], "legal", "yes", "Age 19. Government-run sales; communities may restrict or ban cannabis locally."), + u("nu", "Nunavut", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Sales mostly online through the territorial agency, with few physical shops."), + u("on", "Ontario", [6, 4, 6, 5, 6, 6], "legal", "yes", "Age 19. The largest private retail market in the country; smoking allowed almost anywhere tobacco is."), + u("pe", "Prince Edward Island", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Government-run shops only; consumption limited to private residences."), + u("qc", "Quebec", [5, 1, 6, 5, 3, 5], "legal", "yes", "The strictest province: age 21, no home growing, a 150 g cap at home, no public consumption, and sales only through the state-run SQDC with a narrower product range."), + u("sk", "Saskatchewan", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Private shops; consumption in public places is prohibited."), + u("yt", "Yukon", [6, 4, 6, 5, 3, 6], "legal", "yes", "Age 19. Private shops supplied by the territorial corporation; consumption in private only."), + ], + }, + + AU: { + label: "State or territory", + plural: "states and territories", + intro: "Each Australian state and territory has its own drug law. Medical cannabis is regulated federally and is available on prescription everywhere, mostly through telehealth clinics.", + sources: [ + ["Wikipedia — Cannabis in Australia", "https://en.wikipedia.org/wiki/Cannabis_in_Australia"], + ["The Conversation — ACT cannabis laws come into effect", "https://theconversation.com/act-cannabis-laws-come-into-effect-on-friday-but-they-may-not-be-what-you-hoped-for-130050"], + ], + units: [ + u("act", "Australian Capital Territory", [5, 3, 6, 1, 3, 3], "legal", "yes", "Since 2020 adults may hold 50 g of dried cannabis and grow two plants each, four per household. Selling, gifting and even buying seeds remain illegal."), + u("nsw", "New South Wales", [2, 1, 3, 1, 1, 3], "illegal", "yes", "Police may issue a caution for up to 15 g, at most twice; otherwise possession is an offence with up to two years."), + u("nt", "Northern Territory", [3, 2, 4, 1, 2, 3], "decrim", "yes", "Up to 50 g, or two plants at home, is dealt with by an infringement notice and a fine rather than a charge."), + u("qld", "Queensland", [2, 1, 4, 1, 1, 3], "illegal", "yes", "Police must offer a warning or a diversion programme for small amounts before charging, under a scheme expanded in 2024."), + u("sa", "South Australia", [3, 2, 4, 1, 2, 3], "decrim", "yes", "The first Australian state to decriminalise, in 1987: small amounts and one non-hydroponic plant are an expiation notice with a fine."), + u("tas", "Tasmania", [2, 1, 3, 1, 1, 3], "illegal", "yes", "Police may caution for up to 50 g, up to three times in ten years; otherwise it is a criminal offence."), + u("vic", "Victoria", [2, 1, 3, 1, 1, 3], "illegal", "yes", "A cannabis cautioning programme covers up to 50 g for people who admit the offence; otherwise possession is prosecuted."), + u("wa", "Western Australia", [2, 1, 3, 1, 1, 3], "illegal", "yes", "Up to 10 g leads to a one-off intervention session instead of a charge; beyond that it is a criminal offence."), + ], + }, +};
src/lib/countries.js +20 −0
@@ -24,2 +24,22 @@ export const regions = [...new Set(countries.map((c) => c.region))].sort(); export const segAvg = SEGMENTS.map((_, i) => countries.reduce((a, c) => a + c.s[i], 0) / countries.length); export const worldAvg = countries.reduce((a, c) => a + c.raw, 0) / countries.length; + +// ── States, provinces and territories of federations ── +import { SUBUNITS } from "../data/subunits.js"; + +const scoreOf = (s) => s.reduce((a, v, i) => a + segmentPoints(i, v), 0); + +// For a country id, returns { label, plural, intro, sources, units } with each unit scored, +// ranked within its country, and placed where it would sit among countries. Undefined if none. +export function subunitsOf(id) { + const group = SUBUNITS[id]; + if (!group) return undefined; + const units = group.units + .map((x) => { + const raw = scoreOf(x.s); + return { ...x, raw, score: Math.round(raw), worldRank: 1 + countries.filter((c) => c.raw > raw).length }; + }) + .sort((a, b) => b.raw - a.raw || a.name.localeCompare(b.name)); + units.forEach((x, i) => (x.rank = i + 1)); + return { ...group, units }; +}
src/pages/country/[id].astro +7 −0
@@ -31,3 +31,10 @@ const description = `${c.name} scores ${c.score} out of 100 on the Cannabis Free </nav> </main> + <script> + // On a full page the state/province picker simply navigates. + document.addEventListener("change", (e) => { + const select = e.target.closest("select[data-subunit-select]"); + if (select) location.href = select.value; + }); + </script> </Base>
src/pages/country/[id]/[sub].astro +32 −0
@@ -0,0 +1,32 @@ +--- +// One page per state, province or territory of a federation, e.g. /country/us/ca/. +import Base from "../../../layouts/Base.astro"; +import CountryArticle from "../../../components/CountryArticle.astro"; +import { countries, subunitsOf } from "../../../lib/countries.js"; +import { tierOf } from "../../../lib/tiers.js"; + +export function getStaticPaths() { + return countries.flatMap((c) => + (subunitsOf(c.id)?.units ?? []).map((sub) => ({ params: { id: c.slug, sub: sub.id }, props: { c, sub } })) + ); +} + +const { c, sub } = Astro.props; +const base = import.meta.env.BASE_URL; +const description = `${sub.name} (${c.name}) scores ${sub.score} out of 100 on the Cannabis Freedom Index (${tierOf(sub.raw).name}). ${sub.note}`; +--- +<Base title={`${sub.name}, ${c.name}`} description={description}> + <main class="country-page"> + <div class="page-bar"> + <a class="back" href={`${base}country/${c.slug}/`}>← {c.flag} {c.name}</a> + </div> + <CountryArticle c={c} sub={sub} /> + </main> + <script> + // On a full page the picker simply navigates. + document.addEventListener("change", (e) => { + const select = e.target.closest("select[data-subunit-select]"); + if (select) location.href = select.value; + }); + </script> +</Base>
src/pages/index.astro +27 −6
@@ -321,14 +321,35 @@ const mapData = countries.map(({ id, num, name, flag, raw, score, rank, s }) => const cache = new Map(); - async function loadArticle(id) { - if (!cache.has(id)) { - const html = await (await fetch(pageUrl(id))).text(); + // Articles are cached by page address, which also covers states and provinces. + async function loadArticle(url) { + if (!cache.has(url)) { + const html = await (await fetch(url)).text(); const article = new DOMParser().parseFromString(html, "text/html").querySelector("article.country"); // The full page shows every explanation; in the drawer they start collapsed. article.querySelectorAll("details[open]").forEach((el) => el.removeAttribute("open")); - cache.set(id, article); + cache.set(url, article); } - return cache.get(id).cloneNode(true); + return cache.get(url).cloneNode(true); } + // Swap the drawer to another view of the same country (a state, or back to national). + async function showInDrawer(url) { + try { + body.replaceChildren(await loadArticle(url)); + $("#dExpand").href = url; + dialog.scrollTop = 0; + } catch (err) { + location.href = url; + } + } + body.addEventListener("change", (e) => { + const select = e.target.closest("select[data-subunit-select]"); + if (select) showInDrawer(select.value); + }); + body.addEventListener("click", (e) => { + const a = e.target.closest("a[data-subunit]"); + if (!a || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; + e.preventDefault(); + showInDrawer(a.getAttribute("href")); + }); function markActive() { @@ -352,5 +373,5 @@ const mapData = countries.map(({ id, num, name, flag, raw, score, rank, s }) => $("#dCompare").href = `${base}compare/?c=${id.toLowerCase()}`; try { - const article = await loadArticle(id); + const article = await loadArticle(pageUrl(id)); if (currentId() !== id) return; body.replaceChildren(article);
src/styles/global.css +16 −0
@@ -411,2 +411,18 @@ td.num, th.num { width: 52px; color: var(--muted); } .lore-src a:hover { color: var(--text); } @media (max-width: 860px) { .lore-grid { grid-template-columns: 1fr; } } + +/* ── States, provinces and territories ──────────────── */ +.sub-select { margin-top: 14px; flex-wrap: wrap; } +.sub-select select { flex: 1; min-width: 200px; max-width: 100%; } +.d-hint a { color: var(--text-2); text-decoration: underline; text-underline-offset: 3px; } +.segrow-top .vs { color: var(--muted); font-weight: 400; margin-left: 8px; font-size: 12.5px; } +.units { list-style: none; margin: 8px 0 12px; padding: 0; columns: 2; column-gap: 28px; } +.drawer-inner .units { columns: 1; } +.units li { break-inside: avoid; } +.units a { display: grid; grid-template-columns: 26px minmax(0, 1fr) 30px 70px; gap: 8px; align-items: center; padding: 5px 8px; margin: 0 -8px; border-radius: 8px; font-size: 14px; } +.units a:hover { background: var(--surface-2); } +.units .current a { background: var(--surface-2); font-weight: 600; } +.u-rank { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; } +.u-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.u-score { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; } +@media (max-width: 640px) { .units { columns: 1; } }