diff --git a/docker-compose.yml b/docker-compose.yml index 2b2ff44..c8126ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: SCREENSHOT_URL: http://screenshot:8002 GEMINI_API_KEY: AIzaSyC2mQDcEwILHM3uOE2C-lxUQbQrKTX9Xi4 GEMINI_MODEL: gemini-3-flash-preview - PUBLIC_URL: https://perfectpostcodes.schmelczer.dev + PUBLIC_URL: https://perfect-postcodes.co.uk GOOGLE_MAPS_API_KEY: "AIzaSyBgBn9LjrxHCjb9j1LZbLYpEdCJj-NkHPY" STRIPE_SECRET_KEY: sk_test_51SyVcePRjj2bdyn1HLkatQ5onwp8kamm41tjMcRdxXnJYWVPsVd9usMTOSNtNdGhrjbsrtNbgTdKXICg2qBiocEn00PvNDC0d3 STRIPE_WEBHOOK_SECRET: whsec_pIkGZblYlcN2VesTxq4pk1cDqdxOQ1y0 diff --git a/finder/storage.py b/finder/storage.py index deda74f..ded3f42 100644 --- a/finder/storage.py +++ b/finder/storage.py @@ -92,5 +92,14 @@ def write_parquet(properties: list[dict], path: Path, channel: str) -> None: }, ) + # Derive asking price per sqm for buy listings + if channel == "buy": + df = df.with_columns( + (pl.col("Asking price") / pl.col("Total floor area (sqm)")) + .round(0) + .cast(pl.Int32, strict=False) + .alias("Asking price per sqm"), + ) + df.write_parquet(path) log.info("Wrote %d properties to %s", len(df), path) diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..88d5585 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,9 @@ +User-agent: * +Allow: / +Disallow: /api/ +Disallow: /metrics +Disallow: /health +Disallow: /pb/ +Disallow: /s/ + +Sitemap: https://perfect-postcode.co.uk/sitemap.xml diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml new file mode 100644 index 0000000..da7f388 --- /dev/null +++ b/frontend/public/sitemap.xml @@ -0,0 +1,23 @@ + + + + https://perfect-postcode.co.uk/ + weekly + 1.0 + + + https://perfect-postcode.co.uk/dashboard + daily + 0.9 + + + https://perfect-postcode.co.uk/learn + monthly + 0.7 + + + https://perfect-postcode.co.uk/pricing + monthly + 0.8 + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3b7dbbc..5aedbd5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,7 @@ import { INITIAL_VIEW_STATE } from './lib/consts'; import { useTheme } from './hooks/useTheme'; import { useIsMobile } from './hooks/useIsMobile'; import { useAuth } from './hooks/useAuth'; +import { useTelemetry } from './hooks/useTelemetry'; import { useSavedSearches } from './hooks/useSavedSearches'; import { useSavedProperties } from './hooks/useSavedProperties'; @@ -107,6 +108,7 @@ export default function App() { const { theme, toggleTheme } = useTheme(); const isMobile = useIsMobile(); + useTelemetry(); const { user, loading: authLoading, @@ -337,12 +339,14 @@ export default function App() { searches={savedSearches.searches} searchesLoading={savedSearches.loading} onDeleteSearch={savedSearches.deleteSearch} + onUpdateSearchNotes={savedSearches.updateSearchNotes} onOpenSearch={(params) => { window.location.href = `/dashboard?${params}`; }} savedProperties={savedProperties.properties} propertiesLoading={savedProperties.loading} onDeleteProperty={savedProperties.deleteProperty} + onUpdatePropertyNotes={savedProperties.updatePropertyNotes} onOpenProperty={(postcode) => { window.location.href = `/dashboard?pc=${encodeURIComponent(postcode)}`; }} diff --git a/frontend/src/components/account/AccountPage.tsx b/frontend/src/components/account/AccountPage.tsx index e9fa7fe..63b0f8d 100644 --- a/frontend/src/components/account/AccountPage.tsx +++ b/frontend/src/components/account/AccountPage.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import type { AuthUser } from '../../hooks/useAuth'; import type { SavedSearch } from '../../hooks/useSavedSearches'; import type { SavedProperty, SavedPropertyData } from '../../hooks/useSavedProperties'; @@ -71,6 +71,63 @@ function DeleteDialog({ ); } +function NotesInput({ + value, + onSave, +}: { + value: string; + onSave: (notes: string) => void; +}) { + const [text, setText] = useState(value); + const textareaRef = useRef(null); + const timerRef = useRef | null>(null); + + // Sync from parent when value changes externally + useEffect(() => { + setText(value); + }, [value]); + + const autoResize = useCallback(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = 'auto'; + el.style.height = `${el.scrollHeight}px`; + }, []); + + // Resize on mount and when text changes + useEffect(() => { + autoResize(); + }, [text, autoResize]); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const newText = e.target.value; + setText(newText); + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => onSave(newText), 600); + }, + [onSave] + ); + + // Save immediately on blur + const handleBlur = useCallback(() => { + if (timerRef.current) clearTimeout(timerRef.current); + if (text !== value) onSave(text); + }, [text, value, onSave]); + + return ( +