Built in Kingston
${e(site.operator.statement)}
About ${e(site.operator.name)}diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f341b..ed2ca53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Notable user-visible changes to Food Help are recorded here. Community publicati ### Changed +- Directory cards are easier to scan: schedule status is a coloured badge, today’s hours show one time range per line with shorter twelve-hour times (for example “9–11 a.m.”), field labels are larger, the current view’s tag and a repeated operator name are no longer shown, and the directory introduction is shorter. Resource pages show Call and Directions before the detailed schedule and sources, phone numbers in North American format, and addresses without the country code. The project homepage links straight to the directory from its introduction when one community is available. - Configured community directories can link discreetly back to the Food Help project without changing their resource URLs or provider data. - Category controls now show only primary types represented in the active browsing view, and omit “All types” when only one type is available. - Published resources require reviewed, evidence-backed cost information. Free services appear in Emergency food, low-cost and subsidized services appear in Affordable food, and mixed-cost services appear in both without duplicate records. diff --git a/deployments/kingston/site.json b/deployments/kingston/site.json index 0337e17..365dacc 100644 --- a/deployments/kingston/site.json +++ b/deployments/kingston/site.json @@ -39,7 +39,7 @@ "interval_days": 30 }, "presentation": { - "introduction": "Find Emergency food or Affordable food in Kingston. Use meal, grocery and market filters to see what is offered, and check details before travelling.", + "introduction": "Free and low-cost meals and groceries in Kingston, and when to go.", "category_groups": [ { "id": "meals", "label": "Meals", "categories": ["prepared_meals"] }, { "id": "groceries", "label": "Groceries", "categories": ["groceries", "food_banks_pantries"] }, diff --git a/project-site/styles.css b/project-site/styles.css index 98dd16f..02e78b6 100644 --- a/project-site/styles.css +++ b/project-site/styles.css @@ -34,6 +34,8 @@ h1,h2,h3{color:var(--navy);line-height:1.15} .eyebrow{margin:0 0 .8rem;color:var(--brick);font-size:.875rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase} h1{max-width:14ch;margin:0;font-size:clamp(2.7rem,6.3vw,5.6rem);font-weight:650;letter-spacing:-.04em} .hero-lead{max-width:40rem;margin:1.4rem 0 0;color:var(--muted);font-size:clamp(1.08rem,1.8vw,1.3rem);line-height:1.55} +.hero-actions{margin:1.6rem 0 0} +.hero-actions .button{min-height:52px;padding-inline:1.4rem;font-size:1.05rem} .hero-note{border-left:4px solid var(--brick);padding:.25rem 0 .25rem 1.25rem;color:var(--muted)} .hero-note strong{display:block;color:var(--navy);font-size:1.15rem} .hero-note p{margin:.35rem 0 0} @@ -78,7 +80,7 @@ h1{max-width:14ch;margin:0;font-size:clamp(2.7rem,6.3vw,5.6rem);font-weight:650; .header-inner{align-items:flex-start;flex-direction:column;gap:.25rem;padding-block:.75rem} .site-nav{justify-content:flex-start;gap:.1rem 1rem} .hero,.section-heading,.split,.contact-panel,.footer-inner{grid-template-columns:1fr} - .hero{gap:2rem;padding-top:3.5rem} + .hero{gap:2rem;padding-top:2rem} .community-card{grid-template-columns:1fr} .principles{grid-template-columns:1fr} .footer-links{justify-content:flex-start} diff --git a/scripts/build-project.ts b/scripts/build-project.ts index addd291..70f5a99 100644 --- a/scripts/build-project.ts +++ b/scripts/build-project.ts @@ -61,7 +61,7 @@ function home(site: ProjectSite, communities: CommunityList, production: boolean { '@type': 'ItemList', name: 'Food Help community directories', itemListElement: communities.communities.map((community, index) => ({ '@type': 'ListItem', position: index + 1, name: `${community.name} Food Help`, url: `${community.canonical_url}/` })) } ] }; return `
${head(site, '/', site.site_name, site.description, index, css, logo, jsonld)}${header(site, logo)}Emergency and affordable food directories
${e(site.introduction)}
Emergency and affordable food directories
${e(site.introduction)}
${communities.communities.length === 1 ? `Find food in ${e(communities.communities[0]!.name)}` : 'Choose a community'}
Community directories are the place to find food programs. This project homepage does not duplicate their listings.
${e(site.maintenance)}
Published program pages and trusted public directories provide an evidence trail.
Provider details can clarify what is offered and what people should know before travelling.
Community feedback is reviewed before it changes a public listing.
${e(site.operator.statement)}
About ${e(site.operator.name)}The software and setup guidance are public. Community operators keep control of their own reviewed information and publication.
${c.scheduleKinds[s.kind]}${s.confirmed ? '' : ` — ${c.unconfirmed}`}. ${escape(s.note)}
${c.timezone}: ${escape(resource.time_zone ?? site.time_zone)}
${s.valid_from || s.valid_to ? `${c.validity}: ${s.valid_from ? formatDate(s.valid_from, site) : c.unknown} – ${s.valid_to ? formatDate(s.valid_to, site) : c.unknown}
` : ''}${s.weekly.length ? `${c.scheduleKinds[s.kind]}${s.confirmed ? '' : ` — ${c.unconfirmed}`}. ${escape(s.note)}
${c.timezone}: ${escape(resource.time_zone ?? site.time_zone)}
${s.valid_from || s.valid_to ? `${c.validity}: ${s.valid_from ? formatDate(s.valid_from, site) : c.unknown} – ${s.valid_to ? formatDate(s.valid_to, site) : c.unknown}
` : ''}${s.weekly.length ? `${escape(address(r))}
${r.phone ? `${c.phone}: ${escape(r.phone)}${r.phone_extension ? ` · ext ${escape(r.phone_extension)}` : ''}
` : ''}`)}${field(c.access, `${escape(address(r))}
${r.phone ? `${c.phone}: ${escape(formatPhone(r.phone))}${r.phone_extension ? ` · ext ${escape(r.phone_extension)}` : ''}
` : ''}`)}${field(c.access, `${c.checked}: . ${escape(item.note)}
${full ? `${c.supports}: ${escape(item.supports.map(evidenceField).join(', '))}
` : ''}${c.methods[r.verification.method]}. ${c.verification[r.verification.state]}.
`; const detail = `${facts}${r.food_access_purpose !== r.summary ? field(c.foodPurpose, `${escape(r.food_access_purpose)}
`) : ''}${r.service_languages?.length ? field(c.languages, list(r.service_languages)) : ''}${r.notices?.length ? field(c.notices, list(r.notices.map(item => `${item.text} (${formatDate(item.starts_on, site)}${item.ends_on ? ` – ${formatDate(item.ends_on, site)}` : ''})`))) : ''}${field(c.sources, evidence)}`; const status = scheduleSummary(r, site, now), level = `h${headingLevel}`; @@ -55,12 +69,12 @@ export function card(resource: Resource, data: PublicDataset, site: Site, full = const link = (url: string, text: string, style: string, event: string) => `${escape(text)}`; const scheduleAction = scheduleEvidence?.url ? link(scheduleEvidence.url, c.scheduleSource, 'button-quiet', 'source') : ''; const officialAction = r.official_url && r.official_url !== scheduleEvidence?.url ? link(r.official_url, c.source, 'button-quiet', 'source') : ''; + const actions = `${c.draftBadge}
` : ''}${c.provider} ${escape(org.name)}
${c.draftBadge}
` : ''}${c.provider} ${escape(org.name)}
`}${c.conditions[r.service_condition]}
` : ''}${notices.map(notice => `${escape(notice.text)}
`).join('')} -${escape(r.summary)}
${escape(r.summary)}
${full ? actions : ''}${c.sourcesChecked} ${full && r.updated_on !== r.verification.reviewed_on ? ` · ${c.updated} ${formatDate(r.updated_on, site)}` : ''}
${now && reviewOverdue(r, site, now) ? c.overdue : ''}
`; } diff --git a/src/styles.css b/src/styles.css index a48ef52..c172b11 100644 --- a/src/styles.css +++ b/src/styles.css @@ -280,7 +280,7 @@ h1 em { } .browse-view-description { max-width:70ch; - margin:.8rem 0 1.2rem; + margin:.7rem 0 .9rem; color:var(--muted); } .draft-review-banner { @@ -345,7 +345,7 @@ h1 em { display:flex; align-items:center; gap:.65rem; - font-size:.88rem; + font-size:.95rem; min-height:46px; } .check-control input { @@ -367,7 +367,7 @@ h1 em { border-radius:3px; text-decoration:none; font-weight:700; - font-size:.88rem; + font-size:.95rem; line-height:1.3; } .button-primary { @@ -399,7 +399,7 @@ h1 em { margin:.6rem 0 0; } .estimate-note { - font-size:.85rem; + font-size:.9rem; line-height:1.5; color:var(--muted); padding:0 0 1rem; @@ -442,13 +442,13 @@ h1 em { padding:1.5rem; background:var(--white); border:1px solid var(--line); - border-radius:4px; - + border-radius:10px; + box-shadow:0 1px 3px #173f4c0f; } .card-header { display:flex; flex-direction:column; - gap:.9rem; + gap:.75rem; } .tags { display:flex; @@ -459,16 +459,20 @@ h1 em { font-size:.85rem; letter-spacing:0; text-transform:none; - font-weight:800; + font-weight:700; color:var(--teal); + padding:.15rem .6rem; + border-radius:999px; + background:var(--pale); } .browse-view-tag { - padding:.1rem .4rem; - border:1px solid #9aaba5; - border-radius:3px; - background:var(--pale); color:var(--navy); } +/* The selected browse view already names this listing's view; show only the other one. */ +.directory-shell[data-browse-view=emergency] .browse-view-tag[data-view=emergency], +.directory-shell[data-browse-view=affordable] .browse-view-tag[data-view=affordable] { + display:none; +} .draft-badge { margin:0 0 1rem; padding:.5rem .65rem; @@ -486,39 +490,44 @@ h1 em { margin:.45rem 0 .35rem; } .operator { - font-size:.875rem; + font-size:.95rem; color:var(--muted); margin:0; } .availability { display:flex; - align-items:baseline; + align-items:center; flex-wrap:wrap; - gap:.35rem .7rem; + gap:.4rem .7rem; font-size:.95rem; - border-left:3px solid #a3ada6; - padding-left:.6rem; } .availability strong { + display:inline-flex; + align-items:center; + gap:.45rem; + padding:.3rem .75rem; + border:1px solid transparent; + border-radius:999px; + background:#ebebe6; + color:#3f4a4c; font-size:.95rem; font-weight:750; } +.availability strong::before { + content:""; + flex-shrink:0; + width:.55rem; + height:.55rem; + border-radius:50%; + background:currentColor; +} .availability span { color:var(--muted); - font-size:.875rem; -} -.availability-available_now,.availability-opening_soon { - border-color:var(--teal); -} -.availability-now,.availability-later{border-color:var(--teal)} -.availability-now strong{color:#225b3c} -.availability-unknown,.availability-unavailable{border-color:var(--brick)} -.availability-available_now strong { - color:#225b3c; -} -.availability-check_hours,.availability-temporarily_unavailable { - border-color:var(--brick); + font-size:.95rem; } +.availability-now strong{background:#dcefe2;color:#1d5a36} +.availability-later strong{background:#fbecd2;color:#73460b} +.availability-unknown strong,.availability-unavailable strong{background:var(--caution);color:#83401f} .summary { font-size:1rem; line-height:1.5; @@ -532,17 +541,20 @@ h1 em { } .fact { display:grid; - grid-template-columns:3.5rem minmax(0,1fr); + grid-template-columns:5.5rem minmax(0,1fr); gap:.7rem; padding:.35rem 0; } .fact dt { text-transform:none; - font-size:.8rem; - font-weight:650; + font-size:.9rem; + font-weight:700; letter-spacing:0; - color:var(--brick); - padding-top:.18rem; + color:var(--muted); + padding-top:.1rem; +} +.today-hours { + white-space:pre-line; } .fact dd { font-size:1rem; @@ -608,6 +620,13 @@ h1 em { text-align:left; justify-content:start; } +.card-actions .button-primary,.card-actions .button-secondary { + font-size:1rem; + min-height:50px; +} +.content-page .resource-card>.card-actions { + margin:0 0 1rem; +} .card-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); @@ -619,7 +638,7 @@ h1 em { grid-column:1/-1; min-height:40px; justify-self:start; - font-size:.9rem; + font-size:.95rem; padding-top:.35rem; padding-bottom:.35rem; } @@ -796,14 +815,14 @@ h1 em { } .hero { display:block; - padding:1.3rem 0 1.4rem; + padding:1.2rem 0 .6rem; } .hero h1 { font-size:2.05rem; } .hero-lead { - font-size:.95rem; - margin-top:.8rem; + font-size:1.05rem; + margin-top:.6rem; } .help-panel { display:none; @@ -849,8 +868,8 @@ h1 em { } .estimate-note { font-size:.9rem; - margin-top:.8rem; - padding-bottom:.75rem; + margin-top:.6rem; + padding-bottom:.6rem; } .result-status { font-size:.875rem; @@ -913,6 +932,9 @@ h1 em { .offline-status span::before { display:none; } + .availability strong { + border-color:CanvasText; + } } @media print { body { diff --git a/tests/unit/presentation.test.ts b/tests/unit/presentation.test.ts index bcfb2b6..f22f4bb 100644 --- a/tests/unit/presentation.test.ts +++ b/tests/unit/presentation.test.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'; import siteJSON from '../../examples/exampleville/site.json' with { type: 'json' }; import sourceJSON from '../../examples/exampleville/resources.json' with { type: 'json' }; import { categoryFilter, categoryGroups, publishedSchedule, resourceInCategoryGroup, scheduledToday } from '../../src/domain.ts'; -import { card, scheduleSummary } from '../../src/presentation.ts'; +import { card, formatPhone, formatTimeRange, scheduleSummary } from '../../src/presentation.ts'; import { assertSite } from '../../src/validation.ts'; import type { Site, Dataset, PublicDataset } from '../../src/types.ts'; const site = siteJSON as Site, source = sourceJSON as unknown as Dataset; @@ -47,6 +47,14 @@ describe('directory presentation and schedule decisions', () => { expect(html).toContain('