Small changes and fix zooming
This commit is contained in:
parent
c69bb0d614
commit
329685a4ee
16 changed files with 823 additions and 202 deletions
|
|
@ -1,10 +1,13 @@
|
|||
# Stage 1: Build frontend
|
||||
FROM node:22-slim AS frontend
|
||||
FROM node:22-bookworm-slim AS frontend
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN apt-get update \
|
||||
&& npx puppeteer browsers install chrome --install-deps \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY frontend/ ./
|
||||
RUN npm run build:no-prerender
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build Rust server
|
||||
FROM rust:1.84-bookworm AS server
|
||||
|
|
|
|||
744
frontend/package-lock.json
generated
744
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -62,6 +62,7 @@
|
|||
"prettier": "^3.2.0",
|
||||
"puppeteer": "^24.0.0",
|
||||
"react-refresh": "^0.18.0",
|
||||
"sharp": "^0.34.5",
|
||||
"style-loader": "^4.0.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"ts-loader": "^9.5.0",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
|
|
@ -5,5 +5,9 @@ Disallow: /metrics
|
|||
Disallow: /health
|
||||
Disallow: /pb/
|
||||
Disallow: /s/
|
||||
Disallow: /account
|
||||
Disallow: /saved
|
||||
Disallow: /invites
|
||||
Disallow: /invite/
|
||||
|
||||
Sitemap: https://perfect-postcode.co.uk/sitemap.xml
|
||||
|
|
|
|||
|
|
@ -87,13 +87,11 @@ function DeckOverlay({
|
|||
getTooltip: any;
|
||||
}) {
|
||||
const overlay = useControl(() => new MapboxOverlay({ interleaved: true }));
|
||||
const prevLayersRef = useRef(layers);
|
||||
const prevTooltipRef = useRef(getTooltip);
|
||||
if (layers !== prevLayersRef.current || getTooltip !== prevTooltipRef.current) {
|
||||
prevLayersRef.current = layers;
|
||||
prevTooltipRef.current = getTooltip;
|
||||
overlay.setProps({ layers, getTooltip });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
overlay.setProps({ layers: layers.filter(Boolean), getTooltip });
|
||||
}, [overlay, layers, getTooltip]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -376,7 +374,7 @@ export default memo(function Map({
|
|||
</div>
|
||||
{popupInfo && (
|
||||
<div
|
||||
className="absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg text-sm dark:text-white"
|
||||
className="pointer-events-none absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg text-sm dark:text-white"
|
||||
style={{
|
||||
left: popupInfo.x,
|
||||
top: popupInfo.y - 50,
|
||||
|
|
@ -385,7 +383,7 @@ export default memo(function Map({
|
|||
}}
|
||||
>
|
||||
<button
|
||||
className="absolute -top-2 -right-2 w-5 h-5 flex items-center justify-center rounded-full bg-warm-200 dark:bg-warm-700 text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shadow-sm"
|
||||
className="pointer-events-auto absolute -top-2 -right-2 w-5 h-5 flex items-center justify-center rounded-full bg-warm-200 dark:bg-warm-700 text-warm-500 dark:text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shadow-sm"
|
||||
onClick={clearPopupInfo}
|
||||
>
|
||||
<CloseIcon className="w-3 h-3" />
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ export type Page =
|
|||
| 'dashboard'
|
||||
| 'learn'
|
||||
| 'pricing'
|
||||
| 'property-price-map'
|
||||
| 'postcode-property-search'
|
||||
| 'commute-property-search'
|
||||
| 'school-property-search'
|
||||
| 'postcode-checker'
|
||||
| 'birmingham-property-search'
|
||||
| 'manchester-property-search'
|
||||
| 'bristol-property-search'
|
||||
| 'data-sources'
|
||||
| 'methodology'
|
||||
| 'privacy-security'
|
||||
| 'account'
|
||||
| 'saved'
|
||||
| 'invites'
|
||||
|
|
@ -31,6 +42,17 @@ export const PAGE_PATHS: Record<Page, string> = {
|
|||
dashboard: '/dashboard',
|
||||
learn: '/learn',
|
||||
pricing: '/pricing',
|
||||
'property-price-map': '/property-price-map',
|
||||
'postcode-property-search': '/postcode-property-search',
|
||||
'commute-property-search': '/commute-property-search',
|
||||
'school-property-search': '/school-property-search',
|
||||
'postcode-checker': '/postcode-checker',
|
||||
'birmingham-property-search': '/property-search/birmingham',
|
||||
'manchester-property-search': '/property-search/manchester',
|
||||
'bristol-property-search': '/property-search/bristol',
|
||||
'data-sources': '/data-sources',
|
||||
methodology: '/methodology',
|
||||
'privacy-security': '/privacy-security',
|
||||
saved: '/saved',
|
||||
invites: '/invites',
|
||||
account: '/account',
|
||||
|
|
@ -134,7 +156,7 @@ export default function Header({
|
|||
onClick={(e) => navLink('home', e)}
|
||||
>
|
||||
<LogoIcon className="w-5 h-5 text-teal-400" />
|
||||
<span className="font-semibold text-lg">{t('header.appName')}</span>
|
||||
<span className="text-lg font-semibold text-teal-300">{t('header.appName')}</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop nav */}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SUPPORTED_LANGUAGES, type LanguageCode } from '../../i18n';
|
||||
import {
|
||||
changeLanguage as changeAppLanguage,
|
||||
SUPPORTED_LANGUAGES,
|
||||
type LanguageCode,
|
||||
} from '../../i18n';
|
||||
|
||||
export default function LanguageDropdown() {
|
||||
const { i18n } = useTranslation();
|
||||
|
|
@ -20,8 +24,8 @@ export default function LanguageDropdown() {
|
|||
}, [open]);
|
||||
|
||||
const changeLanguage = (code: LanguageCode) => {
|
||||
i18n.changeLanguage(code);
|
||||
localStorage.setItem('language', code);
|
||||
void changeAppLanguage(code);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import type { Page } from './Header';
|
||||
import { PAGE_PATHS } from './Header';
|
||||
import type { AuthUser } from '../../hooks/useAuth';
|
||||
import { SUPPORTED_LANGUAGES } from '../../i18n';
|
||||
import { changeLanguage as changeAppLanguage, SUPPORTED_LANGUAGES } from '../../i18n';
|
||||
import { DownloadIcon } from './icons/DownloadIcon';
|
||||
import { BookmarkIcon } from './icons/BookmarkIcon';
|
||||
import { CheckIcon } from './icons/CheckIcon';
|
||||
|
|
@ -161,8 +161,8 @@ export default function MobileMenu({
|
|||
<button
|
||||
key={lang.code}
|
||||
onClick={() => {
|
||||
i18n.changeLanguage(lang.code);
|
||||
localStorage.setItem('language', lang.code);
|
||||
void changeAppLanguage(lang.code);
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-2 rounded text-sm ${
|
||||
i18n.language === lang.code
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ interface UseMapDataOptions {
|
|||
features: FeatureMeta[];
|
||||
viewFeature: string | null;
|
||||
activeFeature: string | null;
|
||||
pinnedFeature: string | null;
|
||||
travelTimeEntries: TravelTimeEntry[];
|
||||
/** Share-link code from the URL; appended to data fetches so the backend
|
||||
* grants bbox-scoped access for unlicensed recipients. */
|
||||
|
|
@ -50,6 +51,7 @@ export function useMapData({
|
|||
features,
|
||||
viewFeature,
|
||||
activeFeature,
|
||||
pinnedFeature,
|
||||
travelTimeEntries,
|
||||
shareCode,
|
||||
}: UseMapDataOptions) {
|
||||
|
|
@ -83,6 +85,10 @@ export function useMapData({
|
|||
() => (viewFeature ? (getSchoolBackendFeatureName(viewFeature) ?? viewFeature) : null),
|
||||
[viewFeature]
|
||||
);
|
||||
const pinnedDataViewFeature = useMemo(
|
||||
() => (pinnedFeature ? (getSchoolBackendFeatureName(pinnedFeature) ?? pinnedFeature) : null),
|
||||
[pinnedFeature]
|
||||
);
|
||||
|
||||
// Determine if the current viewFeature is an enum (for enum_dist param)
|
||||
const viewFeatureIsEnum = useMemo(
|
||||
|
|
@ -95,6 +101,7 @@ export function useMapData({
|
|||
(): string => buildFilterString(filters, features),
|
||||
[filters, features]
|
||||
);
|
||||
const filtersParam = useMemo(() => buildFilterParam(), [buildFilterParam]);
|
||||
|
||||
// Build the travel param string from entries with destinations.
|
||||
// Format: mode:slug[:best][:min:max] — server filters rows outside [min,max].
|
||||
|
|
@ -122,6 +129,37 @@ export function useMapData({
|
|||
);
|
||||
|
||||
const travelParam = useMemo(() => buildTravelParam(), [buildTravelParam]);
|
||||
const boundsParam = useMemo(
|
||||
() => (bounds ? `${bounds.south},${bounds.west},${bounds.north},${bounds.east}` : ''),
|
||||
[bounds]
|
||||
);
|
||||
const dataRequestKey = useMemo(
|
||||
() =>
|
||||
bounds
|
||||
? [
|
||||
usePostcodeView ? 'postcodes' : 'hexagons',
|
||||
resolution,
|
||||
boundsParam,
|
||||
filtersParam,
|
||||
dataViewFeature ?? '',
|
||||
viewFeatureIsEnum && dataViewFeature ? dataViewFeature : '',
|
||||
travelParam,
|
||||
shareCode ?? '',
|
||||
].join('|')
|
||||
: '',
|
||||
[
|
||||
bounds,
|
||||
boundsParam,
|
||||
dataViewFeature,
|
||||
filtersParam,
|
||||
resolution,
|
||||
shareCode,
|
||||
travelParam,
|
||||
usePostcodeView,
|
||||
viewFeatureIsEnum,
|
||||
]
|
||||
);
|
||||
const [loadedDataKey, setLoadedDataKey] = useState<string>('');
|
||||
|
||||
// Keep activeFeatureRef in sync
|
||||
useEffect(() => {
|
||||
|
|
@ -219,12 +257,11 @@ export function useMapData({
|
|||
|
||||
setLoading(true);
|
||||
try {
|
||||
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
||||
const filtersStr = buildFilterParam();
|
||||
const requestKey = dataRequestKey;
|
||||
|
||||
if (usePostcodeView) {
|
||||
const params = new URLSearchParams({ bounds: boundsStr });
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
const params = new URLSearchParams({ bounds: boundsParam });
|
||||
if (filtersParam) params.set('filters', filtersParam);
|
||||
params.set(
|
||||
'fields',
|
||||
dataViewFeature && !dataViewFeature.startsWith('tt_') ? dataViewFeature : ''
|
||||
|
|
@ -254,12 +291,13 @@ export function useMapData({
|
|||
const json: { features: PostcodeFeature[] } = await res.json();
|
||||
setPostcodeData(json.features);
|
||||
setRawData([]);
|
||||
setLoadedDataKey(requestKey);
|
||||
} else {
|
||||
const params = new URLSearchParams({
|
||||
resolution: resolution.toString(),
|
||||
bounds: boundsStr,
|
||||
bounds: boundsParam,
|
||||
});
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
if (filtersParam) params.set('filters', filtersParam);
|
||||
params.set(
|
||||
'fields',
|
||||
dataViewFeature && !dataViewFeature.startsWith('tt_') ? dataViewFeature : ''
|
||||
|
|
@ -289,6 +327,7 @@ export function useMapData({
|
|||
const json: ApiResponse = await res.json();
|
||||
setRawData(json.features);
|
||||
setPostcodeData([]);
|
||||
setLoadedDataKey(requestKey);
|
||||
}
|
||||
|
||||
// Clear drag data when committed fetch completes and we're not mid-drag
|
||||
|
|
@ -315,7 +354,9 @@ export function useMapData({
|
|||
resolution,
|
||||
bounds,
|
||||
filters,
|
||||
buildFilterParam,
|
||||
filtersParam,
|
||||
boundsParam,
|
||||
dataRequestKey,
|
||||
dataViewFeature,
|
||||
viewFeatureIsEnum,
|
||||
usePostcodeView,
|
||||
|
|
@ -377,8 +418,8 @@ export function useMapData({
|
|||
];
|
||||
}, [dataViewFeature, rawData, postcodeData, usePostcodeView, features, bounds]);
|
||||
|
||||
// Color range for the legend and hex coloring
|
||||
const colorRange = useMemo((): [number, number] | null => {
|
||||
// Live color range for the legend and hex coloring.
|
||||
const liveColorRange = useMemo((): [number, number] | null => {
|
||||
if (!dataViewFeature) return null;
|
||||
|
||||
// Travel time keys: use dataRange directly (no FeatureMeta)
|
||||
|
|
@ -396,6 +437,61 @@ export function useMapData({
|
|||
return null;
|
||||
}, [dataViewFeature, features, dataRange]);
|
||||
|
||||
const isEyePreviewingPinnedFeature =
|
||||
!activeFeature && dataViewFeature != null && dataViewFeature === pinnedDataViewFeature;
|
||||
|
||||
const [frozenPreviewRange, setFrozenPreviewRange] = useState<{
|
||||
feature: string;
|
||||
range: [number, number];
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setFrozenPreviewRange((prev) => {
|
||||
if (!pinnedDataViewFeature) return prev ? null : prev;
|
||||
return prev?.feature === pinnedDataViewFeature ? prev : null;
|
||||
});
|
||||
}, [pinnedDataViewFeature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEyePreviewingPinnedFeature || !pinnedDataViewFeature) return;
|
||||
|
||||
const meta = pinnedDataViewFeature.startsWith('tt_')
|
||||
? null
|
||||
: features.find((f) => f.name === pinnedDataViewFeature);
|
||||
const rangeToFreeze =
|
||||
dataRange && loadedDataKey === dataRequestKey
|
||||
? dataRange
|
||||
: meta?.type === 'enum' && liveColorRange
|
||||
? liveColorRange
|
||||
: null;
|
||||
if (!rangeToFreeze) return;
|
||||
|
||||
setFrozenPreviewRange((prev) =>
|
||||
prev?.feature === pinnedDataViewFeature
|
||||
? prev
|
||||
: { feature: pinnedDataViewFeature, range: rangeToFreeze }
|
||||
);
|
||||
}, [
|
||||
dataRange,
|
||||
dataRequestKey,
|
||||
features,
|
||||
isEyePreviewingPinnedFeature,
|
||||
loadedDataKey,
|
||||
liveColorRange,
|
||||
pinnedDataViewFeature,
|
||||
]);
|
||||
|
||||
const colorRange = useMemo((): [number, number] | null => {
|
||||
if (
|
||||
isEyePreviewingPinnedFeature &&
|
||||
frozenPreviewRange &&
|
||||
frozenPreviewRange.feature === dataViewFeature
|
||||
) {
|
||||
return frozenPreviewRange.range;
|
||||
}
|
||||
return liveColorRange;
|
||||
}, [dataViewFeature, frozenPreviewRange, isEyePreviewingPinnedFeature, liveColorRange]);
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
({
|
||||
resolution: newRes,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Step, CallBackProps } from 'react-joyride';
|
||||
import { ACTIONS, EVENTS, STATUS } from 'react-joyride';
|
||||
|
||||
const STORAGE_KEY = 'tutorial_completed';
|
||||
const JOYRIDE_ACTION_CLOSE = 'close';
|
||||
const JOYRIDE_EVENT_STEP_AFTER = 'step:after';
|
||||
const JOYRIDE_STATUS_FINISHED = 'finished';
|
||||
const JOYRIDE_STATUS_SKIPPED = 'skipped';
|
||||
|
||||
export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked = false) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -67,12 +70,12 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked
|
|||
const handleCallback = useCallback((data: CallBackProps) => {
|
||||
const { status, action, type } = data;
|
||||
|
||||
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
||||
if (status === JOYRIDE_STATUS_FINISHED || status === JOYRIDE_STATUS_SKIPPED) {
|
||||
localStorage.setItem(STORAGE_KEY, '1');
|
||||
setRun(false);
|
||||
}
|
||||
// Also stop if user closes a tooltip via the X button
|
||||
if (action === ACTIONS.CLOSE && type === EVENTS.STEP_AFTER) {
|
||||
if (action === JOYRIDE_ACTION_CLOSE && type === JOYRIDE_EVENT_STEP_AFTER) {
|
||||
localStorage.setItem(STORAGE_KEY, '1');
|
||||
setRun(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,8 +332,8 @@ const en = {
|
|||
// ── Home Page ──────────────────────────────────────
|
||||
home: {
|
||||
heroEyebrow: 'For buyers asking “where should I even look?”',
|
||||
heroTitle1: 'Find the postcodes',
|
||||
heroTitle2: 'that fit your life',
|
||||
heroTitle1: 'Find the postcodes that',
|
||||
heroTitle2: 'fit your life',
|
||||
heroTitle3: 'Not just the areas you already know.',
|
||||
heroSubtitle:
|
||||
'From London boroughs to commuter towns and regional cities, England has too many places to research one by one.',
|
||||
|
|
@ -341,7 +341,7 @@ const en = {
|
|||
'Set your budget, commute, schools, safety, noise, broadband, and lifestyle needs. Perfect Postcode scans England’s postcodes and reveals the places that actually fit, including areas you would never have typed into a listing portal.',
|
||||
exploreTheMap: 'Find my matching postcodes',
|
||||
seeTheDifference: 'See how it works',
|
||||
showcaseHeader: 'Product showcase',
|
||||
showcaseHeader: 'How it works',
|
||||
showcaseContext: 'How Perfect Postcode works',
|
||||
showcaseStep1Tab: 'Filter',
|
||||
showcaseStep1Title: 'Turn vague needs into a tight search',
|
||||
|
|
@ -362,8 +362,8 @@ const en = {
|
|||
showcaseStep3Title: 'Inspect why a postcode made the cut',
|
||||
showcaseStep3Body:
|
||||
'Open any matching area and check prices, safety, schools, broadband, and trade-offs in one pane before you spend a weekend there.',
|
||||
showcaseStep3HeaderArea: 'Penge · SE20',
|
||||
showcaseStep3HeaderFit: 'Strong fit · 7/8',
|
||||
showcaseStep3HeaderArea: 'Your perfect postcode',
|
||||
showcaseStep3HeaderFit: 'Neighbourhood evidence',
|
||||
showcaseStep3Stat1Label: 'Sold price trend',
|
||||
showcaseStep3Stat2Label: 'Crime rate',
|
||||
showcaseStep3Stat2Value: 'Below borough avg.',
|
||||
|
|
|
|||
|
|
@ -101,65 +101,6 @@ h3 {
|
|||
}
|
||||
}
|
||||
|
||||
/* Cereal aside — hover to reveal */
|
||||
@keyframes cereal-wobble {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(6deg);
|
||||
}
|
||||
45% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
60% {
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
.cereal-wobble {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.group:hover .cereal-wobble {
|
||||
animation: cereal-wobble 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.cereal-reveal {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition:
|
||||
grid-template-rows 0.5s ease-out,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.group:hover .cereal-reveal {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.cereal-reveal > * {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cereal-text {
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.4s ease-out,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.group:hover .cereal-text {
|
||||
opacity: 1;
|
||||
transition-delay: 0.2s, 0s;
|
||||
}
|
||||
|
||||
/* Aurora gradient animation for pricing hero */
|
||||
@keyframes aurora-1 {
|
||||
0%,
|
||||
|
|
|
|||
|
|
@ -127,11 +127,7 @@ async fn fetch_share_bounds(state: &AppState, code: &str) -> Option<ShareBounds>
|
|||
return None;
|
||||
}
|
||||
let json: Value = resp.json().await.ok()?;
|
||||
let params = json["items"]
|
||||
.as_array()?
|
||||
.first()?
|
||||
.get("params")?
|
||||
.as_str()?;
|
||||
let params = json["items"].as_array()?.first()?.get("params")?.as_str()?;
|
||||
parse_view_from_params(params).map(|(lat, lon, zoom)| bounds_from_view(lat, lon, zoom))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -298,16 +298,12 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let poi_category_groups = poi_data.category_groups()?;
|
||||
|
||||
// Read index.html at startup for crawler OG injection (only when --dist is provided)
|
||||
let index_html = if let Some(ref dist) = cli.dist {
|
||||
let index_path = dist.join("index.html");
|
||||
let html = std::fs::read_to_string(&index_path)
|
||||
.with_context(|| format!("Failed to read {}", index_path.display()))?;
|
||||
info!("Loaded index.html for OG injection");
|
||||
Some(html)
|
||||
let is_dev = if cli.dist.is_some() {
|
||||
info!("Static frontend serving enabled");
|
||||
false
|
||||
} else {
|
||||
info!("No --dist provided; static serving and OG injection disabled");
|
||||
None
|
||||
info!("No --dist provided; static serving disabled");
|
||||
true
|
||||
};
|
||||
|
||||
let http_client = reqwest::Client::builder()
|
||||
|
|
@ -406,8 +402,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
features_response,
|
||||
screenshot_url: cli.screenshot_url,
|
||||
public_url: cli.public_url,
|
||||
is_dev: index_html.is_none(),
|
||||
index_html,
|
||||
is_dev,
|
||||
http_client,
|
||||
pocketbase_url: cli.pocketbase_url,
|
||||
pocketbase_admin_email: cli.pocketbase_admin_email,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@ pub struct AppState {
|
|||
pub public_url: String,
|
||||
/// True when --dist is not provided (no static serving, relaxed auth checks)
|
||||
pub is_dev: bool,
|
||||
/// Contents of index.html read at startup, used for crawler OG injection (None when --dist omitted)
|
||||
pub index_html: Option<String>,
|
||||
/// Shared HTTP client for proxying to the screenshot service and PocketBase
|
||||
pub http_client: reqwest::Client,
|
||||
/// PocketBase server URL for authentication (e.g. http://localhost:8090)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue