Small
This commit is contained in:
parent
bce34b73de
commit
b47c9ba1ec
13 changed files with 60 additions and 38 deletions
|
|
@ -476,7 +476,6 @@ export default function HomePage({
|
|||
<div className="home-hero-container relative z-10 mx-auto flex w-full max-w-[104rem] flex-1 flex-col px-6 pb-8 pt-6 backdrop-blur-[2px] md:px-10 md:py-10">
|
||||
<div className="home-hero-layout hero-roomy-lift grid flex-1 items-center gap-x-8 gap-y-6">
|
||||
<div className="home-hero-copy min-w-0 max-w-4xl">
|
||||
<p className="text-sm font-semibold text-teal-300 mb-3">{t('home.heroEyebrow')}</p>
|
||||
<h1 className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-[1.1]">
|
||||
{t('home.heroTitle1')}{' '}
|
||||
<span className="text-teal-400">{t('home.heroTitle2')}</span>.
|
||||
|
|
@ -511,8 +510,7 @@ export default function HomePage({
|
|||
</div>
|
||||
<p className="text-sm font-medium text-teal-200 mb-4">{t('home.freeToExplore')}</p>
|
||||
<PriceStrip onOpenPricing={onOpenPricing} hidePricing={hidePricing} />
|
||||
<p className="text-sm text-warm-400 mb-6">{t('home.coverageNote')}</p>
|
||||
<div className="home-hero-stats flex flex-wrap pt-3 border-t border-white/10">
|
||||
<div className="home-hero-stats flex flex-wrap mt-6 pt-3 border-t border-white/10">
|
||||
<div className="home-hero-stat">
|
||||
<div className="home-hero-stat-value tabular-nums">
|
||||
<TickerValue text="13M" active={statsActive} />
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function EnumFeatureFilterCard({
|
|||
onRemove={onRemoveFilter}
|
||||
/>
|
||||
</div>
|
||||
<PillGroup>
|
||||
<PillGroup bleed>
|
||||
{allValues.map((val) => (
|
||||
<PillToggle
|
||||
key={val}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ function EditableLabel({
|
|||
if (e.key === 'Escape') setEditing(false);
|
||||
}}
|
||||
onBlur={commit}
|
||||
className="absolute w-16 text-[10px] text-center rounded border border-warm-300 dark:border-warm-600 bg-white dark:bg-warm-800 text-warm-700 dark:text-warm-200 px-0.5 focus:outline-none focus:ring-1 focus:ring-teal-400"
|
||||
// w-16 fits ~7 digits at 10px, but touch devices force this to 16px (see
|
||||
// index.css) to stop iOS zooming on focus, so widen to match there.
|
||||
className="absolute w-16 pointer-coarse:w-24 text-[10px] text-center rounded border border-warm-300 dark:border-warm-600 bg-white dark:bg-warm-800 text-warm-700 dark:text-warm-200 px-0.5 focus:outline-none focus:ring-1 focus:ring-teal-400"
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,16 +4,30 @@ interface PillGroupProps {
|
|||
children: ReactNode;
|
||||
className?: string;
|
||||
wrap?: boolean;
|
||||
/**
|
||||
* Widen the scroll area by the 16px of filter-card padding on each side, and re-add it as
|
||||
* inner padding. Pills rest where they did, but scroll to the sidebar edge before clipping.
|
||||
*/
|
||||
bleed?: boolean;
|
||||
}
|
||||
|
||||
export function PillGroup({ children, className = '', wrap = false }: PillGroupProps) {
|
||||
const SCROLL =
|
||||
'flex-nowrap overflow-x-auto overscroll-x-contain touch-pan-x touch-pan-y scrollbar-hide md:flex-wrap md:overflow-x-visible';
|
||||
|
||||
export function PillGroup({
|
||||
children,
|
||||
className = '',
|
||||
wrap = false,
|
||||
bleed = false,
|
||||
}: PillGroupProps) {
|
||||
// max-w-full would clamp a bleeding group back to the padded width, undoing the negative margin.
|
||||
const scroll = bleed
|
||||
? `${SCROLL} -mx-4 px-4 md:mx-0 md:max-w-full md:px-0`
|
||||
: `${SCROLL} max-w-full`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex min-w-0 max-w-full gap-1.5 ${
|
||||
wrap
|
||||
? 'flex-wrap overflow-x-visible'
|
||||
: 'flex-nowrap overflow-x-auto overscroll-x-contain touch-pan-x touch-pan-y scrollbar-hide md:flex-wrap md:overflow-x-visible'
|
||||
} ${className}`}
|
||||
className={`flex min-w-0 gap-1.5 ${wrap ? 'max-w-full flex-wrap overflow-x-visible' : scroll} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1043,7 +1043,6 @@ const de: Translations = {
|
|||
|
||||
// ── Home Page ──────────────────────────────────────
|
||||
home: {
|
||||
heroEyebrow: 'Für Käufer, die nicht für den Ruf eines Postcodes zu viel zahlen wollen',
|
||||
heroTitle1: 'Finde den',
|
||||
heroTitle2: 'Geheimtipp-Postcode',
|
||||
heroTitle3: 'Gleiche Schulen, gleicher Pendelweg, und still günstiger.',
|
||||
|
|
@ -1112,8 +1111,6 @@ const de: Translations = {
|
|||
statFilters: 'kombinierbare Filter',
|
||||
statEvery: 'Jeder',
|
||||
statPostcodeInEngland: 'Postcode in England',
|
||||
coverageNote:
|
||||
'Deckt jeden Postcode in England ab, je 200+ Datenfelder. Schottland und Wales sind in Planung.',
|
||||
priceStrip:
|
||||
'Einmal zahlen für lebenslangen Zugang: aktuell {{price}}, steigend, sobald sich jede Stufe füllt.',
|
||||
priceStripSpots: 'Noch {{count}} Platz zu diesem Preis.',
|
||||
|
|
|
|||
|
|
@ -1030,7 +1030,6 @@ const en = {
|
|||
|
||||
// ── Home Page ──────────────────────────────────────
|
||||
home: {
|
||||
heroEyebrow: 'For buyers who refuse to overpay for a postcode’s reputation',
|
||||
heroTitle1: 'Find the',
|
||||
heroTitle2: 'hidden-gem postcode',
|
||||
heroTitle3: 'Same schools, same commute, quietly cheaper.',
|
||||
|
|
@ -1097,8 +1096,6 @@ const en = {
|
|||
statFilters: 'combinable filters',
|
||||
statEvery: 'Every',
|
||||
statPostcodeInEngland: 'postcode in England',
|
||||
coverageNote:
|
||||
'Covers every postcode in England, with 200+ data fields each. Scotland & Wales are on the roadmap.',
|
||||
priceStrip: 'Pay once for lifetime access, currently {{price}}, rising as each tier fills.',
|
||||
priceStripSpots: '{{count}} spot left at this price.',
|
||||
priceStripSpotsPlural: '{{count}} spots left at this price.',
|
||||
|
|
|
|||
|
|
@ -1053,7 +1053,6 @@ const fr: Translations = {
|
|||
|
||||
// ── Home Page ──────────────────────────────────────
|
||||
home: {
|
||||
heroEyebrow: 'Pour les acheteurs qui refusent de surpayer la réputation d’un code postal',
|
||||
heroTitle1: 'Trouvez le',
|
||||
heroTitle2: 'code postal pépite cachée',
|
||||
heroTitle3: 'Mêmes écoles, même trajet, discrètement moins cher.',
|
||||
|
|
@ -1122,8 +1121,6 @@ const fr: Translations = {
|
|||
statFilters: 'filtres combinables',
|
||||
statEvery: 'Chaque',
|
||||
statPostcodeInEngland: 'code postal d’Angleterre',
|
||||
coverageNote:
|
||||
'Couvre tous les codes postaux d’Angleterre, plus de 200 champs de données chacun. L’Écosse et le pays de Galles sont sur la feuille de route.',
|
||||
priceStrip:
|
||||
'Payez une fois pour un accès à vie : actuellement {{price}}, en hausse à mesure que chaque palier se remplit.',
|
||||
priceStripSpots: '{{count}} place restante à ce prix.',
|
||||
|
|
|
|||
|
|
@ -1003,8 +1003,6 @@ const hi: Translations = {
|
|||
},
|
||||
|
||||
home: {
|
||||
heroEyebrow:
|
||||
'उन खरीदारों के लिए जो किसी पोस्टकोड की प्रतिष्ठा के लिए ज़्यादा कीमत चुकाने से इनकार करते हैं',
|
||||
heroTitle1: 'खोजें वह',
|
||||
heroTitle2: 'छिपा हुआ रत्न पोस्टकोड',
|
||||
heroTitle3: 'वही स्कूल, वही आवागमन: चुपचाप सस्ता.',
|
||||
|
|
@ -1071,8 +1069,6 @@ const hi: Translations = {
|
|||
statFilters: 'जोड़े जा सकने वाले फ़िल्टर',
|
||||
statEvery: 'इंग्लैंड का हर',
|
||||
statPostcodeInEngland: 'पोस्टकोड',
|
||||
coverageNote:
|
||||
'इंग्लैंड का हर पोस्टकोड कवर: हर एक के लिए 200+ डेटा फ़ील्ड. स्कॉटलैंड और वेल्स रोडमैप पर हैं.',
|
||||
priceStrip:
|
||||
'आजीवन पहुँच के लिए एक बार भुगतान करें: अभी {{price}}, हर टियर भरने पर कीमत बढ़ती है.',
|
||||
priceStripSpots: 'इस कीमत पर {{count}} जगह बची है.',
|
||||
|
|
|
|||
|
|
@ -1040,7 +1040,6 @@ const hu: Translations = {
|
|||
|
||||
// ── Home Page ──────────────────────────────────────
|
||||
home: {
|
||||
heroEyebrow: 'Vevőknek, akik nem hajlandók túlfizetni egy irányítószám hírnevéért',
|
||||
heroTitle1: 'Találd meg a',
|
||||
heroTitle2: 'rejtett kincs irányítószámot',
|
||||
heroTitle3: 'Ugyanazok az iskolák, ugyanaz az ingázás, csendben olcsóbban.',
|
||||
|
|
@ -1110,8 +1109,6 @@ const hu: Translations = {
|
|||
statFilters: 'kombinálható szűrő',
|
||||
statEvery: 'Minden',
|
||||
statPostcodeInEngland: 'irányítószám Angliában',
|
||||
coverageNote:
|
||||
'Anglia összes irányítószámát lefedi, egyenként 200+ adatmezővel. Skócia és Wales a terveink között szerepel.',
|
||||
priceStrip:
|
||||
'Fizess egyszer az örökös hozzáférésért: jelenleg {{price}}, és az ár a szintek betelésével emelkedik.',
|
||||
priceStripSpots: 'Már csak {{count}} hely ezen az áron.',
|
||||
|
|
|
|||
|
|
@ -976,7 +976,6 @@ const zh: Translations = {
|
|||
|
||||
// ── Home Page ──────────────────────────────────────
|
||||
home: {
|
||||
heroEyebrow: '献给不愿为邮编名气多付钱的买家',
|
||||
heroTitle1: '找到那个',
|
||||
heroTitle2: '被低估的宝藏邮编',
|
||||
heroTitle3: '同样的学校、同样的通勤,却悄悄更便宜。',
|
||||
|
|
@ -1042,7 +1041,6 @@ const zh: Translations = {
|
|||
statFilters: '可组合筛选条件',
|
||||
statEvery: '每一个',
|
||||
statPostcodeInEngland: '英格兰的邮编',
|
||||
coverageNote: '覆盖英格兰所有邮编,每个邮编 200 多个数据字段。苏格兰和威尔士已在计划中。',
|
||||
priceStrip: '一次付费,终身使用。当前价格 {{price}},随着各档名额售罄而上调。',
|
||||
priceStripSpots: '此价格仅剩 {{count}} 个名额。',
|
||||
priceStripSpotsPlural: '此价格仅剩 {{count}} 个名额。',
|
||||
|
|
|
|||
|
|
@ -270,6 +270,12 @@ export const POI_CATEGORY_LOGOS: Record<string, string> = {
|
|||
'Whole Foods Market': '/assets/poi-icons/brands_2024/wholefoods.svg',
|
||||
};
|
||||
|
||||
/** Zoom level below which every POI is hidden. Deliberately set to the same
|
||||
* value as the map overlays' limit (POSTCODE_ZOOM_THRESHOLD) so POIs and
|
||||
* overlays clear together on the way out, but kept as its own constant so POI
|
||||
* visibility can be retuned without moving the postcode/overlay limit. */
|
||||
export const POI_ZOOM_THRESHOLD = 12;
|
||||
|
||||
/** Categories only shown when zoomed in past MINOR_POI_ZOOM_THRESHOLD */
|
||||
export const MINOR_POI_CATEGORIES = new Set(['Bus stop', 'Taxi rank', 'EV Charging', 'Playground']);
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ _AREA_COLUMNS = [
|
|||
# postcode ever recorded as social housing per EPC, and the ever-social
|
||||
# subset whose latest EPC certificate is no longer social rented (sold off).
|
||||
# Aggregated from the per-property "was_council_house" / "latest_tenure_status"
|
||||
# flags in _epc_council_by_postcode and joined onto the AREA frame only.
|
||||
# flags in _epc_council_by_postcode, then joined onto the area frame and the
|
||||
# wide frame (the latter carries them onto listing rows).
|
||||
"% Council housing",
|
||||
"% Ex-council",
|
||||
# Politics
|
||||
|
|
@ -2329,7 +2330,11 @@ def _epc_council_by_postcode(wide: pl.LazyFrame) -> pl.LazyFrame:
|
|||
|
||||
``was_council_house`` is already "Yes"/"No" filled for every row (see
|
||||
``_fill_property_level_no_defaults``), so the means are over the full postcode.
|
||||
Returns a postcode-keyed LazyFrame to left-join onto the AREA frame only.
|
||||
Returns a postcode-keyed LazyFrame to left-join onto the area and wide frames.
|
||||
|
||||
Call this against the dwelling universe only. `_build` runs it before the
|
||||
listings splice: unmatched listings append synthetic rows that are not
|
||||
dwellings, so they must not reach the denominator.
|
||||
"""
|
||||
currently_social = (pl.col("latest_tenure_status") == "Rented (social)").fill_null(
|
||||
False
|
||||
|
|
@ -2420,6 +2425,17 @@ def _build(
|
|||
pl.lit(None, dtype=pl.Utf8).alias(LISTED_BUILDING_FEATURE)
|
||||
)
|
||||
|
||||
# EPC-derived council/ex-council postcode shares, computed here against the
|
||||
# pre-splice dwelling universe so both output modes report identical values.
|
||||
# In listings mode the splice below appends one seed row per unmatched
|
||||
# listing; those rows would otherwise enter the group_by denominator, and
|
||||
# since they carry no latest_tenure_status every ever-social one would count
|
||||
# as ex-council. Reading `wide` here does not rebind it, so the aggregate
|
||||
# keeps the pre-splice plan; the fill is idempotent with the one below.
|
||||
epc_council_by_postcode = _epc_council_by_postcode(
|
||||
_fill_property_level_no_defaults(wide)
|
||||
)
|
||||
|
||||
if actual_listings_path is not None:
|
||||
wide = _integrate_listings(
|
||||
wide,
|
||||
|
|
@ -2515,12 +2531,14 @@ def _build(
|
|||
wide = _join_area_side_tables(wide, **area_side_tables)
|
||||
postcode_area = _join_area_side_tables(postcode_area, **area_side_tables)
|
||||
|
||||
# EPC-derived council/ex-council shares: aggregate the per-property social
|
||||
# tenure flags to POSTCODE percentages and attach to the AREA frame only
|
||||
# (these are area columns, like the Census tenure block, not per-property).
|
||||
# Built before dropping latest_tenure_status, which is its only consumer.
|
||||
epc_council_by_postcode = _epc_council_by_postcode(wide)
|
||||
postcode_area = postcode_area.join(epc_council_by_postcode, on="postcode", how="left")
|
||||
# EPC-derived council/ex-council shares (aggregated above): attach to both
|
||||
# frames, like every other area side table. The area frame feeds
|
||||
# postcode.parquet; the wide frame carries them onto listing rows, which are
|
||||
# projected out of `wide` alone and never see the area frame.
|
||||
wide = wide.join(epc_council_by_postcode, on="postcode", how="left")
|
||||
postcode_area = postcode_area.join(
|
||||
epc_council_by_postcode, on="postcode", how="left"
|
||||
)
|
||||
# latest_tenure_status is property-grain and not in _AREA_COLUMNS, so the
|
||||
# split would otherwise leak it into properties.parquet. It has served its
|
||||
# purpose (the postcode aggregate above), so drop it from the property frame.
|
||||
|
|
|
|||
|
|
@ -296,7 +296,9 @@ export interface VideoConfig {
|
|||
* chosen here as a voice file instead. */
|
||||
export interface VoiceConfig {
|
||||
/** Predefined voice filename on the Chatterbox server, INCLUDING `.wav`
|
||||
* (e.g. "Southern-M.wav"). `GET /v1/audio/voices` lists the valid names. */
|
||||
* (e.g. "Southern-M.wav"). `GET /get_predefined_voices` lists the valid
|
||||
* names; `GET /v1/audio/voices` looks like it does but answers from a
|
||||
* boot-time snapshot that omits recently uploaded voices. */
|
||||
voice: string;
|
||||
/** ISO code, e.g. "en", "de", "zh". Anything other than "en" makes synth.py
|
||||
* swap the server onto the multilingual checkpoint. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue