Fix LFS
Some checks failed
Build and publish Docker image / build-and-push (push) Failing after 10s
CI / Check (push) Failing after 4m35s

This commit is contained in:
Andras Schmelczer 2026-06-15 20:50:57 +01:00
parent 408d3f87f0
commit 8c679fcf96
3 changed files with 83 additions and 27 deletions

View file

@ -12,6 +12,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
# The product-demo videos and their poster JPGs live in Git LFS
# (see .gitattributes). Without this, checkout writes ~130-byte LFS
# pointer text files, the Docker build copies those stubs into
# frontend/dist/video/*, and the server serves text as video/mp4 —
# so the videos never load in production. Smudge the real binaries.
with:
lfs: true
- name: Build and publish main image
uses: http://forgejo:3000/andras/ci-actions/docker-publish@main

View file

@ -209,6 +209,11 @@ export default function App() {
}, []);
const initialRoute = useMemo(() => pathToPage(window.location.pathname), []);
const [mapUrlState, setMapUrlState] = useState(urlState);
// IP-derived initial map centre (fetched from /api/geo on a fresh visit). Null
// until resolved; `geoChecked` flips once the lookup settles (or times out) so
// the dashboard map mounts with the right centre instead of a London flash.
const [ipViewState, setIpViewState] = useState<typeof INITIAL_VIEW_STATE | null>(null);
const [geoChecked, setGeoChecked] = useState(() => urlState.hasExplicitView);
const [dashboardRouteKey, setDashboardRouteKey] = useState(() =>
window.location.pathname === '/dashboard' ? window.location.search : ''
);
@ -220,8 +225,11 @@ export default function App() {
);
const activePageRef = useRef<Page>('home');
const initialViewState = useMemo(
() => mapUrlState.viewState || INITIAL_VIEW_STATE,
[mapUrlState.viewState]
() =>
mapUrlState.hasExplicitView
? mapUrlState.viewState
: (ipViewState ?? INITIAL_VIEW_STATE),
[mapUrlState.hasExplicitView, mapUrlState.viewState, ipViewState]
);
const isScreenshotMode = useMemo(() => {
@ -416,6 +424,44 @@ export default function App() {
return () => controller.abort();
}, []);
// On a fresh visit (no view in the URL), centre the initial map on the visitor's
// approximate location from their IP. Falls back to INITIAL_VIEW_STATE (inner
// London) when geolocation is unavailable. Skipped when the URL already carries a
// view (shared/dashboard links) so we never override it. `geoChecked` gates the
// dashboard map render so it mounts once with the resolved centre.
useEffect(() => {
if (urlState.hasExplicitView) return; // geoChecked already initialised true
let cancelled = false;
const controller = new AbortController();
const finish = () => {
if (!cancelled) setGeoChecked(true);
};
// Never block the map for more than this if the lookup is slow/hangs.
const timeout = window.setTimeout(finish, 1500);
fetch(apiUrl('geo'), { signal: controller.signal })
.then((res) => (res.ok ? res.json() : null))
.then((data: { center?: { lat: number; lon: number } } | null) => {
if (cancelled || !data?.center) return;
setIpViewState({
...INITIAL_VIEW_STATE,
latitude: data.center.lat,
longitude: data.center.lon,
});
})
.catch(() => {})
.finally(() => {
window.clearTimeout(timeout);
finish();
});
return () => {
cancelled = true;
window.clearTimeout(timeout);
controller.abort();
};
// Mount-only: resolve the IP-based initial view exactly once.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const navigateTo = useCallback(
(page: Page, hash?: string, infoFeature?: string) => {
const targetHash = normalizeHash(hash);
@ -723,6 +769,8 @@ export default function App() {
refreshAuth();
}}
/>
) : !geoChecked ? (
<PageFallback />
) : (
<MapPage
key={dashboardRouteKey}

View file

@ -914,15 +914,15 @@ const en = {
// ── Home Page ──────────────────────────────────────
home: {
heroEyebrow: 'For buyers asking “where should I even look?”',
heroTitle1: 'Find the postcodes that',
heroTitle2: 'fit your life',
heroTitle3: 'Not just the areas you already know.',
heroEyebrow: 'For buyers who refuse to overpay for a postcodes reputation',
heroTitle1: 'Find the',
heroTitle2: 'hidden-gem postcode',
heroTitle3: 'Best £/sqm, schools and commute for the money.',
heroSubtitle:
'From London boroughs to commuter towns and regional cities, England has too many places to research one by one.',
'Across England, two streets can share the same schools and commute yet sell for very different money.',
heroDescription:
'Set your budget, commute, schools, safety, noise, broadband, and lifestyle needs. Perfect Postcode scans Englands postcodes and reveals the places that actually fit, including areas youd never have typed into a listing portal.',
exploreTheMap: 'Find my matching postcodes',
'Set your budget and must-haves. Perfect Postcode ranks every postcode in England on price per square metre, schools, commute and safety, surfacing the underpriced ones with the evidence behind each pick.',
exploreTheMap: 'Find my best-value postcodes',
seeTheDifference: 'See how it works',
productDemoLabel: 'Perfect Postcode product demo',
playProductDemo: 'Play Perfect Postcode product demo',
@ -946,7 +946,7 @@ const en = {
showcaseSendShortlist: 'Send the shortlist',
showcaseDownloadXlsx: 'Download .xlsx',
showcaseTopThree: 'Top 3',
showcaseScoutBullet1: 'Walk the streets before the listing search narrows your options.',
showcaseScoutBullet1: 'Walk the streets of the postcode that beats its pricier neighbour.',
showcaseScoutBullet2: 'Test the commute from a real front door, not a borough name.',
showcaseScoutBullet3: 'Compare viewings with evidence already in hand.',
showcaseStep1Tab: 'Filter',
@ -960,7 +960,7 @@ const en = {
showcaseStep2Tab: 'Match',
showcaseStep2Title: 'Let the map surface places you wouldnt have typed',
showcaseStep2Body:
'Scan England by fit instead of starting from familiar area names. Hidden pockets become visible before listing portals narrow your imagination.',
'Rank England by value for money, not by area names you already know. The underpriced pockets — strong on £/sqm, schools and commute — rise to the top.',
showcaseStep2Region: 'Greater London',
showcaseStep2Sources: 'Land Registry · ONS · Ofsted · DfT',
showcaseStep2ClustersLabel: 'Matching clusters',
@ -999,40 +999,41 @@ const en = {
priceStripSpots: '{{count}} spot left at this price.',
priceStripSpotsPlural: '{{count}} spots left at this price.',
priceStripCta: 'See pricing',
ourPhilosophy: 'Start with what matters, then find the right postcode',
ourPhilosophy: 'Reputation is priced in. Value isnt yet',
philosophyP1:
'Most property sites ask where you want to live. In London thats painfully hard, but the same problem shows up across England: buyers choose from the few places they know, then cross-check commute tools, Ofsted, police data, Street View, broadband checkers, and sold prices in separate tabs.',
'A postcodes price is partly its fundamentals and partly its reputation. People pay up for names they know, while the postcode one street over — same schools, same commute — sits quietly cheaper because nobody has bid it up. Spotting that gap means cross-checking sold prices, Ofsted, crime, noise and floor areas tab by tab.',
philosophyP2:
'Perfect Postcode flips the search. Tell the map what matters and it shows the postcodes that qualify, with evidence for why theyre worth inspecting. Data first, then go test the vibe.',
streetTitle: 'Places change street by street',
'Perfect Postcode ranks by value instead. Tell the map your budget and must-haves, and it surfaces the postcodes with the best £/sqm, schools and commute for the money. Then go test it in person.',
streetTitle: 'Value changes street by street',
streetIntro:
'Broad area names hide the details that matter: the station side, the road noise, the school mix, the exact commute, and what similar homes actually sold for.',
streetCard1Title: 'Find areas you may have missed',
'Broad area names hide where the value sits: the cheaper station side, the quieter road, the same school catchment, and what near-identical homes sold for per square metre.',
streetCard1Title: 'Find the cheaper twin',
streetCard1Body:
'Surface postcodes that match your requirements instead of relying on familiar names, friend recommendations, or “up-and-coming” hype.',
streetCard2Title: 'See the trade-offs before viewings',
'Surface the postcodes with the same schools, commute and build as the names everyone pays a premium for — minus the premium, and minus the “up-and-coming” hype.',
streetCard2Title: 'See what your money actually buys',
streetCard2Body:
'Compare price, space, commute, safety, schools, broadband, noise, and energy ratings before you spend weekends travelling between viewings.',
'Compare £/sqm, sold prices, space, commute, safety, schools, broadband, noise and energy ratings side by side, before you spend weekends between viewings.',
othersVs: 'Others vs',
checkMyPostcode: 'Listing portals',
areaGuides: 'Postcode reports',
compSearchWithout: 'Discover areas before you know their names',
compSearchWithoutSub: '(requirements first, location second)',
compSearchWithout: 'Rank postcodes by value, not by reputation',
compSearchWithoutSub: '(best £/sqm for your needs, not the name)',
compAreaData: 'Postcode-level neighbourhood evidence',
compAreaDataSub: '(crime, schools, noise, broadband, amenities)',
compPropertyData: 'Property-level history',
compPropertyDataSub: '(sold prices, EPC, floor area, estimated value)',
compFilters: '40+ filters working together',
compFiltersSub: '(not one postcode or one listing at a time)',
ctaTitle: 'Stop guessing where to buy.',
ctaTitle: 'Stop overpaying for a name.',
ctaDescription:
'Build a shortlist of postcodes that fit your actual life, then test them in person.',
'Build a shortlist of the best-value postcodes for your budget, then confirm it in person.',
},
// ── Pricing Page ───────────────────────────────────
// ── Footer ─────────────────────────────────────────
footer: {
tagline: 'Find the postcodes that fit your life. Evidence-first area research for England.',
tagline:
'Find the hidden-gem postcode: best £/sqm, schools and commute. Evidence-first, England.',
product: 'Product',
resources: 'Resources',
legal: 'Legal',
@ -1281,9 +1282,9 @@ const en = {
faqEnv3A:
'You can check energy rating, floor area, building age, council tax area, broadband, and noise before viewing. It wont predict your exact bills, but it helps you avoid obvious mismatches early.',
// FAQ items — Listing Portals and Due Diligence
faqDueDiligence1Q: 'Should I use this before or after checking Rightmove?',
faqDueDiligence1Q: 'How is this different from searching on Rightmove?',
faqDueDiligence1A:
'Use Perfect Postcode before and alongside listing sites. Rightmove, Zoopla, and OnTheMarket are still where you check whats for sale now, photos, agents, viewings, and alerts. Perfect Postcode helps you decide which postcodes are worth searching in the first place.',
'They answer different questions. Rightmove, Zoopla, and OnTheMarket show whats for sale now — photos, agents, viewings, and alerts. Perfect Postcode works at the postcode level, weighing sold prices, £/sqm, schools, commute, and crime to show where youd get more home for your money.',
faqDueDiligence2Q: 'Why cant I filter by garden, garage, or layout?',
faqDueDiligence2A:
'Those details arent available consistently for every home. Perfect Postcode can filter by things such as floor area, home type, ownership type, energy rating, sold prices, and local area data. Gardens, garages, aspect, room layout, and estate-agent wording still need to be checked in the listing and at the viewing.',