mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a460b813c1 | ||
|
|
4596442f64 | ||
|
|
0bcfa1d4ad | ||
|
|
0cad05124a | ||
|
|
eb53b68d45 | ||
|
|
ba0732e1f6 | ||
|
|
aa67647bbb | ||
|
|
7a9d49899b | ||
|
|
9a87c58d3e | ||
|
|
fdd7e835e8 | ||
|
|
00d6a12b30 | ||
|
|
05218800d2 | ||
|
|
19d4721f9f | ||
|
|
a794645393 | ||
|
|
fd7e228972 |
@@ -4,7 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NoNewListingsWarning } from './errors.js';
|
import { NoNewListingsWarning } from './errors.js';
|
||||||
import { storeListings, getKnownListingHashesForJobAndProvider } from './services/storage/listingsStorage.js';
|
import {
|
||||||
|
storeListings,
|
||||||
|
getKnownListingHashesForJobAndProvider,
|
||||||
|
deleteListingsById,
|
||||||
|
} from './services/storage/listingsStorage.js';
|
||||||
import { getJob } from './services/storage/jobStorage.js';
|
import { getJob } from './services/storage/jobStorage.js';
|
||||||
import * as notify from './notification/notify.js';
|
import * as notify from './notification/notify.js';
|
||||||
import Extractor from './services/extractor/extractor.js';
|
import Extractor from './services/extractor/extractor.js';
|
||||||
@@ -14,6 +18,7 @@ import { geocodeAddress } from './services/geocoding/geoCodingService.js';
|
|||||||
import { distanceMeters } from './services/listings/distanceCalculator.js';
|
import { distanceMeters } from './services/listings/distanceCalculator.js';
|
||||||
import { getUserSettings } from './services/storage/settingsStorage.js';
|
import { getUserSettings } from './services/storage/settingsStorage.js';
|
||||||
import { updateListingDistance } from './services/storage/listingsStorage.js';
|
import { updateListingDistance } from './services/storage/listingsStorage.js';
|
||||||
|
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Listing
|
* @typedef {Object} Listing
|
||||||
@@ -58,18 +63,21 @@ class FredyPipelineExecutioner {
|
|||||||
* @param {(raw:any)=>Listing} providerConfig.normalize Function to convert raw scraped data into a Listing shape.
|
* @param {(raw:any)=>Listing} providerConfig.normalize Function to convert raw scraped data into a Listing shape.
|
||||||
* @param {(listing:Listing)=>boolean} providerConfig.filter Function to filter out unwanted listings.
|
* @param {(listing:Listing)=>boolean} providerConfig.filter Function to filter out unwanted listings.
|
||||||
* @param {(url:string, waitForSelector?:string)=>Promise<void>|Promise<Listing[]>} [providerConfig.getListings] Optional override to fetch listings.
|
* @param {(url:string, waitForSelector?:string)=>Promise<void>|Promise<Listing[]>} [providerConfig.getListings] Optional override to fetch listings.
|
||||||
*
|
|
||||||
* @param {Object} notificationConfig Notification configuration passed to notification adapters.
|
* @param {Object} notificationConfig Notification configuration passed to notification adapters.
|
||||||
|
* @param {Object} spatialFilter Optional spatial filter configuration.
|
||||||
* @param {string} providerId The ID of the provider currently in use.
|
* @param {string} providerId The ID of the provider currently in use.
|
||||||
* @param {string} jobKey Key of the job that is currently running (from within the config).
|
* @param {string} jobKey Key of the job that is currently running (from within the config).
|
||||||
* @param {SimilarityCache} similarityCache Cache instance for checking similar entries.
|
* @param {SimilarityCache} similarityCache Cache instance for checking similar entries.
|
||||||
|
* @param browser
|
||||||
*/
|
*/
|
||||||
constructor(providerConfig, notificationConfig, providerId, jobKey, similarityCache) {
|
constructor(providerConfig, notificationConfig, spatialFilter, providerId, jobKey, similarityCache, browser) {
|
||||||
this._providerConfig = providerConfig;
|
this._providerConfig = providerConfig;
|
||||||
this._notificationConfig = notificationConfig;
|
this._notificationConfig = notificationConfig;
|
||||||
|
this._spatialFilter = spatialFilter;
|
||||||
this._providerId = providerId;
|
this._providerId = providerId;
|
||||||
this._jobKey = jobKey;
|
this._jobKey = jobKey;
|
||||||
this._similarityCache = similarityCache;
|
this._similarityCache = similarityCache;
|
||||||
|
this._browser = browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,6 +96,7 @@ class FredyPipelineExecutioner {
|
|||||||
.then(this._save.bind(this))
|
.then(this._save.bind(this))
|
||||||
.then(this._calculateDistance.bind(this))
|
.then(this._calculateDistance.bind(this))
|
||||||
.then(this._filterBySimilarListings.bind(this))
|
.then(this._filterBySimilarListings.bind(this))
|
||||||
|
.then(this._filterByArea.bind(this))
|
||||||
.then(this._notify.bind(this))
|
.then(this._notify.bind(this))
|
||||||
.catch(this._handleError.bind(this));
|
.catch(this._handleError.bind(this));
|
||||||
}
|
}
|
||||||
@@ -111,6 +120,47 @@ class FredyPipelineExecutioner {
|
|||||||
return newListings;
|
return newListings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter listings by area using the provider's area filter if available.
|
||||||
|
* Only filters if areaFilter is set on the provider AND the listing has coordinates.
|
||||||
|
*
|
||||||
|
* @param {Listing[]} newListings New listings to filter by area.
|
||||||
|
* @returns {Promise<Listing[]>} Resolves with listings that are within the area (or not filtered if no area is set).
|
||||||
|
*/
|
||||||
|
_filterByArea(newListings) {
|
||||||
|
const polygonFeatures = this._spatialFilter?.features?.filter((f) => f.geometry?.type === 'Polygon');
|
||||||
|
|
||||||
|
// If no area filter is set, return all listings
|
||||||
|
if (!polygonFeatures?.length) {
|
||||||
|
return newListings;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredIds = [];
|
||||||
|
// Filter listings by area - keep only those within the polygon
|
||||||
|
const keptListings = newListings.filter((listing) => {
|
||||||
|
// If listing doesn't have coordinates, keep it (don't filter out)
|
||||||
|
if (listing.latitude == null || listing.longitude == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the point is inside the polygons
|
||||||
|
const point = [listing.longitude, listing.latitude]; // GeoJSON format: [lon, lat]
|
||||||
|
const isInPolygon = polygonFeatures.some((feature) => booleanPointInPolygon(point, feature));
|
||||||
|
|
||||||
|
if (!isInPolygon) {
|
||||||
|
filteredIds.push(listing.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInPolygon;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredIds.length > 0) {
|
||||||
|
deleteListingsById(filteredIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keptListings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch listings from the provider, using the default Extractor flow unless
|
* Fetch listings from the provider, using the default Extractor flow unless
|
||||||
* a provider-specific getListings override is supplied.
|
* a provider-specific getListings override is supplied.
|
||||||
@@ -119,7 +169,7 @@ class FredyPipelineExecutioner {
|
|||||||
* @returns {Promise<Listing[]>} Resolves with an array of listings (empty when none found).
|
* @returns {Promise<Listing[]>} Resolves with an array of listings (empty when none found).
|
||||||
*/
|
*/
|
||||||
_getListings(url) {
|
_getListings(url) {
|
||||||
const extractor = new Extractor();
|
const extractor = new Extractor({ ...this._providerConfig.puppeteerOptions, browser: this._browser });
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
extractor
|
extractor
|
||||||
.execute(url, this._providerConfig.waitForSelector)
|
.execute(url, this._providerConfig.waitForSelector)
|
||||||
@@ -250,7 +300,8 @@ class FredyPipelineExecutioner {
|
|||||||
* @returns {Listing[]} Listings considered unique enough to keep.
|
* @returns {Listing[]} Listings considered unique enough to keep.
|
||||||
*/
|
*/
|
||||||
_filterBySimilarListings(listings) {
|
_filterBySimilarListings(listings) {
|
||||||
return listings.filter((listing) => {
|
const filteredIds = [];
|
||||||
|
const keptListings = listings.filter((listing) => {
|
||||||
const similar = this._similarityCache.checkAndAddEntry({
|
const similar = this._similarityCache.checkAndAddEntry({
|
||||||
title: listing.title,
|
title: listing.title,
|
||||||
address: listing.address,
|
address: listing.address,
|
||||||
@@ -260,9 +311,16 @@ class FredyPipelineExecutioner {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`Filtering similar entry for title '${listing.title}' and address '${listing.address}' (Provider: '${this._providerId}')`,
|
`Filtering similar entry for title '${listing.title}' and address '${listing.address}' (Provider: '${this._providerId}')`,
|
||||||
);
|
);
|
||||||
|
filteredIds.push(listing.id);
|
||||||
}
|
}
|
||||||
return !similar;
|
return !similar;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (filteredIds.length > 0) {
|
||||||
|
deleteListingsById(filteredIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keptListings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const FEATURES = {
|
export const TRACKING_POIS = {
|
||||||
DISTANCE_ADDRESS_ENTERED: 'DISTANCE_ADDRESS_ENTERED',
|
DISTANCE_ADDRESS_ENTERED: 'DISTANCE_ADDRESS_ENTERED',
|
||||||
|
WELCOME_FINISHED: 'WELCOME_FINISHED',
|
||||||
|
WELCOME_SKIPPED: 'WELCOME_SKIPPED',
|
||||||
};
|
};
|
||||||
@@ -23,6 +23,7 @@ import { listingsRouter } from './routes/listingsRouter.js';
|
|||||||
import { getSettings } from '../services/storage/settingsStorage.js';
|
import { getSettings } from '../services/storage/settingsStorage.js';
|
||||||
import { dashboardRouter } from './routes/dashboardRouter.js';
|
import { dashboardRouter } from './routes/dashboardRouter.js';
|
||||||
import { backupRouter } from './routes/backupRouter.js';
|
import { backupRouter } from './routes/backupRouter.js';
|
||||||
|
import { trackingRouter } from './routes/trackingRoute.js';
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const staticService = files(path.join(getDirName(), '../ui/public'));
|
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||||
const PORT = (await getSettings()).port || 9998;
|
const PORT = (await getSettings()).port || 9998;
|
||||||
@@ -36,6 +37,7 @@ service.use('/api/version', authInterceptor());
|
|||||||
service.use('/api/listings', authInterceptor());
|
service.use('/api/listings', authInterceptor());
|
||||||
service.use('/api/dashboard', authInterceptor());
|
service.use('/api/dashboard', authInterceptor());
|
||||||
service.use('/api/user/settings', authInterceptor());
|
service.use('/api/user/settings', authInterceptor());
|
||||||
|
service.use('/api/tracking', authInterceptor());
|
||||||
|
|
||||||
// /admin can only be accessed when user is having admin permissions
|
// /admin can only be accessed when user is having admin permissions
|
||||||
service.use('/api/admin', adminInterceptor());
|
service.use('/api/admin', adminInterceptor());
|
||||||
@@ -50,6 +52,7 @@ service.use('/api/jobs', jobRouter);
|
|||||||
service.use('/api/login', loginRouter);
|
service.use('/api/login', loginRouter);
|
||||||
service.use('/api/listings', listingsRouter);
|
service.use('/api/listings', listingsRouter);
|
||||||
service.use('/api/dashboard', dashboardRouter);
|
service.use('/api/dashboard', dashboardRouter);
|
||||||
|
service.use('/api/tracking', trackingRouter);
|
||||||
//this route is unsecured intentionally as it is being queried from the login page
|
//this route is unsecured intentionally as it is being queried from the login page
|
||||||
service.use('/api/demo', demoRouter);
|
service.use('/api/demo', demoRouter);
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,16 @@ jobRouter.post('/:jobId/run', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
jobRouter.post('/', async (req, res) => {
|
jobRouter.post('/', async (req, res) => {
|
||||||
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled, shareWithUsers = [] } = req.body;
|
const {
|
||||||
|
provider,
|
||||||
|
notificationAdapter,
|
||||||
|
name,
|
||||||
|
blacklist = [],
|
||||||
|
jobId,
|
||||||
|
enabled,
|
||||||
|
shareWithUsers = [],
|
||||||
|
spatialFilter = null,
|
||||||
|
} = req.body;
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
try {
|
try {
|
||||||
let jobFromDb = jobStorage.getJob(jobId);
|
let jobFromDb = jobStorage.getJob(jobId);
|
||||||
@@ -187,6 +196,7 @@ jobRouter.post('/', async (req, res) => {
|
|||||||
provider,
|
provider,
|
||||||
notificationAdapter,
|
notificationAdapter,
|
||||||
shareWithUsers,
|
shareWithUsers,
|
||||||
|
spatialFilter,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.send(new Error(error));
|
res.send(new Error(error));
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import restana from 'restana';
|
import restana from 'restana';
|
||||||
|
import logger from '../../services/logger.js';
|
||||||
|
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const notificationAdapterRouter = service.newRouter();
|
const notificationAdapterRouter = service.newRouter();
|
||||||
const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js'));
|
const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js'));
|
||||||
@@ -34,11 +36,14 @@ notificationAdapterRouter.post('/try', async (req, res) => {
|
|||||||
serviceName: 'TestCall',
|
serviceName: 'TestCall',
|
||||||
newListings: [
|
newListings: [
|
||||||
{
|
{
|
||||||
price: '42 €',
|
address: 'Heidestrasse 17, 51147 Köln',
|
||||||
title: 'This is a test listing',
|
description: exampleDescription,
|
||||||
address: 'some address',
|
id: '1',
|
||||||
size: '666 2m',
|
imageUrl: 'https://placehold.co/600x400/png',
|
||||||
link: 'https://www.orange-coding.net',
|
price: '1.000 €',
|
||||||
|
size: '76 m²',
|
||||||
|
title: 'Stilvolle gepflegte 3-Raum-Wohnung mit gehobener Innenausstattung',
|
||||||
|
url: 'https://www.orange-coding.net',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
notificationConfig,
|
notificationConfig,
|
||||||
@@ -46,6 +51,7 @@ notificationAdapterRouter.post('/try', async (req, res) => {
|
|||||||
});
|
});
|
||||||
res.send();
|
res.send();
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
|
logger.error('Error during notification adapter test:', Exception);
|
||||||
res.send(new Error(Exception));
|
res.send(new Error(Exception));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -54,3 +60,51 @@ notificationAdapterRouter.get('/', async (req, res) => {
|
|||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
export { notificationAdapterRouter };
|
export { notificationAdapterRouter };
|
||||||
|
|
||||||
|
const exampleDescription = `
|
||||||
|
Wohnungstyp: Etagenwohnung
|
||||||
|
Nutzfläche: 76 m²
|
||||||
|
Etage: 2 von 3
|
||||||
|
Schlafzimmer: 1
|
||||||
|
Badezimmer: 1
|
||||||
|
Bezugsfrei ab: 1.4.2026
|
||||||
|
Haustiere: Nein
|
||||||
|
Garage/Stellplatz: Tiefgarage
|
||||||
|
Anzahl Garage/Stellplatz: 1
|
||||||
|
Kaltmiete (zzgl. Nebenkosten): 1.000 €
|
||||||
|
Preis/m²: 13,16 €/m²
|
||||||
|
Nebenkosten: 230 €
|
||||||
|
Heizkosten in Nebenkosten enthalten: Ja
|
||||||
|
Gesamtmiete: 1.230 €
|
||||||
|
Kaution: 3.000,00
|
||||||
|
Preis pro Parkfläche: 60 €
|
||||||
|
Baujahr: 2000
|
||||||
|
Objektzustand: Modernisiert
|
||||||
|
Qualität der Ausstattung: Gehoben
|
||||||
|
Heizungsart: Fernwärme
|
||||||
|
Energieausweistyp: Verbrauchsausweis
|
||||||
|
Energieausweis: liegt vor
|
||||||
|
Endenergieverbrauch: 72 kWh/(m²∙a)
|
||||||
|
Baujahr laut Energieausweis: 2000
|
||||||
|
|
||||||
|
Diese moderne 3-Zimmer-Wohnung liegt direkt neben einem Park und nur wenige Minuten von der S-Bahn-Haltestelle entfernt. Das Stadtzentrum sowie Freizeiteinrichtungen sind 1,5 km entfernt.
|
||||||
|
|
||||||
|
Die Wohnung ist ideal für Paare oder kleine Familien geeignet.
|
||||||
|
|
||||||
|
Ausstattung:
|
||||||
|
- neuer hochwertiger Bodenbelag (Holzoptik) in allen Räumen außer Bad/Küche
|
||||||
|
- sonniger Balkon (Süd)
|
||||||
|
- Tiefgaragenstellplatz
|
||||||
|
- Kellerabteil
|
||||||
|
- gepflegtes Mehrfamilienhaus
|
||||||
|
|
||||||
|
Die Küche ist vom Mieter nach eigenen Wünschen einzurichten.
|
||||||
|
|
||||||
|
Vermietung direkt vom Eigentümer - provisionsfrei!
|
||||||
|
|
||||||
|
Lage:
|
||||||
|
• Park: 1 Minute zu Fuß
|
||||||
|
• S-Bahn Station: 2 Minuten zu Fuß
|
||||||
|
• Supermärkte, Restaurants, täglicher Bedarf in der Nähe
|
||||||
|
• Gute Anbindung Richtung Großstadt und Flughafen
|
||||||
|
`;
|
||||||
|
|||||||
37
lib/api/routes/trackingRoute.js
Normal file
37
lib/api/routes/trackingRoute.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import restana from 'restana';
|
||||||
|
import { trackPoi } from '../../services/tracking/Tracker.js';
|
||||||
|
import { TRACKING_POIS } from '../../TRACKING_POIS.js';
|
||||||
|
import logger from '../../services/logger.js';
|
||||||
|
|
||||||
|
const service = restana();
|
||||||
|
const trackingRouter = service.newRouter();
|
||||||
|
|
||||||
|
trackingRouter.get('/trackingPois', async (req, res) => {
|
||||||
|
res.body = TRACKING_POIS;
|
||||||
|
res.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
trackingRouter.post('/poi', async (req, res) => {
|
||||||
|
const { poi } = req.body;
|
||||||
|
if (!poi) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.send({ error: 'Feature name is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await trackPoi(poi);
|
||||||
|
res.send({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error tracking feature', error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { trackingRouter };
|
||||||
@@ -10,8 +10,8 @@ import { resetGeocoordinatesAndDistanceForUser } from '../../services/storage/li
|
|||||||
import { geocodeAddress } from '../../services/geocoding/geoCodingService.js';
|
import { geocodeAddress } from '../../services/geocoding/geoCodingService.js';
|
||||||
import { autocompleteAddress } from '../../services/geocoding/autocompleteService.js';
|
import { autocompleteAddress } from '../../services/geocoding/autocompleteService.js';
|
||||||
import { fromJson } from '../../utils.js';
|
import { fromJson } from '../../utils.js';
|
||||||
import { trackFeature } from '../../services/tracking/Tracker.js';
|
import { trackPoi } from '../../services/tracking/Tracker.js';
|
||||||
import { FEATURES } from '../../features.js';
|
import { TRACKING_POIS } from '../../TRACKING_POIS.js';
|
||||||
import logger from '../../services/logger.js';
|
import logger from '../../services/logger.js';
|
||||||
import { runGeoCordTask } from '../../services/crons/geocoding-cron.js';
|
import { runGeoCordTask } from '../../services/crons/geocoding-cron.js';
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ userSettingsRouter.post('/home-address', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (home_address) {
|
if (home_address) {
|
||||||
await trackFeature(FEATURES.DISTANCE_ADDRESS_ENTERED);
|
await trackPoi(TRACKING_POIS.DISTANCE_ADDRESS_ENTERED);
|
||||||
const coords = await geocodeAddress(home_address);
|
const coords = await geocodeAddress(home_address);
|
||||||
if (coords && coords.lat !== -1) {
|
if (coords && coords.lat !== -1) {
|
||||||
upsertSettings({ home_address: { address: home_address, coords } }, userId);
|
upsertSettings({ home_address: { address: home_address, coords } }, userId);
|
||||||
@@ -76,4 +76,46 @@ userSettingsRouter.post('/home-address', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
userSettingsRouter.post('/news-hash', async (req, res) => {
|
||||||
|
const userId = req.session.currentUser;
|
||||||
|
const { news_hash } = req.body;
|
||||||
|
|
||||||
|
const globalSettings = await getSettings();
|
||||||
|
if (globalSettings.demoMode) {
|
||||||
|
res.statusCode = 403;
|
||||||
|
res.send({ error: 'In demo mode, it is not allowed to change settings.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
upsertSettings({ news_hash }, userId);
|
||||||
|
res.send({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating news hash', error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
userSettingsRouter.post('/immoscout-details', async (req, res) => {
|
||||||
|
const userId = req.session.currentUser;
|
||||||
|
const { immoscout_details } = req.body;
|
||||||
|
|
||||||
|
const globalSettings = await getSettings();
|
||||||
|
if (globalSettings.demoMode) {
|
||||||
|
res.statusCode = 403;
|
||||||
|
res.send({ error: 'In demo mode, it is not allowed to change settings.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
upsertSettings({ immoscout_details: !!immoscout_details }, userId);
|
||||||
|
res.send({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating immoscout details setting', error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.send({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export { userSettingsRouter };
|
export { userSettingsRouter };
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const mapListing = (listing) => ({
|
|||||||
url: listing.link,
|
url: listing.link,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const send = ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
export const send = async ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
const { authToken, endpointUrl } = notificationConfig.find((a) => a.id === config.id).fields;
|
const { authToken, endpointUrl, selfSignedCerts } = notificationConfig.find((a) => a.id === config.id).fields;
|
||||||
|
|
||||||
const listings = newListings.map(mapListing);
|
const listings = newListings.map(mapListing);
|
||||||
const body = {
|
const body = {
|
||||||
@@ -34,11 +34,20 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
headers['Authorization'] = `Bearer ${authToken}`;
|
headers['Authorization'] = `Bearer ${authToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(endpointUrl, {
|
let fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: headers,
|
headers,
|
||||||
|
timeout: 10000,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (selfSignedCerts === true) {
|
||||||
|
fetchOptions.dispatcher = new (await import('undici')).Agent({
|
||||||
|
connect: { rejectUnauthorized: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(endpointUrl, fetchOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -52,6 +61,10 @@ export const config = {
|
|||||||
label: 'Endpoint URL',
|
label: 'Endpoint URL',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
selfSignedCerts: {
|
||||||
|
label: 'Self-signed certificates',
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
authToken: {
|
authToken: {
|
||||||
description: "Your application's auth token, if required by your endpoint.",
|
description: "Your application's auth token, if required by your endpoint.",
|
||||||
label: 'Auth token (optional)',
|
label: 'Auth token (optional)',
|
||||||
|
|||||||
88
lib/notification/adapter/resend.js
Executable file
88
lib/notification/adapter/resend.js
Executable file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Resend } from 'resend';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import Handlebars from 'handlebars';
|
||||||
|
import { markdown2Html } from '../../services/markdown.js';
|
||||||
|
import { getDirName, normalizeImageUrl } from '../../utils.js';
|
||||||
|
|
||||||
|
const __dirname = getDirName();
|
||||||
|
const template = fs.readFileSync(path.resolve(__dirname + '/notification/emailTemplate/template.hbs'), 'utf8');
|
||||||
|
const emailTemplate = Handlebars.compile(template);
|
||||||
|
|
||||||
|
const mapListings = (serviceName, jobKey, listings) =>
|
||||||
|
listings.map((l) => {
|
||||||
|
const image = normalizeImageUrl(l.image);
|
||||||
|
return {
|
||||||
|
title: l.title || '',
|
||||||
|
link: l.link || '',
|
||||||
|
address: l.address || '',
|
||||||
|
size: l.size || '',
|
||||||
|
price: l.price || '',
|
||||||
|
image,
|
||||||
|
hasImage: Boolean(image),
|
||||||
|
serviceName,
|
||||||
|
jobKey,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const send = async ({ serviceName, newListings, notificationConfig, jobKey }) => {
|
||||||
|
const { apiKey, receiver, from } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
|
|
||||||
|
const to = receiver
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const resend = new Resend(apiKey);
|
||||||
|
|
||||||
|
const listings = mapListings(serviceName, jobKey, newListings);
|
||||||
|
|
||||||
|
const html = emailTemplate({
|
||||||
|
serviceName: `Job: (${jobKey}) | Service: ${serviceName}`,
|
||||||
|
numberOfListings: listings.length,
|
||||||
|
listings,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { error } = await resend.emails.send({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
subject: `Fredy found ${listings.length} new listing(s) for ${serviceName}`,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return Promise.reject(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
id: 'resend',
|
||||||
|
name: 'Resend',
|
||||||
|
description: 'Resend is being used to send new listings via mail.',
|
||||||
|
readme: markdown2Html('lib/notification/adapter/resend.md'),
|
||||||
|
fields: {
|
||||||
|
apiKey: {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Api Key',
|
||||||
|
description: 'The Resend API key used to send emails.',
|
||||||
|
},
|
||||||
|
receiver: {
|
||||||
|
type: 'email',
|
||||||
|
label: 'Receiver Email',
|
||||||
|
description: 'Comma-separated email addresses Fredy will send notifications to.',
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
type: 'email',
|
||||||
|
label: 'Sender Email',
|
||||||
|
description: 'The verified email address or domain you send from in Resend.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
17
lib/notification/adapter/resend.md
Normal file
17
lib/notification/adapter/resend.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
### Resend Adapter
|
||||||
|
|
||||||
|
Resend is a modern email delivery service that Fredy can use to send notifications.
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
- Create a Resend account: https://resend.com/
|
||||||
|
- Create an API key and add it to Fredy's configuration.
|
||||||
|
- Choose the sender address (e.g., you@yourdomain.com). Verify the domain (https://resend.com/domains/) in Resend before using it.
|
||||||
|
- Optional for local testing: you can use `onboarding@resend.dev`, but Resend may restrict who you can send to when using test domains.
|
||||||
|
|
||||||
|
Multiple recipients:
|
||||||
|
- Separate email addresses with commas (e.g., some@email.com, someOther@email.com).
|
||||||
|
|
||||||
|
Notes & Troubleshooting:
|
||||||
|
- Ensure the `from` address is verified or belongs to a verified domain in Resend.
|
||||||
|
- If emails don't arrive, check your spam folder and Resend dashboard logs.
|
||||||
|
- The template displays listing images via their public URLs; make sure images are reachable.
|
||||||
@@ -9,7 +9,9 @@ import checkIfListingIsActive from '../services/listings/listingActiveTester.js'
|
|||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function shortenLink(link) {
|
function shortenLink(link) {
|
||||||
return link.substring(0, link.indexOf('?'));
|
if (!link) return '';
|
||||||
|
const index = link.indexOf('?');
|
||||||
|
return index === -1 ? link : link.substring(0, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseId(shortenedLink) {
|
function parseId(shortenedLink) {
|
||||||
@@ -23,7 +25,7 @@ function normalize(o) {
|
|||||||
const title = o.title || 'No title available';
|
const title = o.title || 'No title available';
|
||||||
const address = o.address || null;
|
const address = o.address || null;
|
||||||
const shortLink = shortenLink(o.link);
|
const shortLink = shortenLink(o.link);
|
||||||
const link = `${baseUrl}/${shortLink}`;
|
const link = baseUrl + shortLink;
|
||||||
const image = baseUrl + o.image;
|
const image = baseUrl + o.image;
|
||||||
const id = buildHash(parseId(shortLink), o.price);
|
const id = buildHash(parseId(shortLink), o.price);
|
||||||
return Object.assign(o, { id, price, size, title, address, link, image });
|
return Object.assign(o, { id, price, size, title, address, link, image });
|
||||||
@@ -37,18 +39,18 @@ function applyBlacklist(o) {
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '._ref',
|
crawlContainer: 'a:has(div.list_entry)',
|
||||||
sortByDateParam: 'sort_col=*created_ts&sort_dir=desc',
|
sortByDateParam: 'sort_col=*created_ts&sort_dir=desc',
|
||||||
waitForSelector: 'body',
|
waitForSelector: 'body',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '@href', //will be transformed later
|
id: '@href', //will be transformed later
|
||||||
price: '.list_entry .immo_preis .label_info',
|
price: '.immo_preis .label_info',
|
||||||
size: '.list_entry .flaeche .label_info | removeNewline | trim',
|
size: '.flaeche .label_info | removeNewline | trim',
|
||||||
title: '.list_entry .part_text h3 span',
|
title: 'h3 span',
|
||||||
description: '.list_entry .description | trim',
|
description: '.description | trim',
|
||||||
link: '@href',
|
link: '@href',
|
||||||
address: '.list_entry .place',
|
address: '.place',
|
||||||
image: '.list_entry img@src',
|
image: 'img@src',
|
||||||
},
|
},
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2026 by Christian Kellner.
|
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { isOneOf, buildHash } from '../utils.js';
|
|
||||||
import checkIfListingIsActive from '../services/listings/listingActiveTester.js';
|
|
||||||
let appliedBlackList = [];
|
|
||||||
|
|
||||||
function normalize(o) {
|
|
||||||
const size = o.size != null ? o.size.replace('Wohnfläche ', '') : 'N/A m²';
|
|
||||||
const price = o.price.replace('Kaufpreis ', '');
|
|
||||||
const address = o.address?.split(' • ')?.pop() ?? null;
|
|
||||||
const title = o.title || 'No title available';
|
|
||||||
const link = o.link != null ? decodeURIComponent(o.link) : config.url;
|
|
||||||
const id = buildHash(title, price);
|
|
||||||
return Object.assign(o, { id, address, price, size, title, link });
|
|
||||||
}
|
|
||||||
function applyBlacklist(o) {
|
|
||||||
const titleNotBlacklisted = !isOneOf(o.title, appliedBlackList);
|
|
||||||
const descNotBlacklisted = !isOneOf(o.description, appliedBlackList);
|
|
||||||
return titleNotBlacklisted && descNotBlacklisted;
|
|
||||||
}
|
|
||||||
const config = {
|
|
||||||
url: null,
|
|
||||||
crawlContainer: 'div[data-testid="serp-core-classified-card-testid"]',
|
|
||||||
sortByDateParam: 'sortby=19',
|
|
||||||
waitForSelector: 'div[data-testid="serp-gridcontainer-testid"]',
|
|
||||||
crawlFields: {
|
|
||||||
id: 'button@title |trim',
|
|
||||||
title: 'button@title |trim',
|
|
||||||
price: 'div[data-testid="cardmfe-price-testid"] | trim',
|
|
||||||
size: 'div[data-testid="cardmfe-keyfacts-testid"] | trim',
|
|
||||||
address: 'div[data-testid="cardmfe-description-box-address"] | trim',
|
|
||||||
image: 'div[data-testid="cardmfe-picture-box-test-id"] img@src',
|
|
||||||
link: 'button@data-base',
|
|
||||||
description: 'div[data-testid="cardmfe-description-text-test-id"] | trim',
|
|
||||||
},
|
|
||||||
normalize: normalize,
|
|
||||||
filter: applyBlacklist,
|
|
||||||
activeTester: checkIfListingIsActive,
|
|
||||||
};
|
|
||||||
export const init = (sourceConfig, blacklist) => {
|
|
||||||
config.enabled = sourceConfig.enabled;
|
|
||||||
config.url = sourceConfig.url;
|
|
||||||
appliedBlackList = blacklist || [];
|
|
||||||
};
|
|
||||||
export const metaInformation = {
|
|
||||||
name: 'Immonet',
|
|
||||||
baseUrl: 'https://www.immonet.de/',
|
|
||||||
id: 'immonet',
|
|
||||||
};
|
|
||||||
export { config };
|
|
||||||
@@ -46,7 +46,9 @@ import {
|
|||||||
convertWebToMobile,
|
convertWebToMobile,
|
||||||
} from '../services/immoscout/immoscout-web-translator.js';
|
} from '../services/immoscout/immoscout-web-translator.js';
|
||||||
import logger from '../services/logger.js';
|
import logger from '../services/logger.js';
|
||||||
|
import { getUserSettings } from '../services/storage/settingsStorage.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
let currentUserId = null;
|
||||||
|
|
||||||
async function getListings(url) {
|
async function getListings(url) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -66,23 +68,86 @@ async function getListings(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responseBody = await response.json();
|
const responseBody = await response.json();
|
||||||
return responseBody.resultListItems
|
return Promise.all(
|
||||||
.filter((item) => item.type === 'EXPOSE_RESULT')
|
responseBody.resultListItems
|
||||||
.map((expose) => {
|
.filter((item) => item.type === 'EXPOSE_RESULT')
|
||||||
const item = expose.item;
|
.map(async (expose) => {
|
||||||
const [price, size] = item.attributes;
|
const item = expose.item;
|
||||||
const image = item?.titlePicture?.preview ?? null;
|
const [price, size] = item.attributes;
|
||||||
return {
|
const image = item?.titlePicture?.full ?? item?.titlePicture?.preview ?? null;
|
||||||
id: item.id,
|
let listing = {
|
||||||
price: price?.value,
|
id: item.id,
|
||||||
size: size?.value,
|
price: price?.value,
|
||||||
title: item.title,
|
size: size?.value,
|
||||||
description: item.description,
|
title: item.title,
|
||||||
link: `${metaInformation.baseUrl}expose/${item.id}`,
|
link: `${metaInformation.baseUrl}expose/${item.id}`,
|
||||||
address: item.address?.line,
|
address: item.address?.line,
|
||||||
image,
|
image,
|
||||||
};
|
};
|
||||||
});
|
if (currentUserId) {
|
||||||
|
const userSettings = getUserSettings(currentUserId);
|
||||||
|
if (userSettings.immoscout_details) {
|
||||||
|
return await pushDetails(listing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listing;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushDetails(listing) {
|
||||||
|
const detailed = await fetch(`https://api.mobile.immobilienscout24.de/expose/${listing.id}`, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'ImmoScout_27.3_26.0_._',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!detailed.ok) {
|
||||||
|
logger.error('Error fetching listing details from ImmoScout Mobile API:', detailed.statusText);
|
||||||
|
return listing;
|
||||||
|
}
|
||||||
|
const detailBody = await detailed.json();
|
||||||
|
|
||||||
|
listing.description = buildDescription(detailBody);
|
||||||
|
|
||||||
|
return listing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDescription(detailBody) {
|
||||||
|
const sections = detailBody.sections || [];
|
||||||
|
const contact = detailBody.contact || {};
|
||||||
|
const cData = contact?.contactData || {};
|
||||||
|
const agentName = cData?.agent?.name || '';
|
||||||
|
const agentCompany = cData?.agent?.company || '';
|
||||||
|
const stars = cData?.agent?.rating?.numberOfStars || '';
|
||||||
|
const phoneNumbers = contact?.phoneNumbers || [];
|
||||||
|
const phoneNumbersMapped = phoneNumbers
|
||||||
|
.map((p) => `${p.label}: ${p.text}`)
|
||||||
|
.join('\n')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const attributes = sections
|
||||||
|
.filter((s) => s.type === 'ATTRIBUTE_LIST')
|
||||||
|
.flatMap((s) => s.attributes)
|
||||||
|
.filter((attr) => attr.label && attr.text)
|
||||||
|
.map((attr) => `${attr.label} ${attr.text}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const freeText = sections
|
||||||
|
.filter((s) => s.type === 'TEXT_AREA')
|
||||||
|
.map((s) => {
|
||||||
|
return `${s.title}\n${s.text}`;
|
||||||
|
})
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
return (
|
||||||
|
`Agent: ${agentName ? agentName : 'Unbekannt'} ${agentCompany ? `(${agentCompany}) ` : ''}${stars ? `- ${stars} stars` : ''}\n` +
|
||||||
|
(phoneNumbersMapped ? `Phone Numbers:\n${phoneNumbersMapped}` : '') +
|
||||||
|
'\n\n' +
|
||||||
|
attributes.trim() +
|
||||||
|
'\n\n' +
|
||||||
|
freeText.trim()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isListingActive(link) {
|
async function isListingActive(link) {
|
||||||
@@ -137,6 +202,7 @@ export const init = (sourceConfig, blacklist) => {
|
|||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = convertWebToMobile(sourceConfig.url);
|
config.url = convertWebToMobile(sourceConfig.url);
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
|
currentUserId = sourceConfig.userId || null;
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Immoscout',
|
name: 'Immoscout',
|
||||||
|
|||||||
@@ -19,52 +19,80 @@ import path from 'path';
|
|||||||
|
|
||||||
puppeteer.use(StealthPlugin());
|
puppeteer.use(StealthPlugin());
|
||||||
|
|
||||||
export default async function execute(url, waitForSelector, options) {
|
export async function launchBrowser(url, options) {
|
||||||
let browser;
|
const preCfg = getPreLaunchConfig(url, options || {});
|
||||||
let page;
|
const launchArgs = [
|
||||||
let result;
|
'--no-sandbox',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-crash-reporter',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-default-browser-check',
|
||||||
|
preCfg.langArg,
|
||||||
|
preCfg.windowSizeArg,
|
||||||
|
...preCfg.extraArgs,
|
||||||
|
];
|
||||||
|
if (options?.proxyUrl) {
|
||||||
|
launchArgs.push(`--proxy-server=${options.proxyUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
let userDataDir;
|
let userDataDir;
|
||||||
let removeUserDataDir = false;
|
let removeUserDataDir = false;
|
||||||
|
if (options && options.userDataDir) {
|
||||||
|
userDataDir = options.userDataDir;
|
||||||
|
} else {
|
||||||
|
const prefix = path.join(os.tmpdir(), 'puppeteer-fredy-');
|
||||||
|
userDataDir = fs.mkdtempSync(prefix);
|
||||||
|
removeUserDataDir = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: options?.puppeteerHeadless ?? true,
|
||||||
|
args: launchArgs,
|
||||||
|
timeout: options?.puppeteerTimeout || 45_000,
|
||||||
|
userDataDir,
|
||||||
|
executablePath: options?.executablePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.__fredy_userDataDir = userDataDir;
|
||||||
|
browser.__fredy_removeUserDataDir = removeUserDataDir;
|
||||||
|
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeBrowser(browser) {
|
||||||
|
if (!browser) return;
|
||||||
|
const userDataDir = browser.__fredy_userDataDir;
|
||||||
|
const removeUserDataDir = browser.__fredy_removeUserDataDir;
|
||||||
|
try {
|
||||||
|
await browser.close();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
if (removeUserDataDir && userDataDir) {
|
||||||
|
try {
|
||||||
|
await fs.promises.rm(userDataDir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function execute(url, waitForSelector, options) {
|
||||||
|
let browser = options?.browser;
|
||||||
|
let isExternalBrowser = !!browser;
|
||||||
|
let page;
|
||||||
|
let result;
|
||||||
try {
|
try {
|
||||||
debug(`Sending request to ${url} using Puppeteer.`);
|
debug(`Sending request to ${url} using Puppeteer.`);
|
||||||
|
|
||||||
// Prepare a dedicated temporary userDataDir to avoid leaking /tmp/.org.chromium.* dirs
|
if (!isExternalBrowser) {
|
||||||
if (options && options.userDataDir) {
|
browser = await launchBrowser(url, options);
|
||||||
userDataDir = options.userDataDir;
|
|
||||||
removeUserDataDir = !!options.cleanupUserDataDir;
|
|
||||||
} else {
|
|
||||||
const prefix = path.join(os.tmpdir(), 'puppeteer-fredy-');
|
|
||||||
userDataDir = fs.mkdtempSync(prefix);
|
|
||||||
removeUserDataDir = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchArgs = [
|
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-gpu',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
'--disable-dev-shm-usage',
|
|
||||||
'--disable-crash-reporter',
|
|
||||||
'--no-first-run',
|
|
||||||
'--no-default-browser-check',
|
|
||||||
];
|
|
||||||
if (options?.proxyUrl) {
|
|
||||||
launchArgs.push(`--proxy-server=${options.proxyUrl}`);
|
|
||||||
}
|
|
||||||
// Prepare bot prevention pre-launch config
|
|
||||||
const preCfg = getPreLaunchConfig(url, options || {});
|
|
||||||
launchArgs.push(preCfg.langArg);
|
|
||||||
launchArgs.push(preCfg.windowSizeArg);
|
|
||||||
launchArgs.push(...preCfg.extraArgs);
|
|
||||||
|
|
||||||
browser = await puppeteer.launch({
|
|
||||||
headless: options?.puppeteerHeadless ?? true,
|
|
||||||
args: launchArgs,
|
|
||||||
timeout: options?.puppeteerTimeout || 30_000,
|
|
||||||
userDataDir,
|
|
||||||
executablePath: options?.executablePath, // allow using system Chrome
|
|
||||||
});
|
|
||||||
|
|
||||||
page = await browser.newPage();
|
page = await browser.newPage();
|
||||||
|
const preCfg = getPreLaunchConfig(url, options || {});
|
||||||
await applyBotPreventionToPage(page, preCfg);
|
await applyBotPreventionToPage(page, preCfg);
|
||||||
// Provide languages value before navigation
|
// Provide languages value before navigation
|
||||||
await applyLanguagePersistence(page, preCfg);
|
await applyLanguagePersistence(page, preCfg);
|
||||||
@@ -104,7 +132,7 @@ export default async function execute(url, waitForSelector, options) {
|
|||||||
result = pageSource || (await page.content());
|
result = pageSource || (await page.content());
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.message?.includes('Timeout')) {
|
if (error?.name?.includes('Timeout')) {
|
||||||
logger.debug('Error executing with puppeteer executor', error);
|
logger.debug('Error executing with puppeteer executor', error);
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Error executing with puppeteer executor', error);
|
logger.warn('Error executing with puppeteer executor', error);
|
||||||
@@ -118,19 +146,8 @@ export default async function execute(url, waitForSelector, options) {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
try {
|
if (browser != null && !isExternalBrowser) {
|
||||||
if (browser != null) {
|
await closeBrowser(browser);
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (removeUserDataDir && userDataDir) {
|
|
||||||
await fs.promises.rm(userDataDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ async function doGeocode(address) {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
agent,
|
agent,
|
||||||
|
timeout: 60000,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': userAgent,
|
'User-Agent': userAgent,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const PARAM_NAME_MAP = {
|
|||||||
shape: 'shape',
|
shape: 'shape',
|
||||||
sorting: 'sorting',
|
sorting: 'sorting',
|
||||||
newbuilding: 'newbuilding',
|
newbuilding: 'newbuilding',
|
||||||
|
fulltext: 'fulltext',
|
||||||
};
|
};
|
||||||
|
|
||||||
const EQUIPMENT_MAP = {
|
const EQUIPMENT_MAP = {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import FredyPipelineExecutioner from '../../FredyPipelineExecutioner.js';
|
|||||||
import * as similarityCache from '../similarity-check/similarityCache.js';
|
import * as similarityCache from '../similarity-check/similarityCache.js';
|
||||||
import { isRunning, markFinished, markRunning } from './run-state.js';
|
import { isRunning, markFinished, markRunning } from './run-state.js';
|
||||||
import { sendToUsers } from '../sse/sse-broker.js';
|
import { sendToUsers } from '../sse/sse-broker.js';
|
||||||
|
import * as puppeteerExtractor from '../extractor/puppeteerExtractor.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the job execution service.
|
* Initializes the job execution service.
|
||||||
@@ -94,7 +95,7 @@ export function initJobExecutionService({ providers, settings, intervalMs }) {
|
|||||||
* @param {{userId?: string, isAdmin?: boolean}} [context] - Who requested the run; determines job filtering.
|
* @param {{userId?: string, isAdmin?: boolean}} [context] - Who requested the run; determines job filtering.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function runAll(respectWorkingHours = true, context = undefined) {
|
async function runAll(respectWorkingHours = true, context = undefined) {
|
||||||
if (settings.demoMode) return;
|
if (settings.demoMode) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const withinHours = duringWorkingHoursOrNotSet(settings, now);
|
const withinHours = duringWorkingHoursOrNotSet(settings, now);
|
||||||
@@ -103,15 +104,18 @@ export function initJobExecutionService({ providers, settings, intervalMs }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.lastRun = now;
|
settings.lastRun = now;
|
||||||
jobStorage
|
const jobs = jobStorage
|
||||||
.getJobs()
|
.getJobs()
|
||||||
.filter((job) => job.enabled)
|
.filter((job) => job.enabled)
|
||||||
.filter((job) => {
|
.filter((job) => {
|
||||||
if (!context) return true; // startup/cron → all
|
if (!context) return true; // startup/cron → all
|
||||||
if (context.isAdmin) return true; // admin → all
|
if (context.isAdmin) return true; // admin → all
|
||||||
return context.userId ? job.userId === context.userId : false; // user → own
|
return context.userId ? job.userId === context.userId : false; // user → own
|
||||||
})
|
});
|
||||||
.forEach((job) => executeJob(job));
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
await executeJob(job);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,28 +158,43 @@ export function initJobExecutionService({ providers, settings, intervalMs }) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Failed to emit start status for job', job.id, err);
|
logger.warn('Failed to emit start status for job', job.id, err);
|
||||||
}
|
}
|
||||||
|
let browser;
|
||||||
try {
|
try {
|
||||||
const jobProviders = job.provider.filter(
|
const jobProviders = job.provider.filter(
|
||||||
(p) => providers.find((loaded) => loaded.metaInformation.id === p.id) != null,
|
(p) => providers.find((loaded) => loaded.metaInformation.id === p.id) != null,
|
||||||
);
|
);
|
||||||
const executions = jobProviders.map(async (prov) => {
|
for (const prov of jobProviders) {
|
||||||
const matchedProvider = providers.find((loaded) => loaded.metaInformation.id === prov.id);
|
try {
|
||||||
matchedProvider.init(prov, job.blacklist);
|
const matchedProvider = providers.find((loaded) => loaded.metaInformation.id === prov.id);
|
||||||
await new FredyPipelineExecutioner(
|
matchedProvider.init({ ...prov, userId: job.userId }, job.blacklist);
|
||||||
matchedProvider.config,
|
|
||||||
job.notificationAdapter,
|
if (browser && !browser.isConnected()) {
|
||||||
prov.id,
|
logger.debug('Browser is disconnected, nullifying to launch a new one.');
|
||||||
job.id,
|
await puppeteerExtractor.closeBrowser(browser);
|
||||||
similarityCache,
|
browser = null;
|
||||||
).execute();
|
}
|
||||||
});
|
|
||||||
const results = await Promise.allSettled(executions);
|
if (!browser && matchedProvider.config.getListings == null) {
|
||||||
for (const r of results) {
|
browser = await puppeteerExtractor.launchBrowser(matchedProvider.config.url, {});
|
||||||
if (r.status === 'rejected') {
|
}
|
||||||
logger.error(r.reason);
|
|
||||||
|
await new FredyPipelineExecutioner(
|
||||||
|
matchedProvider.config,
|
||||||
|
job.notificationAdapter,
|
||||||
|
job.spatialFilter,
|
||||||
|
prov.id,
|
||||||
|
job.id,
|
||||||
|
similarityCache,
|
||||||
|
browser,
|
||||||
|
).execute();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (browser) {
|
||||||
|
await puppeteerExtractor.closeBrowser(browser);
|
||||||
|
}
|
||||||
markFinished(job.id);
|
markFinished(job.id);
|
||||||
try {
|
try {
|
||||||
bus.emit('jobs:status', { jobId: job.id, running: false });
|
bus.emit('jobs:status', { jobId: job.id, running: false });
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const upsertJob = ({
|
|||||||
notificationAdapter,
|
notificationAdapter,
|
||||||
userId,
|
userId,
|
||||||
shareWithUsers = [],
|
shareWithUsers = [],
|
||||||
|
spatialFilter = null,
|
||||||
}) => {
|
}) => {
|
||||||
const id = jobId || nanoid();
|
const id = jobId || nanoid();
|
||||||
const existing = SqliteConnection.query(`SELECT id, user_id FROM jobs WHERE id = @id LIMIT 1`, { id })[0];
|
const existing = SqliteConnection.query(`SELECT id, user_id FROM jobs WHERE id = @id LIMIT 1`, { id })[0];
|
||||||
@@ -42,7 +43,8 @@ export const upsertJob = ({
|
|||||||
blacklist = @blacklist,
|
blacklist = @blacklist,
|
||||||
provider = @provider,
|
provider = @provider,
|
||||||
notification_adapter = @notification_adapter,
|
notification_adapter = @notification_adapter,
|
||||||
shared_with_user = @shareWithUsers
|
shared_with_user = @shareWithUsers,
|
||||||
|
spatial_filter = @spatialFilter
|
||||||
WHERE id = @id`,
|
WHERE id = @id`,
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
@@ -52,12 +54,13 @@ export const upsertJob = ({
|
|||||||
shareWithUsers: toJson(shareWithUsers ?? []),
|
shareWithUsers: toJson(shareWithUsers ?? []),
|
||||||
provider: toJson(provider ?? []),
|
provider: toJson(provider ?? []),
|
||||||
notification_adapter: toJson(notificationAdapter ?? []),
|
notification_adapter: toJson(notificationAdapter ?? []),
|
||||||
|
spatialFilter: spatialFilter ? toJson(spatialFilter) : null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
SqliteConnection.execute(
|
SqliteConnection.execute(
|
||||||
`INSERT INTO jobs (id, user_id, enabled, name, blacklist, provider, notification_adapter, shared_with_user)
|
`INSERT INTO jobs (id, user_id, enabled, name, blacklist, provider, notification_adapter, shared_with_user, spatial_filter)
|
||||||
VALUES (@id, @user_id, @enabled, @name, @blacklist, @provider, @notification_adapter, @shareWithUsers)`,
|
VALUES (@id, @user_id, @enabled, @name, @blacklist, @provider, @notification_adapter, @shareWithUsers, @spatialFilter)`,
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
user_id: ownerId,
|
user_id: ownerId,
|
||||||
@@ -67,6 +70,7 @@ export const upsertJob = ({
|
|||||||
provider: toJson(provider ?? []),
|
provider: toJson(provider ?? []),
|
||||||
shareWithUsers: toJson(shareWithUsers ?? []),
|
shareWithUsers: toJson(shareWithUsers ?? []),
|
||||||
notification_adapter: toJson(notificationAdapter ?? []),
|
notification_adapter: toJson(notificationAdapter ?? []),
|
||||||
|
spatialFilter: spatialFilter ? toJson(spatialFilter) : null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,6 +91,7 @@ export const getJob = (jobId) => {
|
|||||||
j.provider,
|
j.provider,
|
||||||
j.shared_with_user,
|
j.shared_with_user,
|
||||||
j.notification_adapter AS notificationAdapter,
|
j.notification_adapter AS notificationAdapter,
|
||||||
|
j.spatial_filter AS spatialFilter,
|
||||||
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings
|
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings
|
||||||
FROM jobs j
|
FROM jobs j
|
||||||
WHERE j.id = @id
|
WHERE j.id = @id
|
||||||
@@ -101,6 +106,7 @@ export const getJob = (jobId) => {
|
|||||||
provider: fromJson(row.provider, []),
|
provider: fromJson(row.provider, []),
|
||||||
shared_with_user: fromJson(row.shared_with_user, []),
|
shared_with_user: fromJson(row.shared_with_user, []),
|
||||||
notificationAdapter: fromJson(row.notificationAdapter, []),
|
notificationAdapter: fromJson(row.notificationAdapter, []),
|
||||||
|
spatialFilter: fromJson(row.spatialFilter, null),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,6 +156,7 @@ export const getJobs = () => {
|
|||||||
j.provider,
|
j.provider,
|
||||||
j.shared_with_user,
|
j.shared_with_user,
|
||||||
j.notification_adapter AS notificationAdapter,
|
j.notification_adapter AS notificationAdapter,
|
||||||
|
j.spatial_filter AS spatialFilter,
|
||||||
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings
|
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings
|
||||||
FROM jobs j
|
FROM jobs j
|
||||||
WHERE j.enabled = 1
|
WHERE j.enabled = 1
|
||||||
@@ -162,6 +169,7 @@ export const getJobs = () => {
|
|||||||
provider: fromJson(row.provider, []),
|
provider: fromJson(row.provider, []),
|
||||||
shared_with_user: fromJson(row.shared_with_user, []),
|
shared_with_user: fromJson(row.shared_with_user, []),
|
||||||
notificationAdapter: fromJson(row.notificationAdapter, []),
|
notificationAdapter: fromJson(row.notificationAdapter, []),
|
||||||
|
spatialFilter: fromJson(row.spatialFilter, null),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,6 +259,7 @@ export const queryJobs = ({
|
|||||||
j.provider,
|
j.provider,
|
||||||
j.shared_with_user,
|
j.shared_with_user,
|
||||||
j.notification_adapter AS notificationAdapter,
|
j.notification_adapter AS notificationAdapter,
|
||||||
|
j.spatial_filter AS spatialFilter,
|
||||||
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings
|
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings
|
||||||
FROM jobs j
|
FROM jobs j
|
||||||
${whereSql}
|
${whereSql}
|
||||||
@@ -266,6 +275,7 @@ export const queryJobs = ({
|
|||||||
provider: fromJson(row.provider, []),
|
provider: fromJson(row.provider, []),
|
||||||
shared_with_user: fromJson(row.shared_with_user, []),
|
shared_with_user: fromJson(row.shared_with_user, []),
|
||||||
notificationAdapter: fromJson(row.notificationAdapter, []),
|
notificationAdapter: fromJson(row.notificationAdapter, []),
|
||||||
|
spatialFilter: fromJson(row.spatialFilter, null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { totalNumber, page: safePage, result };
|
return { totalNumber, page: safePage, result };
|
||||||
|
|||||||
11
lib/services/storage/migrations/sql/11.add-spatial-filter.js
Normal file
11
lib/services/storage/migrations/sql/11.add-spatial-filter.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Migration: Add spatial_filter column to jobs table for storing GeoJSON-based spatial filters
|
||||||
|
export function up(db) {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE jobs ADD COLUMN spatial_filter JSONB DEFAULT NULL;
|
||||||
|
`);
|
||||||
|
}
|
||||||
@@ -70,11 +70,11 @@ export const trackMainEvent = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trackFeature = async (feature) => {
|
export const trackPoi = async (poi) => {
|
||||||
if (!(await shouldTrack())) return;
|
if (!(await shouldTrack())) return;
|
||||||
|
|
||||||
const trackingObj = await enrichTrackingObject({
|
const trackingObj = await enrichTrackingObject({
|
||||||
feature,
|
feature: poi,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendTrackingData('/feature', trackingObj);
|
await sendTrackingData('/feature', trackingObj);
|
||||||
|
|||||||
13553
package-lock.json
generated
13553
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "19.3.9",
|
"version": "19.6.0",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@@ -60,9 +60,10 @@
|
|||||||
"Firefox ESR"
|
"Firefox ESR"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@douyinfe/semi-icons": "^2.91.0",
|
"@douyinfe/semi-icons": "^2.92.2",
|
||||||
"@douyinfe/semi-ui": "2.91.0",
|
"@douyinfe/semi-ui": "2.92.2",
|
||||||
"@douyinfe/semi-ui-19": "^2.91.0",
|
"@douyinfe/semi-ui-19": "^2.92.2",
|
||||||
|
"@mapbox/mapbox-gl-draw": "^1.5.1",
|
||||||
"@sendgrid/mail": "8.1.6",
|
"@sendgrid/mail": "8.1.6",
|
||||||
"@vitejs/plugin-react": "5.1.4",
|
"@vitejs/plugin-react": "5.1.4",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
@@ -70,17 +71,18 @@
|
|||||||
"body-parser": "2.2.2",
|
"body-parser": "2.2.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
|
"@turf/boolean-point-in-polygon": "^7.3.4",
|
||||||
"cookie-session": "2.1.1",
|
"cookie-session": "2.1.1",
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
"maplibre-gl": "^5.18.0",
|
"maplibre-gl": "^5.19.0",
|
||||||
"nanoid": "5.1.6",
|
"nanoid": "5.1.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-mailjet": "6.0.11",
|
"node-mailjet": "6.0.11",
|
||||||
"p-throttle": "^8.1.0",
|
"p-throttle": "^8.1.0",
|
||||||
"package-up": "^5.0.0",
|
"package-up": "^5.0.0",
|
||||||
"puppeteer": "^24.37.2",
|
"puppeteer": "^24.38.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
"query-string": "9.3.1",
|
"query-string": "9.3.1",
|
||||||
@@ -88,8 +90,9 @@
|
|||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-range-slider-input": "^3.3.2",
|
"react-range-slider-input": "^3.3.2",
|
||||||
"react-router": "7.13.0",
|
"react-router": "7.13.1",
|
||||||
"react-router-dom": "7.13.0",
|
"react-router-dom": "7.13.1",
|
||||||
|
"resend": "^6.9.3",
|
||||||
"restana": "5.1.0",
|
"restana": "5.1.0",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"serve-static": "2.2.1",
|
"serve-static": "2.2.1",
|
||||||
@@ -106,17 +109,17 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"chai": "6.2.2",
|
"chai": "6.2.2",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"eslint": "10.0.0",
|
"eslint": "10.0.3",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"esmock": "2.7.3",
|
"esmock": "2.7.3",
|
||||||
"globals": "^17.3.0",
|
"globals": "^17.4.0",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"less": "4.5.1",
|
"less": "4.5.1",
|
||||||
"lint-staged": "16.2.7",
|
"lint-staged": "16.3.2",
|
||||||
"mocha": "11.7.5",
|
"mocha": "11.7.5",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.14",
|
||||||
"prettier": "3.8.1"
|
"prettier": "3.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,8 @@ export function getUserSettings(userId) {
|
|||||||
export const updateListingDistance = (id, distance) => {
|
export const updateListingDistance = (id, distance) => {
|
||||||
// noop
|
// noop
|
||||||
};
|
};
|
||||||
|
export const deletedIds = [];
|
||||||
|
export const deleteListingsById = (ids) => {
|
||||||
|
deletedIds.push(...ids);
|
||||||
|
};
|
||||||
/* eslint-enable no-unused-vars */
|
/* eslint-enable no-unused-vars */
|
||||||
|
|||||||
89
test/pipeline_filtering.test.js
Normal file
89
test/pipeline_filtering.test.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { mockFredy } from './utils.js';
|
||||||
|
import * as mockStore from './mocks/mockStore.js';
|
||||||
|
|
||||||
|
describe('Issue reproduction: listings filtered by similarity or area should be marked as manually deleted', () => {
|
||||||
|
it('should call deleteListingsById when listings are filtered by similarity', async () => {
|
||||||
|
const Fredy = await mockFredy();
|
||||||
|
|
||||||
|
const mockSimilarityCache = {
|
||||||
|
checkAndAddEntry: () => true, // always similar
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerConfig = {
|
||||||
|
url: 'http://example.com',
|
||||||
|
getListings: () => Promise.resolve([{ id: '1', title: 'test', address: 'addr', price: '100' }]),
|
||||||
|
normalize: (l) => l,
|
||||||
|
filter: () => true,
|
||||||
|
crawlFields: { id: 'id', title: 'title', address: 'address', price: 'price' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fredy = new Fredy(providerConfig, null, null, 'test-provider', 'test-job', mockSimilarityCache);
|
||||||
|
|
||||||
|
// Clear deletedIds before test
|
||||||
|
mockStore.deletedIds.length = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fredy.execute();
|
||||||
|
} catch {
|
||||||
|
// Might throw NoNewListingsWarning if all are filtered out
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockStore.deletedIds).to.include('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call deleteListingsById when listings are filtered by area', async () => {
|
||||||
|
const Fredy = await mockFredy();
|
||||||
|
|
||||||
|
const mockSimilarityCache = {
|
||||||
|
checkAndAddEntry: () => false, // never similar
|
||||||
|
};
|
||||||
|
|
||||||
|
const spatialFilter = {
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [
|
||||||
|
[
|
||||||
|
[0, 0],
|
||||||
|
[0, 1],
|
||||||
|
[1, 1],
|
||||||
|
[1, 0],
|
||||||
|
[0, 0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerConfig = {
|
||||||
|
url: 'http://example.com',
|
||||||
|
getListings: () =>
|
||||||
|
Promise.resolve([{ id: '2', title: 'test', address: 'addr', price: '100', latitude: 2, longitude: 2 }]), // outside polygon
|
||||||
|
normalize: (l) => l,
|
||||||
|
filter: () => true,
|
||||||
|
crawlFields: { id: 'id', title: 'title', address: 'address', price: 'price' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fredy = new Fredy(providerConfig, null, spatialFilter, 'test-provider', 'test-job', mockSimilarityCache);
|
||||||
|
|
||||||
|
// Clear deletedIds before test
|
||||||
|
mockStore.deletedIds.length = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fredy.execute();
|
||||||
|
} catch {
|
||||||
|
// Might throw NoNewListingsWarning if all are filtered out
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockStore.deletedIds).to.include('2');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,7 +14,14 @@ describe('#einsAImmobilien testsuite()', () => {
|
|||||||
it('should test einsAImmobilien provider', async () => {
|
it('should test einsAImmobilien provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'einsAImmobilien', similarityCache);
|
const fredy = new Fredy(
|
||||||
|
provider.config,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
provider.metaInformation.id,
|
||||||
|
'einsAImmobilien',
|
||||||
|
similarityCache,
|
||||||
|
);
|
||||||
fredy.execute().then((listings) => {
|
fredy.execute().then((listings) => {
|
||||||
expect(listings).to.be.a('array');
|
expect(listings).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#immobilien.de testsuite()', () => {
|
|||||||
it('should test immobilien.de provider', async () => {
|
it('should test immobilien.de provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'test1', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'test1', similarityCache);
|
||||||
fredy.execute().then((listing) => {
|
fredy.execute().then((listing) => {
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2026 by Christian Kellner.
|
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
|
||||||
import { get } from '../mocks/mockNotification.js';
|
|
||||||
import { mockFredy, providerConfig } from '../utils.js';
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import * as provider from '../../lib/provider/immonet.js';
|
|
||||||
|
|
||||||
describe('#immonet testsuite()', () => {
|
|
||||||
it('should test immonet provider', async () => {
|
|
||||||
const Fredy = await mockFredy();
|
|
||||||
provider.init(providerConfig.immonet, [], []);
|
|
||||||
|
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immonet', similarityCache);
|
|
||||||
const listing = await fredy.execute();
|
|
||||||
|
|
||||||
expect(listing).to.be.a('array');
|
|
||||||
const notificationObj = get();
|
|
||||||
expect(notificationObj).to.be.a('object');
|
|
||||||
expect(notificationObj.serviceName).to.equal('immonet');
|
|
||||||
notificationObj.payload.forEach((notify) => {
|
|
||||||
/** check the actual structure **/
|
|
||||||
expect(notify.id).to.be.a('string');
|
|
||||||
expect(notify.price).to.be.a('string');
|
|
||||||
expect(notify.size).to.be.a('string');
|
|
||||||
expect(notify.title).to.be.a('string');
|
|
||||||
expect(notify.link).to.be.a('string');
|
|
||||||
expect(notify.address).to.be.a('string');
|
|
||||||
/** check the values if possible **/
|
|
||||||
expect(notify.size).that.does.include('m²');
|
|
||||||
expect(notify.title).to.be.not.empty;
|
|
||||||
expect(notify.address).to.be.not.empty;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -14,7 +14,7 @@ describe('#immoscout provider testsuite()', () => {
|
|||||||
it('should test immoscout provider', async () => {
|
it('should test immoscout provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, '', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, '', similarityCache);
|
||||||
fredy.execute().then((listings) => {
|
fredy.execute().then((listings) => {
|
||||||
expect(listings).to.be.a('array');
|
expect(listings).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#immoswp testsuite()', () => {
|
|||||||
it('should test immoswp provider', async () => {
|
it('should test immoswp provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immoswp', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'immoswp', similarityCache);
|
||||||
fredy.execute().then((listing) => {
|
fredy.execute().then((listing) => {
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#immowelt testsuite()', () => {
|
|||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
provider.init(providerConfig.immowelt, [], []);
|
provider.init(providerConfig.immowelt, [], []);
|
||||||
|
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immowelt', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'immowelt', similarityCache);
|
||||||
const listing = await fredy.execute();
|
const listing = await fredy.execute();
|
||||||
|
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ describe('#kleinanzeigen testsuite()', () => {
|
|||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
provider.init(providerConfig.kleinanzeigen, [], []);
|
provider.init(providerConfig.kleinanzeigen, [], []);
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'kleinanzeigen', similarityCache);
|
const fredy = new Fredy(
|
||||||
|
provider.config,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
provider.metaInformation.id,
|
||||||
|
'kleinanzeigen',
|
||||||
|
similarityCache,
|
||||||
|
);
|
||||||
fredy.execute().then((listing) => {
|
fredy.execute().then((listing) => {
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#mcMakler testsuite()', () => {
|
|||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
provider.init(providerConfig.mcMakler, []);
|
provider.init(providerConfig.mcMakler, []);
|
||||||
|
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'mcMakler', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'mcMakler', similarityCache);
|
||||||
const listing = await fredy.execute();
|
const listing = await fredy.execute();
|
||||||
|
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ describe('#neubauKompass testsuite()', () => {
|
|||||||
it('should test neubauKompass provider', async () => {
|
it('should test neubauKompass provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'neubauKompass', similarityCache);
|
const fredy = new Fredy(
|
||||||
|
provider.config,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
provider.metaInformation.id,
|
||||||
|
'neubauKompass',
|
||||||
|
similarityCache,
|
||||||
|
);
|
||||||
fredy.execute().then((listing) => {
|
fredy.execute().then((listing) => {
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#ohneMakler testsuite()', () => {
|
|||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
provider.init(providerConfig.ohneMakler, []);
|
provider.init(providerConfig.ohneMakler, []);
|
||||||
|
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'ohneMakler', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'ohneMakler', similarityCache);
|
||||||
const listing = await fredy.execute();
|
const listing = await fredy.execute();
|
||||||
|
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('#regionalimmobilien24 testsuite()', () => {
|
|||||||
const fredy = new Fredy(
|
const fredy = new Fredy(
|
||||||
provider.config,
|
provider.config,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
provider.metaInformation.id,
|
provider.metaInformation.id,
|
||||||
'regionalimmobilien24',
|
'regionalimmobilien24',
|
||||||
similarityCache,
|
similarityCache,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#sparkasse testsuite()', () => {
|
|||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
provider.init(providerConfig.sparkasse, []);
|
provider.init(providerConfig.sparkasse, []);
|
||||||
|
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'sparkasse', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'sparkasse', similarityCache);
|
||||||
const listing = await fredy.execute();
|
const listing = await fredy.execute();
|
||||||
|
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
"url": "https://www.immobilien.de/Wohnen/Suchergebnisse-51797.html?search._digest=true&search._filter=wohnen&search.flaeche_von=50&search.objektart=wohnung&search.preis_bis=1200&search.typ=mieten&search.umkreis=15&search.wo=district%3A2434%2C2695%2C2621%2C2700%2C2967%2C2734%2C2909%2C2955%2C2392%2C2746%2C2767%2C2982%2C2904%2C2612%2C2892%2C2587%2C2871%2C2975%2C2591%2C2887%2C2569%2C2640%2C2735&sort_col=*created_ts&sort_dir=desc",
|
"url": "https://www.immobilien.de/Wohnen/Suchergebnisse-51797.html?search._digest=true&search._filter=wohnen&search.flaeche_von=50&search.objektart=wohnung&search.preis_bis=1200&search.typ=mieten&search.umkreis=15&search.wo=district%3A2434%2C2695%2C2621%2C2700%2C2967%2C2734%2C2909%2C2955%2C2392%2C2746%2C2767%2C2982%2C2904%2C2612%2C2892%2C2587%2C2871%2C2975%2C2591%2C2887%2C2569%2C2640%2C2735&sort_col=*created_ts&sort_dir=desc",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"immonet": {
|
|
||||||
"url": "https://www.immonet.de/classified-search?distributionTypes=Buy,Buy_Auction,Compulsory_Auction&estateTypes=House,Apartment&locations=AD08DE2112&order=Default&m=homepage_new_search_classified_search_result",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"immowelt": {
|
"immowelt": {
|
||||||
"url": "https://www.immowelt.de/classified-search?distributionTypes=Buy,Buy_Auction,Compulsory_Auction&estateTypes=House,Apartment&locations=AD08DE2350",
|
"url": "https://www.immowelt.de/classified-search?distributionTypes=Buy,Buy_Auction,Compulsory_Auction&estateTypes=House,Apartment&locations=AD08DE2350",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe('#wgGesucht testsuite()', () => {
|
|||||||
it('should test wgGesucht provider', async () => {
|
it('should test wgGesucht provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'wgGesucht', similarityCache);
|
const fredy = new Fredy(provider.config, null, null, provider.metaInformation.id, 'wgGesucht', similarityCache);
|
||||||
fredy.execute().then((listing) => {
|
fredy.execute().then((listing) => {
|
||||||
expect(listing).to.be.a('array');
|
expect(listing).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ describe('#wohnungsboerse testsuite()', () => {
|
|||||||
it('should test wohnungsboerse provider', async () => {
|
it('should test wohnungsboerse provider', async () => {
|
||||||
const Fredy = await mockFredy();
|
const Fredy = await mockFredy();
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'wohnungsboerse', similarityCache);
|
const fredy = new Fredy(
|
||||||
|
provider.config,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
provider.metaInformation.id,
|
||||||
|
'wohnungsboerse',
|
||||||
|
similarityCache,
|
||||||
|
);
|
||||||
fredy.execute().then((listings) => {
|
fredy.execute().then((listings) => {
|
||||||
expect(listings).to.be.a('array');
|
expect(listings).to.be.a('array');
|
||||||
const notificationObj = get();
|
const notificationObj = get();
|
||||||
|
|||||||
@@ -14,12 +14,6 @@
|
|||||||
"shouldBecome": "https://www.wg-gesucht.de/1-zimmer-wohnungen-in-Dusseldorf.30.1.1.0.html?sort_column=0&sort_order=0",
|
"shouldBecome": "https://www.wg-gesucht.de/1-zimmer-wohnungen-in-Dusseldorf.30.1.1.0.html?sort_column=0&sort_order=0",
|
||||||
"id": "wgGesucht"
|
"id": "wgGesucht"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"url": "https://www.immonet.de/immobiliensuche/sel.do?sortby=0&suchart=1&objecttype=1&marketingtype=2&parentcat=1&locationname=d%C3%BCsseldorf",
|
|
||||||
"shouldBecome": "https://www.immonet.de/immobiliensuche/sel.do?sortby=19&suchart=1&objecttype=1&marketingtype=2&parentcat=1&locationname=d%C3%BCsseldorf",
|
|
||||||
"id": "immonet"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "https://www.neubaukompass.de/neubau-immobilien/berlin-region/",
|
"url": "https://www.neubaukompass.de/neubau-immobilien/berlin-region/",
|
||||||
"shouldBecome": "https://www.neubaukompass.de/neubau-immobilien/berlin-region/?Sortierung=Id&Richtung=DESC",
|
"shouldBecome": "https://www.neubaukompass.de/neubau-immobilien/berlin-region/?Sortierung=Id&Richtung=DESC",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import FredyFooter from './components/footer/FredyFooter.jsx';
|
|||||||
import WatchlistManagement from './views/listings/management/WatchlistManagement.jsx';
|
import WatchlistManagement from './views/listings/management/WatchlistManagement.jsx';
|
||||||
import Dashboard from './views/dashboard/Dashboard.jsx';
|
import Dashboard from './views/dashboard/Dashboard.jsx';
|
||||||
import ListingDetail from './views/listings/ListingDetail.jsx';
|
import ListingDetail from './views/listings/ListingDetail.jsx';
|
||||||
|
import NewsModal from './components/news/NewsModal.jsx';
|
||||||
|
|
||||||
export default function FredyApp() {
|
export default function FredyApp() {
|
||||||
const actions = useActions();
|
const actions = useActions();
|
||||||
@@ -48,6 +49,7 @@ export default function FredyApp() {
|
|||||||
await actions.generalSettings.getGeneralSettings();
|
await actions.generalSettings.getGeneralSettings();
|
||||||
await actions.userSettings.getUserSettings();
|
await actions.userSettings.getUserSettings();
|
||||||
await actions.versionUpdate.getVersionUpdate();
|
await actions.versionUpdate.getVersionUpdate();
|
||||||
|
await actions.tracking.getTrackingPois();
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -88,6 +90,7 @@ export default function FredyApp() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
|
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
|
||||||
|
{!settings.demoMode && <NewsModal />}
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/403" element={<InsufficientPermission />} />
|
<Route path="/403" element={<InsufficientPermission />} />
|
||||||
<Route path="/jobs/new" element={<JobMutation />} />
|
<Route path="/jobs/new" element={<JobMutation />} />
|
||||||
|
|||||||
BIN
ui/src/assets/news/1.png
Normal file
BIN
ui/src/assets/news/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
BIN
ui/src/assets/news/2.png
Normal file
BIN
ui/src/assets/news/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
20
ui/src/assets/news/news.json
Normal file
20
ui/src/assets/news/news.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"key": "00e6b81777a275f5a140fc9101cb943810db6a69f6eb3927319c5aee0c876510",
|
||||||
|
"content":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Listings can now be filtered by location",
|
||||||
|
"text": "Thanks to https://github.com/strech345 for adding a new feature that allows you to filter listings based on their their location. When creating a job, you can now create multiple areas in which Fredy searches for new listings",
|
||||||
|
"image": "1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "More details from the Immoscout Provider",
|
||||||
|
"text": "https://github.com/MindCollaps added a new user-setting. When enabled Fredy will pull way more information for each listing from Immoscout.",
|
||||||
|
"image": "2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Listings now marked as invisible if filtered",
|
||||||
|
"text": "So far, when a listing was filtered e.g. due to being a duplicate, it was not send to you, but still shown on the map. This was now fixed."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
213
ui/src/components/map/Map.jsx
Normal file
213
ui/src/components/map/Map.jsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||||
|
import { fixMapboxDrawCompatibility, addDrawingControl, setupAreaFilterEventListeners } from './MapDrawingExtension.js';
|
||||||
|
import './Map.less';
|
||||||
|
|
||||||
|
export const GERMANY_BOUNDS = [
|
||||||
|
[5.866, 47.27], // Southwest coordinates
|
||||||
|
[15.042, 55.059], // Northeast coordinates
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STYLES = {
|
||||||
|
STANDARD: 'https://tiles.openfreemap.org/styles/bright',
|
||||||
|
SATELLITE: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
'satellite-tiles': {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution:
|
||||||
|
'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
||||||
|
},
|
||||||
|
'satellite-labels': {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [
|
||||||
|
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
|
||||||
|
],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© Esri',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 'satellite-tiles',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'satellite-tiles',
|
||||||
|
minzoom: 0,
|
||||||
|
maxzoom: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'satellite-labels',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'satellite-labels',
|
||||||
|
minzoom: 0,
|
||||||
|
maxzoom: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Map({
|
||||||
|
style = 'STANDARD',
|
||||||
|
show3dBuildings = false,
|
||||||
|
onMapReady = null,
|
||||||
|
enableDrawing = false,
|
||||||
|
initialSpatialFilter = null,
|
||||||
|
onDrawingChange = null,
|
||||||
|
}) {
|
||||||
|
const mapContainerRef = useRef(null);
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const drawRef = useRef(null);
|
||||||
|
|
||||||
|
// Initialize map - ONLY when container changes, never reinitialize
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapRef.current) return; // Map already exists, don't reinitialize
|
||||||
|
|
||||||
|
mapRef.current = new maplibregl.Map({
|
||||||
|
container: mapContainerRef.current,
|
||||||
|
style: STYLES[style],
|
||||||
|
center: [10.4515, 51.1657], // Center of Germany
|
||||||
|
zoom: 4,
|
||||||
|
maxBounds: GERMANY_BOUNDS,
|
||||||
|
antialias: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.current.addControl(
|
||||||
|
new maplibregl.NavigationControl({
|
||||||
|
showCompass: true,
|
||||||
|
visualizePitch: true,
|
||||||
|
visualizeRoll: true,
|
||||||
|
}),
|
||||||
|
'top-right',
|
||||||
|
);
|
||||||
|
|
||||||
|
mapRef.current.addControl(
|
||||||
|
new maplibregl.GeolocateControl({
|
||||||
|
positionOptions: {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
},
|
||||||
|
trackUserLocation: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize drawing extension only if enabled
|
||||||
|
if (enableDrawing) {
|
||||||
|
fixMapboxDrawCompatibility();
|
||||||
|
drawRef.current = addDrawingControl(mapRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call onMapReady callback if provided
|
||||||
|
if (onMapReady) {
|
||||||
|
onMapReady(mapRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapRef.current.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [mapContainerRef]); // ONLY depend on mapContainerRef - nothing else!
|
||||||
|
|
||||||
|
// Load spatial filter and setup area filter event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapRef.current || !drawRef.current || !enableDrawing) return;
|
||||||
|
|
||||||
|
// Load initial spatial filter if provided
|
||||||
|
if (initialSpatialFilter) {
|
||||||
|
try {
|
||||||
|
drawRef.current.set(initialSpatialFilter);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading spatial filter:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup drawing event listeners
|
||||||
|
const cleanup = setupAreaFilterEventListeners(mapRef.current, drawRef.current, onDrawingChange);
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}, [initialSpatialFilter, onDrawingChange, enableDrawing]);
|
||||||
|
|
||||||
|
// Handle style changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapRef.current.setStyle(STYLES[style]);
|
||||||
|
}
|
||||||
|
}, [style]);
|
||||||
|
|
||||||
|
// Handle 3D buildings layer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapRef.current) return;
|
||||||
|
|
||||||
|
const add3dLayer = () => {
|
||||||
|
if (!mapRef.current || !mapRef.current.isStyleLoaded()) return;
|
||||||
|
if (show3dBuildings) {
|
||||||
|
if (!mapRef.current.getSource('openfreemap')) {
|
||||||
|
mapRef.current.addSource('openfreemap', {
|
||||||
|
type: 'vector',
|
||||||
|
url: 'https://tiles.openfreemap.org/planet',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!mapRef.current.getLayer('3d-buildings')) {
|
||||||
|
const layers = mapRef.current.getStyle().layers;
|
||||||
|
let labelLayerId;
|
||||||
|
for (let i = 0; i < layers.length; i++) {
|
||||||
|
if (layers[i].type === 'symbol' && layers[i].layout?.['text-field']) {
|
||||||
|
labelLayerId = layers[i].id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapRef.current.addLayer(
|
||||||
|
{
|
||||||
|
id: '3d-buildings',
|
||||||
|
source: 'openfreemap',
|
||||||
|
'source-layer': 'building',
|
||||||
|
type: 'fill-extrusion',
|
||||||
|
minzoom: 15,
|
||||||
|
filter: ['!=', ['get', 'hide_3d'], true],
|
||||||
|
paint: {
|
||||||
|
'fill-extrusion-color': [
|
||||||
|
'interpolate',
|
||||||
|
['linear'],
|
||||||
|
['get', 'render_height'],
|
||||||
|
0,
|
||||||
|
'lightgray',
|
||||||
|
200,
|
||||||
|
'royalblue',
|
||||||
|
400,
|
||||||
|
'lightblue',
|
||||||
|
],
|
||||||
|
'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 16, ['get', 'render_height']],
|
||||||
|
'fill-extrusion-base': ['case', ['>=', ['get', 'zoom'], 16], ['get', 'render_min_height'], 0],
|
||||||
|
'fill-extrusion-opacity': 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLayerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mapRef.current.getLayer('3d-buildings')) {
|
||||||
|
mapRef.current.removeLayer('3d-buildings');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
add3dLayer();
|
||||||
|
}, [show3dBuildings, style]);
|
||||||
|
|
||||||
|
// Handle pitch for 3D
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapRef.current) return;
|
||||||
|
mapRef.current.setPitch(show3dBuildings ? 45 : 0);
|
||||||
|
}, [show3dBuildings]);
|
||||||
|
|
||||||
|
return <div ref={mapContainerRef} className="map-container" />;
|
||||||
|
}
|
||||||
45
ui/src/components/map/Map.less
Normal file
45
ui/src/components/map/Map.less
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix Mapbox Draw cursors for MapLibre GL compatibility */
|
||||||
|
.maplibregl-map.mouse-pointer .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mouse-move .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mouse-add .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mouse-move.mode-direct_select .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mode-direct_select.feature-vertex.mouse-move .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mode-direct_select.feature-midpoint.mouse-pointer .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mode-direct_select.feature-feature.mouse-move .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-map.mode-static.mouse-pointer .maplibregl-canvas-container.maplibregl-interactive {
|
||||||
|
cursor: grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
}
|
||||||
177
ui/src/components/map/MapDrawingExtension.js
Normal file
177
ui/src/components/map/MapDrawingExtension.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||||
|
|
||||||
|
const drawStyles = [
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-fill-inactive',
|
||||||
|
type: 'fill',
|
||||||
|
filter: ['all', ['==', 'active', 'false'], ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
|
||||||
|
paint: { 'fill-color': '#3bb2d0', 'fill-outline-color': '#3bb2d0', 'fill-opacity': 0.1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-fill-active',
|
||||||
|
type: 'fill',
|
||||||
|
filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
|
||||||
|
paint: { 'fill-color': '#fbb03b', 'fill-outline-color': '#fbb03b', 'fill-opacity': 0.1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-midpoint',
|
||||||
|
type: 'circle',
|
||||||
|
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']],
|
||||||
|
paint: { 'circle-radius': 3, 'circle-color': '#fbb03b' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-stroke-inactive',
|
||||||
|
type: 'line',
|
||||||
|
filter: ['all', ['==', 'active', 'false'], ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#3bb2d0', 'line-width': 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-stroke-active',
|
||||||
|
type: 'line',
|
||||||
|
filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#fbb03b', 'line-dasharray': [0.2, 2], 'line-width': 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-line-inactive',
|
||||||
|
type: 'line',
|
||||||
|
filter: ['all', ['==', 'active', 'false'], ['==', '$type', 'LineString'], ['!=', 'mode', 'static']],
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#3bb2d0', 'line-width': 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-line-active',
|
||||||
|
type: 'line',
|
||||||
|
filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']],
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#fbb03b', 'line-dasharray': [0.2, 2], 'line-width': 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-and-line-vertex-stroke-inactive',
|
||||||
|
type: 'circle',
|
||||||
|
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
|
||||||
|
paint: { 'circle-radius': 5, 'circle-color': '#fff' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-and-line-vertex-inactive',
|
||||||
|
type: 'circle',
|
||||||
|
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
|
||||||
|
paint: { 'circle-radius': 3, 'circle-color': '#fbb03b' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-point-point-stroke-inactive',
|
||||||
|
type: 'circle',
|
||||||
|
filter: [
|
||||||
|
'all',
|
||||||
|
['==', 'active', 'false'],
|
||||||
|
['==', '$type', 'Point'],
|
||||||
|
['==', 'meta', 'feature'],
|
||||||
|
['!=', 'mode', 'static'],
|
||||||
|
],
|
||||||
|
paint: { 'circle-radius': 5, 'circle-opacity': 1, 'circle-color': '#fff' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-point-inactive',
|
||||||
|
type: 'circle',
|
||||||
|
filter: [
|
||||||
|
'all',
|
||||||
|
['==', 'active', 'false'],
|
||||||
|
['==', '$type', 'Point'],
|
||||||
|
['==', 'meta', 'feature'],
|
||||||
|
['!=', 'mode', 'static'],
|
||||||
|
],
|
||||||
|
paint: { 'circle-radius': 3, 'circle-color': '#3bb2d0' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-point-stroke-active',
|
||||||
|
type: 'circle',
|
||||||
|
filter: ['all', ['==', '$type', 'Point'], ['==', 'active', 'true'], ['!=', 'meta', 'midpoint']],
|
||||||
|
paint: { 'circle-radius': 7, 'circle-color': '#fff' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-point-active',
|
||||||
|
type: 'circle',
|
||||||
|
filter: ['all', ['==', '$type', 'Point'], ['!=', 'meta', 'midpoint'], ['==', 'active', 'true']],
|
||||||
|
paint: { 'circle-radius': 5, 'circle-color': '#fbb03b' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-fill-static',
|
||||||
|
type: 'fill',
|
||||||
|
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']],
|
||||||
|
paint: { 'fill-color': '#404040', 'fill-outline-color': '#404040', 'fill-opacity': 0.1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-polygon-stroke-static',
|
||||||
|
type: 'line',
|
||||||
|
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']],
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#404040', 'line-width': 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-line-static',
|
||||||
|
type: 'line',
|
||||||
|
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'LineString']],
|
||||||
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||||
|
paint: { 'line-color': '#404040', 'line-width': 2 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-draw-point-static',
|
||||||
|
type: 'circle',
|
||||||
|
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Point']],
|
||||||
|
paint: { 'circle-radius': 5, 'circle-color': '#404040' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function fixMapboxDrawCompatibility() {
|
||||||
|
MapboxDraw.constants.classes.CANVAS = 'maplibregl-canvas';
|
||||||
|
MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl';
|
||||||
|
MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
|
||||||
|
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';
|
||||||
|
MapboxDraw.constants.classes.ATTRIBUTION = 'maplibregl-ctrl-attrib';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDrawingControl(map) {
|
||||||
|
const draw = new MapboxDraw({
|
||||||
|
displayControlsDefault: false,
|
||||||
|
controls: {
|
||||||
|
polygon: true,
|
||||||
|
trash: true,
|
||||||
|
},
|
||||||
|
styles: drawStyles,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addControl(draw, 'top-left');
|
||||||
|
return draw;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupAreaFilterEventListeners(map, draw, onDrawingChange) {
|
||||||
|
if (!map || !draw) return () => {};
|
||||||
|
|
||||||
|
const handleDrawChange = () => {
|
||||||
|
if (draw) {
|
||||||
|
const data = draw.getAll();
|
||||||
|
if (onDrawingChange) {
|
||||||
|
onDrawingChange(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on('draw.create', handleDrawChange);
|
||||||
|
map.on('draw.update', handleDrawChange);
|
||||||
|
map.on('draw.delete', handleDrawChange);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
if (map) {
|
||||||
|
map.off('draw.create', handleDrawChange);
|
||||||
|
map.off('draw.update', handleDrawChange);
|
||||||
|
map.off('draw.delete', handleDrawChange);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
70
ui/src/components/news/NewsModal.jsx
Normal file
70
ui/src/components/news/NewsModal.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UserGuide } from '@douyinfe/semi-ui-19';
|
||||||
|
import { useScreenWidth } from '../../hooks/screenWidth';
|
||||||
|
import heart from '../../assets/heart.png';
|
||||||
|
import newsConfig from '../../assets/news/news.json';
|
||||||
|
import { useActions, useSelector } from '../../services/state/store';
|
||||||
|
|
||||||
|
import './NewsModal.less';
|
||||||
|
|
||||||
|
const newsImages = import.meta.glob('../../assets/news/*', { eager: true, query: '?url', import: 'default' });
|
||||||
|
|
||||||
|
const NewsModal = () => {
|
||||||
|
const screenWidth = useScreenWidth();
|
||||||
|
const newsHash = useSelector((state) => state.userSettings.settings.news_hash);
|
||||||
|
const userSettingsLoaded = useSelector((state) => state.userSettings.loaded);
|
||||||
|
const pois = useSelector((state) => state.tracking.pois);
|
||||||
|
const actions = useActions();
|
||||||
|
|
||||||
|
if (newsConfig == null || newsConfig.length === 0 || screenWidth <= 768) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = newsConfig.content.map((item) => ({
|
||||||
|
title: (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<img src={heart} width="30" alt="Fredy Logo" style={{ marginRight: '10px' }} />
|
||||||
|
<b>{item.title}</b>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
description: (
|
||||||
|
<div style={{ textAlign: 'left' }}>
|
||||||
|
{item.image && newsImages[`../../assets/news/${item.image}`] && (
|
||||||
|
<img
|
||||||
|
src={newsImages[`../../assets/news/${item.image}`]}
|
||||||
|
alt={item.title}
|
||||||
|
style={{ width: '100%', marginBottom: 10, borderRadius: 4 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: item.text }} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleClose = (poi) => {
|
||||||
|
actions.userSettings.setNewsHash(newsConfig.key);
|
||||||
|
if (poi) {
|
||||||
|
actions.tracking.trackPoi(poi);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserGuide
|
||||||
|
mode="modal"
|
||||||
|
mask={true}
|
||||||
|
steps={steps}
|
||||||
|
visible={userSettingsLoaded && newsHash !== newsConfig.key}
|
||||||
|
onFinish={() => handleClose(pois.WELCOME_FINISHED)}
|
||||||
|
onSkip={() => handleClose(pois.WELCOME_SKIPPED)}
|
||||||
|
modalProps={{
|
||||||
|
width: '10rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsModal;
|
||||||
3
ui/src/components/news/NewsModal.less
Normal file
3
ui/src/components/news/NewsModal.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.semi-userGuide-modal-body-title {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { xhrGet } from '../xhr.js';
|
import { xhrGet, xhrPost } from '../xhr.js';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
|
|
||||||
const logger = (config) => (set, get, api) =>
|
const logger = (config) => (set, get, api) =>
|
||||||
@@ -27,10 +27,21 @@ const logger = (config) => (set, get, api) =>
|
|||||||
api,
|
api,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to track loading state of async actions.
|
||||||
|
*/
|
||||||
|
const loadingTracker = (config) => (set, get, api) => {
|
||||||
|
const wrappedSet = (partial, replace) => {
|
||||||
|
set(partial, replace);
|
||||||
|
};
|
||||||
|
|
||||||
|
return config(wrappedSet, get, api);
|
||||||
|
};
|
||||||
|
|
||||||
// Create the Zustand store with slices and actions
|
// Create the Zustand store with slices and actions
|
||||||
export const useFredyState = create(
|
export const useFredyState = create(
|
||||||
logger(
|
logger(
|
||||||
(set) => {
|
loadingTracker((set) => {
|
||||||
// Async actions that directly set state (no separate reducer concept)
|
// Async actions that directly set state (no separate reducer concept)
|
||||||
const effects = {
|
const effects = {
|
||||||
dashboard: {
|
dashboard: {
|
||||||
@@ -169,6 +180,23 @@ export const useFredyState = create(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tracking: {
|
||||||
|
async getTrackingPois() {
|
||||||
|
try {
|
||||||
|
const response = await xhrGet('/api/tracking/trackingPois');
|
||||||
|
set((state) => ({ tracking: { ...state.tracking, pois: Object.freeze(response.json) } }));
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to get resource for api/tracking. Error:', Exception);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async trackPoi(poi) {
|
||||||
|
try {
|
||||||
|
await xhrPost('/api/tracking/poi', { poi });
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to track poi. Error:', Exception);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
listingsData: {
|
listingsData: {
|
||||||
async getListingsData({
|
async getListingsData({
|
||||||
page = 1,
|
page = 1,
|
||||||
@@ -234,9 +262,60 @@ export const useFredyState = create(
|
|||||||
async getUserSettings() {
|
async getUserSettings() {
|
||||||
try {
|
try {
|
||||||
const response = await xhrGet('/api/user/settings');
|
const response = await xhrGet('/api/user/settings');
|
||||||
set((state) => ({ userSettings: { ...state.userSettings, settings: response.json } }));
|
set((state) => ({ userSettings: { ...state.userSettings, settings: response.json, loaded: true } }));
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
console.error('Error while trying to get resource for api/user/settings. Error:', Exception);
|
console.error('Error while trying to get resource for api/user/settings. Error:', Exception);
|
||||||
|
// Mark as loaded even on error to prevent blocking the UI
|
||||||
|
set((state) => ({ userSettings: { ...state.userSettings, loaded: true } }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setNewsHash(newsHash) {
|
||||||
|
try {
|
||||||
|
await xhrPost('/api/user/settings/news-hash', { news_hash: newsHash });
|
||||||
|
set((state) => ({
|
||||||
|
userSettings: {
|
||||||
|
...state.userSettings,
|
||||||
|
settings: { ...state.userSettings.settings, news_hash: newsHash },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to update news hash. Error:', Exception);
|
||||||
|
throw Exception;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setHomeAddress(address) {
|
||||||
|
try {
|
||||||
|
const response = await xhrPost('/api/user/settings/home-address', { home_address: address });
|
||||||
|
if (response.status === 200) {
|
||||||
|
set((state) => ({
|
||||||
|
userSettings: {
|
||||||
|
...state.userSettings,
|
||||||
|
settings: {
|
||||||
|
...state.userSettings.settings,
|
||||||
|
home_address: { address, coords: response.json.coords },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return response.json;
|
||||||
|
}
|
||||||
|
throw response;
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to update home address. Error:', Exception);
|
||||||
|
throw Exception;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setImmoscoutDetails(enabled) {
|
||||||
|
try {
|
||||||
|
await xhrPost('/api/user/settings/immoscout-details', { immoscout_details: enabled });
|
||||||
|
set((state) => ({
|
||||||
|
userSettings: {
|
||||||
|
...state.userSettings,
|
||||||
|
settings: { ...state.userSettings.settings, immoscout_details: enabled },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to update immoscout details setting. Error:', Exception);
|
||||||
|
throw Exception;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -255,9 +334,10 @@ export const useFredyState = create(
|
|||||||
maxPrice: 0,
|
maxPrice: 0,
|
||||||
},
|
},
|
||||||
generalSettings: { settings: {} },
|
generalSettings: { settings: {} },
|
||||||
userSettings: { settings: {} },
|
userSettings: { settings: {}, loaded: false },
|
||||||
demoMode: { demoMode: false },
|
demoMode: { demoMode: false },
|
||||||
versionUpdate: {},
|
versionUpdate: {},
|
||||||
|
tracking: { pois: {} },
|
||||||
provider: [],
|
provider: [],
|
||||||
jobsData: {
|
jobsData: {
|
||||||
jobs: [],
|
jobs: [],
|
||||||
@@ -276,6 +356,7 @@ export const useFredyState = create(
|
|||||||
generalSettings: { ...effects.generalSettings },
|
generalSettings: { ...effects.generalSettings },
|
||||||
demoMode: { ...effects.demoMode },
|
demoMode: { ...effects.demoMode },
|
||||||
versionUpdate: { ...effects.versionUpdate },
|
versionUpdate: { ...effects.versionUpdate },
|
||||||
|
tracking: { ...effects.tracking },
|
||||||
listingsData: { ...effects.listingsData },
|
listingsData: { ...effects.listingsData },
|
||||||
provider: { ...effects.provider },
|
provider: { ...effects.provider },
|
||||||
jobsData: { ...effects.jobsData },
|
jobsData: { ...effects.jobsData },
|
||||||
@@ -283,12 +364,34 @@ export const useFredyState = create(
|
|||||||
userSettings: { ...effects.userSettings },
|
userSettings: { ...effects.userSettings },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wrap actions to track loading state
|
||||||
|
const wrappedActions = {};
|
||||||
|
Object.keys(actions).forEach((slice) => {
|
||||||
|
wrappedActions[slice] = {};
|
||||||
|
Object.keys(actions[slice]).forEach((actionName) => {
|
||||||
|
const originalAction = actions[slice][actionName];
|
||||||
|
if (typeof originalAction === 'function') {
|
||||||
|
wrappedActions[slice][actionName] = async (...args) => {
|
||||||
|
const fullActionName = `${slice}.${actionName}`;
|
||||||
|
set((state) => ({ loading: { ...state.loading, [fullActionName]: true } }));
|
||||||
|
try {
|
||||||
|
return await originalAction(...args);
|
||||||
|
} finally {
|
||||||
|
set((state) => ({ loading: { ...state.loading, [fullActionName]: false } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
wrappedActions[slice][actionName] = originalAction;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...initial,
|
...initial,
|
||||||
__actions: { actions },
|
loading: {},
|
||||||
|
__actions: { actions: wrappedActions },
|
||||||
};
|
};
|
||||||
},
|
}),
|
||||||
{ name: 'fredy' },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -312,3 +415,27 @@ export function useSelector(selector, equalityFn = shallow) {
|
|||||||
export function useActions() {
|
export function useActions() {
|
||||||
return useFredyState((s) => s.__actions.actions);
|
return useFredyState((s) => s.__actions.actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if a specific action is currently loading.
|
||||||
|
* @param {Function} action - The action function from useActions()
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function useIsLoading(action) {
|
||||||
|
const actions = useActions();
|
||||||
|
const loading = useSelector((state) => state.loading);
|
||||||
|
|
||||||
|
// Find the action name by comparing the function
|
||||||
|
let actionPath = null;
|
||||||
|
for (const slice in actions) {
|
||||||
|
for (const name in actions[slice]) {
|
||||||
|
if (actions[slice][name] === action) {
|
||||||
|
actionPath = `${slice}.${name}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionPath) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!loading[actionPath];
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState, useCallback } from 'react';
|
||||||
|
|
||||||
import NotificationAdapterMutator from './components/notificationAdapter/NotificationAdapterMutator';
|
import NotificationAdapterMutator from './components/notificationAdapter/NotificationAdapterMutator';
|
||||||
import NotificationAdapterTable from '../../../components/table/NotificationAdapterTable';
|
import NotificationAdapterTable from '../../../components/table/NotificationAdapterTable';
|
||||||
import ProviderTable from '../../../components/table/ProviderTable';
|
import ProviderTable from '../../../components/table/ProviderTable';
|
||||||
import ProviderMutator from './components/provider/ProviderMutator';
|
import ProviderMutator from './components/provider/ProviderMutator';
|
||||||
|
import AreaFilter from './components/areaFilter/AreaFilter';
|
||||||
import Headline from '../../../components/headline/Headline';
|
import Headline from '../../../components/headline/Headline';
|
||||||
import { useActions, useSelector } from '../../../services/state/store';
|
import { useActions, useSelector } from '../../../services/state/store';
|
||||||
import { xhrPost } from '../../../services/xhr';
|
import { xhrPost } from '../../../services/xhr';
|
||||||
@@ -44,6 +45,7 @@ export default function JobMutator() {
|
|||||||
const defaultNotificationAdapter = sourceJob?.notificationAdapter || [];
|
const defaultNotificationAdapter = sourceJob?.notificationAdapter || [];
|
||||||
const defaultEnabled = sourceJob?.enabled ?? true;
|
const defaultEnabled = sourceJob?.enabled ?? true;
|
||||||
const defaultShareWithUsers = sourceJob?.shared_with_user ?? [];
|
const defaultShareWithUsers = sourceJob?.shared_with_user ?? [];
|
||||||
|
const defaultSpatialFilter = sourceJob?.spatialFilter || null;
|
||||||
|
|
||||||
const [providerToEdit, setProviderToEdit] = useState(null);
|
const [providerToEdit, setProviderToEdit] = useState(null);
|
||||||
const [providerCreationVisible, setProviderCreationVisibility] = useState(false);
|
const [providerCreationVisible, setProviderCreationVisibility] = useState(false);
|
||||||
@@ -55,9 +57,15 @@ export default function JobMutator() {
|
|||||||
const [notificationAdapterData, setNotificationAdapterData] = useState(defaultNotificationAdapter);
|
const [notificationAdapterData, setNotificationAdapterData] = useState(defaultNotificationAdapter);
|
||||||
const [shareWithUsers, setShareWithUsers] = useState(defaultShareWithUsers);
|
const [shareWithUsers, setShareWithUsers] = useState(defaultShareWithUsers);
|
||||||
const [enabled, setEnabled] = useState(defaultEnabled);
|
const [enabled, setEnabled] = useState(defaultEnabled);
|
||||||
|
const [spatialFilter, setSpatialFilter] = useState(defaultSpatialFilter);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const actions = useActions();
|
const actions = useActions();
|
||||||
|
|
||||||
|
// Memoize the spatial filter change handler to prevent map reinitializations
|
||||||
|
const handleSpatialFilterChange = useCallback((data) => {
|
||||||
|
setSpatialFilter(data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isSavingEnabled = () => {
|
const isSavingEnabled = () => {
|
||||||
return Boolean(notificationAdapterData.length && providerData.length && name);
|
return Boolean(notificationAdapterData.length && providerData.length && name);
|
||||||
};
|
};
|
||||||
@@ -76,6 +84,7 @@ export default function JobMutator() {
|
|||||||
shareWithUsers,
|
shareWithUsers,
|
||||||
name,
|
name,
|
||||||
blacklist,
|
blacklist,
|
||||||
|
spatialFilter,
|
||||||
enabled,
|
enabled,
|
||||||
jobId: jobToBeEdit?.id || null,
|
jobId: jobToBeEdit?.id || null,
|
||||||
});
|
});
|
||||||
@@ -206,6 +215,13 @@ export default function JobMutator() {
|
|||||||
/>
|
/>
|
||||||
</SegmentPart>
|
</SegmentPart>
|
||||||
<Divider margin="1rem" />
|
<Divider margin="1rem" />
|
||||||
|
<SegmentPart
|
||||||
|
name="Area Filter"
|
||||||
|
helpText="Define multiple geographic areas on the map to filter listings. Start drawing by clicking on the square symbol in the top left corner of the map. Click on the map to add points of the polygon. Select the first point to close the polygon. After that, click on a free area of the map to apply this polygon (the color will change from yellow to blue). To delete a polygon, select it first and then click on the trash symbol."
|
||||||
|
>
|
||||||
|
<AreaFilter spatialFilter={spatialFilter} onChange={handleSpatialFilterChange} />
|
||||||
|
</SegmentPart>
|
||||||
|
<Divider margin="1rem" />
|
||||||
<SegmentPart
|
<SegmentPart
|
||||||
Icon={IconUser}
|
Icon={IconUser}
|
||||||
name="Sharing with user"
|
name="Sharing with user"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Map from '../../../../../components/map/Map.jsx';
|
||||||
|
import './AreaFilter.less';
|
||||||
|
|
||||||
|
export default function AreaFilter({ spatialFilter = null, onChange = null }) {
|
||||||
|
return (
|
||||||
|
<div className="areaFilter">
|
||||||
|
<Map
|
||||||
|
style="STANDARD"
|
||||||
|
show3dBuildings={false}
|
||||||
|
enableDrawing={true}
|
||||||
|
initialSpatialFilter={spatialFilter}
|
||||||
|
onDrawingChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
.areaFilter {
|
||||||
|
height: 50rem;
|
||||||
|
}
|
||||||
@@ -27,10 +27,13 @@ const sortAdapter = (a, b) => {
|
|||||||
const validate = (selectedAdapter) => {
|
const validate = (selectedAdapter) => {
|
||||||
const results = [];
|
const results = [];
|
||||||
for (let uiElement of Object.values(selectedAdapter.fields || [])) {
|
for (let uiElement of Object.values(selectedAdapter.fields || [])) {
|
||||||
if (uiElement.value == null && !uiElement.optional) {
|
if (uiElement.value == null && !uiElement.optional && uiElement.type !== 'boolean') {
|
||||||
results.push('All fields are mandatory and must be set.');
|
results.push('All fields are mandatory and must be set.');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (uiElement.type === 'boolean' && typeof uiElement.value !== 'boolean') {
|
||||||
|
uiElement.value = false;
|
||||||
|
}
|
||||||
if (uiElement.type === 'number') {
|
if (uiElement.type === 'number') {
|
||||||
const numberValue = parseFloat(uiElement.value);
|
const numberValue = parseFloat(uiElement.value);
|
||||||
if (isNaN(numberValue) || numberValue < 0) {
|
if (isNaN(numberValue) || numberValue < 0) {
|
||||||
@@ -153,12 +156,15 @@ export default function NotificationAdapterMutator({
|
|||||||
return (
|
return (
|
||||||
<Form key={key}>
|
<Form key={key}>
|
||||||
{uiElement.type === 'boolean' ? (
|
{uiElement.type === 'boolean' ? (
|
||||||
<Switch
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
|
<Switch
|
||||||
checked={uiElement.value || false}
|
checked={uiElement.value || false}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
setValue(selectedAdapter, uiElement, key, checked);
|
setValue(selectedAdapter, uiElement, key, checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{uiElement.label}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Form.Input
|
<Form.Input
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@@ -197,27 +203,6 @@ export default function NotificationAdapterMutator({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{validationMessage != null && (
|
|
||||||
<Banner
|
|
||||||
fullMode={false}
|
|
||||||
type="danger"
|
|
||||||
closeIcon={null}
|
|
||||||
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Error</div>}
|
|
||||||
style={{ marginBottom: '1rem' }}
|
|
||||||
description={<p dangerouslySetInnerHTML={{ __html: validationMessage }} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{successMessage != null && (
|
|
||||||
<Banner
|
|
||||||
fullMode={false}
|
|
||||||
type="success"
|
|
||||||
closeIcon={null}
|
|
||||||
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Yay!</div>}
|
|
||||||
style={{ marginBottom: '1rem' }}
|
|
||||||
description={<p dangerouslySetInnerHTML={{ __html: successMessage }} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{description != null ? (
|
{description != null ? (
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -264,6 +249,28 @@ export default function NotificationAdapterMutator({
|
|||||||
<br />
|
<br />
|
||||||
{selectedAdapter.readme != null && <Help readme={selectedAdapter.readme} />}
|
{selectedAdapter.readme != null && <Help readme={selectedAdapter.readme} />}
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
{validationMessage != null && (
|
||||||
|
<Banner
|
||||||
|
fullMode={false}
|
||||||
|
type="danger"
|
||||||
|
closeIcon={null}
|
||||||
|
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Error</div>}
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
description={<p dangerouslySetInnerHTML={{ __html: validationMessage }} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{successMessage != null && (
|
||||||
|
<Banner
|
||||||
|
fullMode={false}
|
||||||
|
type="success"
|
||||||
|
closeIcon={null}
|
||||||
|
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Yay!</div>}
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
description={<p dangerouslySetInnerHTML={{ __html: successMessage }} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{getFieldsFor(selectedAdapter)}
|
{getFieldsFor(selectedAdapter)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,54 +20,10 @@ import './Map.less';
|
|||||||
import { xhrDelete } from '../../services/xhr.js';
|
import { xhrDelete } from '../../services/xhr.js';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
|
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
|
||||||
|
import Map from '../../components/map/Map.jsx';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const GERMANY_BOUNDS = [
|
|
||||||
[5.866, 47.27], // Southwest coordinates
|
|
||||||
[15.042, 55.059], // Northeast coordinates
|
|
||||||
];
|
|
||||||
|
|
||||||
const STYLES = {
|
|
||||||
STANDARD: 'https://tiles.openfreemap.org/styles/bright',
|
|
||||||
SATELLITE: {
|
|
||||||
version: 8,
|
|
||||||
sources: {
|
|
||||||
'satellite-tiles': {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution:
|
|
||||||
'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
|
||||||
},
|
|
||||||
'satellite-labels': {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [
|
|
||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
|
|
||||||
],
|
|
||||||
tileSize: 256,
|
|
||||||
attribution: '© Esri',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: 'satellite-tiles',
|
|
||||||
type: 'raster',
|
|
||||||
source: 'satellite-tiles',
|
|
||||||
minzoom: 0,
|
|
||||||
maxzoom: 19,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'satellite-labels',
|
|
||||||
type: 'raster',
|
|
||||||
source: 'satellite-labels',
|
|
||||||
minzoom: 0,
|
|
||||||
maxzoom: 19,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MapView() {
|
export default function MapView() {
|
||||||
const mapContainer = useRef(null);
|
const mapContainer = useRef(null);
|
||||||
const map = useRef(null);
|
const map = useRef(null);
|
||||||
@@ -136,117 +92,24 @@ export default function MapView() {
|
|||||||
};
|
};
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
// Get map instance reference after MapComponent renders
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map.current) return;
|
if (mapContainer.current && !map.current) {
|
||||||
|
// Wait for MapComponent to initialize the map
|
||||||
map.current = new maplibregl.Map({
|
const checkMapReady = () => {
|
||||||
container: mapContainer.current,
|
if (mapContainer.current?.map) {
|
||||||
style: STYLES[style],
|
map.current = mapContainer.current.map;
|
||||||
center: [10.4515, 51.1657], // Center of Germany
|
} else {
|
||||||
zoom: 4,
|
setTimeout(checkMapReady, 100);
|
||||||
maxBounds: GERMANY_BOUNDS,
|
}
|
||||||
antialias: true,
|
};
|
||||||
});
|
checkMapReady();
|
||||||
|
}
|
||||||
map.current.addControl(
|
|
||||||
new maplibregl.NavigationControl({
|
|
||||||
showCompass: true,
|
|
||||||
visualizePitch: true,
|
|
||||||
visualizeRoll: true,
|
|
||||||
}),
|
|
||||||
'top-right',
|
|
||||||
);
|
|
||||||
|
|
||||||
map.current.addControl(
|
|
||||||
new maplibregl.GeolocateControl({
|
|
||||||
positionOptions: {
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
},
|
|
||||||
trackUserLocation: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
map.current.remove();
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleMapReady = (mapInstance) => {
|
||||||
if (map.current) {
|
map.current = mapInstance;
|
||||||
map.current.setStyle(STYLES[style]);
|
};
|
||||||
}
|
|
||||||
}, [style]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (show3dBuildings && style !== 'STANDARD') {
|
|
||||||
setStyle('STANDARD');
|
|
||||||
}
|
|
||||||
}, [show3dBuildings, style]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!map.current) return;
|
|
||||||
|
|
||||||
map.current.setPitch(show3dBuildings ? 45 : 0);
|
|
||||||
}, [show3dBuildings]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!map.current) return;
|
|
||||||
|
|
||||||
const add3dLayer = () => {
|
|
||||||
if (!map.current || !map.current.isStyleLoaded()) return;
|
|
||||||
if (show3dBuildings) {
|
|
||||||
if (!map.current.getSource('openfreemap')) {
|
|
||||||
map.current.addSource('openfreemap', {
|
|
||||||
type: 'vector',
|
|
||||||
url: 'https://tiles.openfreemap.org/planet',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!map.current.getLayer('3d-buildings')) {
|
|
||||||
const layers = map.current.getStyle().layers;
|
|
||||||
let labelLayerId;
|
|
||||||
for (let i = 0; i < layers.length; i++) {
|
|
||||||
if (layers[i].type === 'symbol' && layers[i].layout?.['text-field']) {
|
|
||||||
labelLayerId = layers[i].id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.current.addLayer(
|
|
||||||
{
|
|
||||||
id: '3d-buildings',
|
|
||||||
source: 'openfreemap',
|
|
||||||
'source-layer': 'building',
|
|
||||||
type: 'fill-extrusion',
|
|
||||||
minzoom: 15,
|
|
||||||
filter: ['!=', ['get', 'hide_3d'], true],
|
|
||||||
paint: {
|
|
||||||
'fill-extrusion-color': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['get', 'render_height'],
|
|
||||||
0,
|
|
||||||
'lightgray',
|
|
||||||
200,
|
|
||||||
'royalblue',
|
|
||||||
400,
|
|
||||||
'lightblue',
|
|
||||||
],
|
|
||||||
'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 16, ['get', 'render_height']],
|
|
||||||
'fill-extrusion-base': ['case', ['>=', ['get', 'zoom'], 16], ['get', 'render_min_height'], 0],
|
|
||||||
'fill-extrusion-opacity': 0.6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelLayerId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (map.current.getLayer('3d-buildings')) {
|
|
||||||
map.current.removeLayer('3d-buildings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
add3dLayer();
|
|
||||||
}, [show3dBuildings, style]);
|
|
||||||
|
|
||||||
const setMapStyle = (value) => {
|
const setMapStyle = (value) => {
|
||||||
setStyle(value);
|
setStyle(value);
|
||||||
@@ -573,7 +436,7 @@ export default function MapView() {
|
|||||||
description="Keep in mind, only listings with proper adresses are being shown on this map."
|
description="Keep in mind, only listings with proper adresses are being shown on this map."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div ref={mapContainer} className="map-container" />
|
<Map mapContainerRef={mapContainer} style={style} show3dBuildings={show3dBuildings} onMapReady={handleMapReady} />
|
||||||
<ListingDeletionModal
|
<ListingDeletionModal
|
||||||
visible={deleteModalVisible}
|
visible={deleteModalVisible}
|
||||||
onConfirm={confirmListingDeletion}
|
onConfirm={confirmListingDeletion}
|
||||||
|
|||||||
@@ -4,19 +4,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { Divider, Button, AutoComplete, Toast, Banner } from '@douyinfe/semi-ui-19';
|
import { Divider, Button, AutoComplete, Toast, Banner, Switch } from '@douyinfe/semi-ui-19';
|
||||||
import { IconSave, IconHome } from '@douyinfe/semi-icons';
|
import { IconSave, IconHome, IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { useSelector, useActions } from '../../services/state/store';
|
import { useSelector, useActions, useIsLoading } from '../../services/state/store';
|
||||||
import { xhrGet, xhrPost } from '../../services/xhr';
|
import { xhrGet } from '../../services/xhr';
|
||||||
import { SegmentPart } from '../../components/segment/SegmentPart';
|
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
const UserSettings = () => {
|
const UserSettings = () => {
|
||||||
const actions = useActions();
|
const actions = useActions();
|
||||||
const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
|
const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
|
||||||
|
const immoscoutDetails = useSelector((state) => state.userSettings.settings.immoscout_details);
|
||||||
const [address, setAddress] = useState(homeAddress?.address || '');
|
const [address, setAddress] = useState(homeAddress?.address || '');
|
||||||
const [coords, setCoords] = useState(homeAddress?.coords || null);
|
const [coords, setCoords] = useState(homeAddress?.coords || null);
|
||||||
const [saving, setSaving] = useState(false);
|
const saving = useIsLoading(actions.userSettings.setHomeAddress);
|
||||||
const [dataSource, setDataSource] = useState([]);
|
const [dataSource, setDataSource] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -25,22 +26,15 @@ const UserSettings = () => {
|
|||||||
}, [homeAddress]);
|
}, [homeAddress]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
|
||||||
try {
|
try {
|
||||||
const response = await xhrPost('/api/user/settings/home-address', { home_address: address });
|
const responseJson = await actions.userSettings.setHomeAddress(address);
|
||||||
if (response.status === 200) {
|
setCoords(responseJson.coords);
|
||||||
setCoords(response.json.coords);
|
await actions.userSettings.getUserSettings();
|
||||||
await actions.userSettings.getUserSettings();
|
Toast.success(
|
||||||
Toast.success(
|
'Settings saved successfully. We will now start calculating distances for you. This may take a while and runs in the background.',
|
||||||
'Settings saved successfully. We will now start calculating distances for you. This may take a while and runs in the background.',
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Toast.error(response.json.error || 'Failed to save settings');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast.error(error.json?.error || 'Error while saving settings');
|
Toast.error(error.json?.error || 'Error while saving settings');
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,6 +85,33 @@ const UserSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SegmentPart>
|
</SegmentPart>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<SegmentPart
|
||||||
|
name="ImmoScout Details"
|
||||||
|
Icon={IconSearch}
|
||||||
|
helpText="When enabled, Fredy will fetch additional details (description, attributes, agent info) for each listing from ImmoScout. This provides richer notifications but makes an extra API call per listing."
|
||||||
|
>
|
||||||
|
<Banner
|
||||||
|
type="warning"
|
||||||
|
description="Enabling this feature significantly increases the number of API requests to ImmoScout. This raises the likelihood of being detected and rate-limited or blocked. Use at your own risk."
|
||||||
|
closeIcon={null}
|
||||||
|
style={{ marginBottom: '12px', maxWidth: '600px' }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<Switch
|
||||||
|
checked={!!immoscoutDetails}
|
||||||
|
onChange={async (checked) => {
|
||||||
|
try {
|
||||||
|
await actions.userSettings.setImmoscoutDetails(checked);
|
||||||
|
Toast.success('ImmoScout details setting updated.');
|
||||||
|
} catch {
|
||||||
|
Toast.error('Failed to update setting.');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>Fetch detailed ImmoScout listings</span>
|
||||||
|
</div>
|
||||||
|
</SegmentPart>
|
||||||
|
<Divider />
|
||||||
<div style={{ marginTop: '20px' }}>
|
<div style={{ marginTop: '20px' }}>
|
||||||
<Button icon={<IconSave />} theme="solid" type="primary" onClick={handleSave} loading={saving}>
|
<Button icon={<IconSave />} theme="solid" type="primary" onClick={handleSave} loading={saving}>
|
||||||
Save Settings
|
Save Settings
|
||||||
|
|||||||
571
yarn.lock
571
yarn.lock
@@ -904,34 +904,34 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
"@douyinfe/semi-animation-react@2.91.0":
|
"@douyinfe/semi-animation-react@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.91.0.tgz#b5f6bf54866751958ad9c00ccbca1bae2230e7af"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.92.2.tgz#733a51ca8eac16b988ea3ac497a54f325ec99793"
|
||||||
integrity sha512-fnUcGcTYBLvHxbzDdN3cK1f81dQ5JFyon3VKwEdduPQVKdDKeRCVyEOgzB+nAWoIme0/M5k3x5n/XunqSwtbIA==
|
integrity sha512-jO9zplm+he2bGSa93v6lIgvu1g86IGhd2vsfcOZJ+MToj6rUax309Zh1Dll1pF0PZPwMeGt5kCL31+dx5vB3GA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@douyinfe/semi-animation" "2.91.0"
|
"@douyinfe/semi-animation" "2.92.2"
|
||||||
"@douyinfe/semi-animation-styled" "2.91.0"
|
"@douyinfe/semi-animation-styled" "2.92.2"
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
|
|
||||||
"@douyinfe/semi-animation-styled@2.91.0":
|
"@douyinfe/semi-animation-styled@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.91.0.tgz#75b83f556155ab782ab31afe8acf887b967c3709"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.92.2.tgz#08ddf93765f23b9eb5b41ef8b95e0b5a161752af"
|
||||||
integrity sha512-7p79wtHNrDqKlcU0ZWLag8uYogyg3Ao5dy52bMvJrkshVjeWIQZKSxhkGLdtDE3DnJ3eaVEpAChXYrSSs9qO+g==
|
integrity sha512-NslE/t9CFC6zXTaor2/7kq+MIKU2FEMbmhNxNmEezIghD/FVgFvG224QDaT1i7WfZgOldoAEmu8ILn6Rgnw+uA==
|
||||||
|
|
||||||
"@douyinfe/semi-animation@2.91.0":
|
"@douyinfe/semi-animation@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.91.0.tgz#1e7488a6647c38694e08f7b9fdd38c99e45759c7"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.92.2.tgz#a4e8607641a6d44a2fa8fac81ed046420025186a"
|
||||||
integrity sha512-BxEi4BfUqRt26JkgGslOPUXOlsZ+KsaRyqVIGL+fFc3FsH621T75vQEoFfh/3a6k+zlrnFZVGgnz5GTeneS24w==
|
integrity sha512-5zYyzSDQqYByPehUZoJ9SuxbsEh62RJnWcwCP7VlYhE6cpior6KnlKcwtpGO0nuOo6o4LWS+TRuC0CGwBpkn5w==
|
||||||
dependencies:
|
dependencies:
|
||||||
bezier-easing "^2.1.0"
|
bezier-easing "^2.1.0"
|
||||||
|
|
||||||
"@douyinfe/semi-foundation@2.91.0":
|
"@douyinfe/semi-foundation@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.91.0.tgz#d7e80d3ec73d62948f983496e7074dd333fdd503"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.92.2.tgz#768aaf3548408cf29ad7a59877915d9b9d9da774"
|
||||||
integrity sha512-/8b3ce7zWsOPQUtz3wPqU5At7I8fNwgTrt7B4Rld3qZQ9XgxwnUMwaWPRgmJzI8BbHOG/TiCYAZp0iviLOzYAA==
|
integrity sha512-+enk5B1gUKA3uuXm6J3xj6l039aAgESbL91uixS6rOcUkOyaebglxJ0nuAxGFsslOcOUo0Kf/Z5kz64fTO1Y8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@douyinfe/semi-animation" "2.91.0"
|
"@douyinfe/semi-animation" "2.92.2"
|
||||||
"@douyinfe/semi-json-viewer-core" "2.91.0"
|
"@douyinfe/semi-json-viewer-core" "2.92.2"
|
||||||
"@mdx-js/mdx" "^3.0.1"
|
"@mdx-js/mdx" "^3.0.1"
|
||||||
async-validator "^3.5.0"
|
async-validator "^3.5.0"
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
@@ -945,44 +945,44 @@
|
|||||||
remark-gfm "^4.0.0"
|
remark-gfm "^4.0.0"
|
||||||
scroll-into-view-if-needed "^2.2.24"
|
scroll-into-view-if-needed "^2.2.24"
|
||||||
|
|
||||||
"@douyinfe/semi-icons@2.91.0", "@douyinfe/semi-icons@^2.91.0":
|
"@douyinfe/semi-icons@2.92.2", "@douyinfe/semi-icons@^2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.91.0.tgz#9dc5161280b19eae2ba62c3531325e9d220c4a90"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.92.2.tgz#27c91cb7e3a0da8103fd89692afea1897123dc48"
|
||||||
integrity sha512-ZqYUzTIFzLcQyo7no6DfeYYkH130Z9UKDxrmfQwYJGUuZZF0E8KwovSoFGk7tT60AgQLyskJlqRsdTkpULuuWQ==
|
integrity sha512-+fgK2Oe2+uokQZnrd+rHgksmfvDVvqT1fhbl49qJlDsg/xaXRxI2LOQRqBI+x0sOKabGKem+P3lY24Jgd/IGnQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
|
|
||||||
"@douyinfe/semi-illustrations@2.91.0":
|
"@douyinfe/semi-illustrations@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.91.0.tgz#ed079cf581cd41a2dfafd591fb7fbdca28efa54f"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.92.2.tgz#d5805578ec365f500ac3c4a7b593c334d7f4211a"
|
||||||
integrity sha512-4uGONV6JhSD3Lm9YDk2Dm1tYRY0O/fv+wnTEb8BuuWR/85nkNTkfo7gBNWhlxiN0nuUULRAzZV60snWoWI1Q2w==
|
integrity sha512-C120PwRKJ6SbK44O8JD8wVwaqBqyAFqB1wMAdS0iCCeQVuUgn1FkiZcBiEi/Y8PFWlTW9Bv/wWPtTz9LtEpjMw==
|
||||||
|
|
||||||
"@douyinfe/semi-json-viewer-core@2.91.0":
|
"@douyinfe/semi-json-viewer-core@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.91.0.tgz#073452d7fe2cfe427bc8767e5f29a4c54a05381a"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.92.2.tgz#0cfae0aa72b4e87f687ea22f5eb17e59680e7e24"
|
||||||
integrity sha512-H6xkFs0gmqJf1u9OOArF/2mYoV88oz71jwpWNUell154dcMD/c6ePLXyvtMpc/nfxWe5UFOMuoxOljAyoGOeQw==
|
integrity sha512-or92USy1c++nQjD+WAdGUbl/9HqfOFw7QxedPXAlHG5fWZfRfbyhIcEHqwa/SkZGb4po5S9MdjAks5yaj4KQsA==
|
||||||
dependencies:
|
dependencies:
|
||||||
jsonc-parser "^3.3.1"
|
jsonc-parser "^3.3.1"
|
||||||
|
|
||||||
"@douyinfe/semi-theme-default@2.91.0":
|
"@douyinfe/semi-theme-default@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.91.0.tgz#07ef454ebf6975c5120d9380583b61afc7deda8d"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.92.2.tgz#758cf1d04e46d95cbe9e2a1d9d8982c7d6bbf88c"
|
||||||
integrity sha512-Bd4jaD+zpt208bhSWnMAPcm2QOj3DcJ8lKzEuEwYO6l3q2cpcr902zn/crjyyxiePs/OEabocdCRUrYlJR7Zyw==
|
integrity sha512-8+Iq5vLkJDo0oQiwJyN6Nyon54ImDvUXwdX1qa3ex89zlUwApbFVFWH4uXN6huNhYS99qMIwJBs8egEeDGUYJQ==
|
||||||
|
|
||||||
"@douyinfe/semi-ui-19@^2.91.0":
|
"@douyinfe/semi-ui-19@^2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui-19/-/semi-ui-19-2.91.0.tgz#d25d5a92e63035be3ee930a8aa9cd116286b2b50"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui-19/-/semi-ui-19-2.92.2.tgz#e7caaf7f176d3bc1035962e24e8aff354579dc5d"
|
||||||
integrity sha512-dzmEYpvKU/xqCcXG/j5Q7z5Gfj+Y5lalTYGM8/BWfC5dhL4YvgJaGPCz/ZHXRAZdmBxKTFI4l4czmtzX0/TA6A==
|
integrity sha512-VUkdJWHCG6jwhqm0Lc0b+CgxZ7dFfpKsrm2gPcAK6OXemxrczpnShzwkpI1HfoBxAEdglb/bMbEWeQiy8jmESw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@dnd-kit/core" "^6.0.8"
|
"@dnd-kit/core" "^6.0.8"
|
||||||
"@dnd-kit/sortable" "^7.0.2"
|
"@dnd-kit/sortable" "^7.0.2"
|
||||||
"@dnd-kit/utilities" "^3.2.1"
|
"@dnd-kit/utilities" "^3.2.1"
|
||||||
"@douyinfe/semi-animation" "2.91.0"
|
"@douyinfe/semi-animation" "2.92.2"
|
||||||
"@douyinfe/semi-animation-react" "2.91.0"
|
"@douyinfe/semi-animation-react" "2.92.2"
|
||||||
"@douyinfe/semi-foundation" "2.91.0"
|
"@douyinfe/semi-foundation" "2.92.2"
|
||||||
"@douyinfe/semi-icons" "2.91.0"
|
"@douyinfe/semi-icons" "2.92.2"
|
||||||
"@douyinfe/semi-illustrations" "2.91.0"
|
"@douyinfe/semi-illustrations" "2.92.2"
|
||||||
"@douyinfe/semi-theme-default" "2.91.0"
|
"@douyinfe/semi-theme-default" "2.92.2"
|
||||||
"@tiptap/core" "^3.10.7"
|
"@tiptap/core" "^3.10.7"
|
||||||
"@tiptap/extension-document" "^3.10.7"
|
"@tiptap/extension-document" "^3.10.7"
|
||||||
"@tiptap/extension-hard-break" "^3.10.7"
|
"@tiptap/extension-hard-break" "^3.10.7"
|
||||||
@@ -1011,20 +1011,20 @@
|
|||||||
scroll-into-view-if-needed "^2.2.24"
|
scroll-into-view-if-needed "^2.2.24"
|
||||||
utility-types "^3.10.0"
|
utility-types "^3.10.0"
|
||||||
|
|
||||||
"@douyinfe/semi-ui@2.91.0":
|
"@douyinfe/semi-ui@2.92.2":
|
||||||
version "2.91.0"
|
version "2.92.2"
|
||||||
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.91.0.tgz#1959f587ce6185f5e2fac196a2ef17192cd4c622"
|
resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.92.2.tgz#f76613c9cf358fd9774134b70d821a3546e3d268"
|
||||||
integrity sha512-ZtTLa+sf6nfY6Z2X8jC6NVKkSiouNIoBIenvurkTaQn27E1RhR4PME5u24td5qXBRBcX4/0+67BpwT7GNWdCHA==
|
integrity sha512-2jS4pTN+cPXtD3WsS/RRreu5gYx1BTsDnaSAxG6z6adwsk17YuMD2PmJwwHu8gAc46I7Tv4AeqvvIBhyP0WewQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@dnd-kit/core" "^6.0.8"
|
"@dnd-kit/core" "^6.0.8"
|
||||||
"@dnd-kit/sortable" "^7.0.2"
|
"@dnd-kit/sortable" "^7.0.2"
|
||||||
"@dnd-kit/utilities" "^3.2.1"
|
"@dnd-kit/utilities" "^3.2.1"
|
||||||
"@douyinfe/semi-animation" "2.91.0"
|
"@douyinfe/semi-animation" "2.92.2"
|
||||||
"@douyinfe/semi-animation-react" "2.91.0"
|
"@douyinfe/semi-animation-react" "2.92.2"
|
||||||
"@douyinfe/semi-foundation" "2.91.0"
|
"@douyinfe/semi-foundation" "2.92.2"
|
||||||
"@douyinfe/semi-icons" "2.91.0"
|
"@douyinfe/semi-icons" "2.92.2"
|
||||||
"@douyinfe/semi-illustrations" "2.91.0"
|
"@douyinfe/semi-illustrations" "2.92.2"
|
||||||
"@douyinfe/semi-theme-default" "2.91.0"
|
"@douyinfe/semi-theme-default" "2.92.2"
|
||||||
"@tiptap/core" "^3.10.7"
|
"@tiptap/core" "^3.10.7"
|
||||||
"@tiptap/extension-document" "^3.10.7"
|
"@tiptap/extension-document" "^3.10.7"
|
||||||
"@tiptap/extension-hard-break" "^3.10.7"
|
"@tiptap/extension-hard-break" "^3.10.7"
|
||||||
@@ -1195,14 +1195,14 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
|
||||||
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
|
||||||
|
|
||||||
"@eslint/config-array@^0.23.0":
|
"@eslint/config-array@^0.23.3":
|
||||||
version "0.23.1"
|
version "0.23.3"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.1.tgz#908223da7b9148f1af5bfb3144b77a9387a89446"
|
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.3.tgz#3f4a93dd546169c09130cbd10f2415b13a20a219"
|
||||||
integrity sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==
|
integrity sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/object-schema" "^3.0.1"
|
"@eslint/object-schema" "^3.0.3"
|
||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
minimatch "^10.1.1"
|
minimatch "^10.2.4"
|
||||||
|
|
||||||
"@eslint/config-helpers@^0.5.2":
|
"@eslint/config-helpers@^0.5.2":
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
@@ -1218,22 +1218,29 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.15"
|
"@types/json-schema" "^7.0.15"
|
||||||
|
|
||||||
|
"@eslint/core@^1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.1.1.tgz#450f3d2be2d463ccd51119544092256b4e88df32"
|
||||||
|
integrity sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/json-schema" "^7.0.15"
|
||||||
|
|
||||||
"@eslint/js@^10.0.1":
|
"@eslint/js@^10.0.1":
|
||||||
version "10.0.1"
|
version "10.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-10.0.1.tgz#1e8a876f50117af8ab67e47d5ad94d38d6622583"
|
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-10.0.1.tgz#1e8a876f50117af8ab67e47d5ad94d38d6622583"
|
||||||
integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==
|
integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==
|
||||||
|
|
||||||
"@eslint/object-schema@^3.0.1":
|
"@eslint/object-schema@^3.0.3":
|
||||||
version "3.0.1"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.1.tgz#9a1dc9af00d790dc79a9bf57a756e3cb2740ddb9"
|
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.3.tgz#5bf671e52e382e4adc47a9906f2699374637db6b"
|
||||||
integrity sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==
|
integrity sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==
|
||||||
|
|
||||||
"@eslint/plugin-kit@^0.6.0":
|
"@eslint/plugin-kit@^0.6.1":
|
||||||
version "0.6.0"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz#e0cb12ec66719cb2211ad36499fb516f2a63899d"
|
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz#eb9e6689b56ce8bc1855bb33090e63f3fc115e8e"
|
||||||
integrity sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==
|
integrity sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/core" "^1.1.0"
|
"@eslint/core" "^1.1.1"
|
||||||
levn "^0.4.1"
|
levn "^0.4.1"
|
||||||
|
|
||||||
"@floating-ui/core@^1.7.3":
|
"@floating-ui/core@^1.7.3":
|
||||||
@@ -1284,18 +1291,6 @@
|
|||||||
resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz"
|
resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz"
|
||||||
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
||||||
|
|
||||||
"@isaacs/balanced-match@^4.0.1":
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29"
|
|
||||||
integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==
|
|
||||||
|
|
||||||
"@isaacs/brace-expansion@^5.0.1":
|
|
||||||
version "5.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz#0ef5a92d91f2fff2a37646ce54da9e5f599f6eff"
|
|
||||||
integrity sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==
|
|
||||||
dependencies:
|
|
||||||
"@isaacs/balanced-match" "^4.0.1"
|
|
||||||
|
|
||||||
"@isaacs/cliui@^8.0.2":
|
"@isaacs/cliui@^8.0.2":
|
||||||
version "8.0.2"
|
version "8.0.2"
|
||||||
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
||||||
@@ -1347,6 +1342,18 @@
|
|||||||
resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz"
|
resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz"
|
||||||
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
|
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
|
||||||
|
|
||||||
|
"@mapbox/geojson-area@^0.2.2":
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10"
|
||||||
|
integrity sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA==
|
||||||
|
dependencies:
|
||||||
|
wgs84 "0.0.0"
|
||||||
|
|
||||||
|
"@mapbox/geojson-normalize@^0.0.1":
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz#1da1e6b3a7add3ad29909b30f438f60581b7cd80"
|
||||||
|
integrity sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q==
|
||||||
|
|
||||||
"@mapbox/geojson-rewind@^0.5.2":
|
"@mapbox/geojson-rewind@^0.5.2":
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz"
|
resolved "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz"
|
||||||
@@ -1360,6 +1367,18 @@
|
|||||||
resolved "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz"
|
resolved "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz"
|
||||||
integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==
|
integrity sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==
|
||||||
|
|
||||||
|
"@mapbox/mapbox-gl-draw@^1.5.1":
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.5.1.tgz#0081164556fbe8c444e1297f63c02ecd8e442630"
|
||||||
|
integrity sha512-DnR/oarZVoIrVHssAn+mtpuGzYH+ebORoPjow46zTBNPod/HQnvIZGtL6hIb5BVWxxH49RC9D20ipxiO9WDRxA==
|
||||||
|
dependencies:
|
||||||
|
"@mapbox/geojson-area" "^0.2.2"
|
||||||
|
"@mapbox/geojson-normalize" "^0.0.1"
|
||||||
|
"@mapbox/point-geometry" "^1.1.0"
|
||||||
|
"@turf/projection" "^7.2.0"
|
||||||
|
fast-deep-equal "^3.1.3"
|
||||||
|
nanoid "^5.0.9"
|
||||||
|
|
||||||
"@mapbox/point-geometry@^1.1.0", "@mapbox/point-geometry@~1.1.0":
|
"@mapbox/point-geometry@^1.1.0", "@mapbox/point-geometry@~1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz"
|
||||||
@@ -1470,16 +1489,16 @@
|
|||||||
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
|
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
|
||||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||||
|
|
||||||
"@puppeteer/browsers@2.12.0":
|
"@puppeteer/browsers@2.13.0":
|
||||||
version "2.12.0"
|
version "2.13.0"
|
||||||
resolved "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz"
|
resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.13.0.tgz#10f980c6d65efeff77f8a3cac6e1a7ac10604500"
|
||||||
integrity sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==
|
integrity sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
extract-zip "^2.0.1"
|
extract-zip "^2.0.1"
|
||||||
progress "^2.0.3"
|
progress "^2.0.3"
|
||||||
proxy-agent "^6.5.0"
|
proxy-agent "^6.5.0"
|
||||||
semver "^7.7.3"
|
semver "^7.7.4"
|
||||||
tar-fs "^3.1.1"
|
tar-fs "^3.1.1"
|
||||||
yargs "^17.7.2"
|
yargs "^17.7.2"
|
||||||
|
|
||||||
@@ -1616,6 +1635,11 @@
|
|||||||
"@sendgrid/client" "^8.1.5"
|
"@sendgrid/client" "^8.1.5"
|
||||||
"@sendgrid/helpers" "^8.0.0"
|
"@sendgrid/helpers" "^8.0.0"
|
||||||
|
|
||||||
|
"@stablelib/base64@^1.0.0":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.1.tgz#bdfc1c6d3a62d7a3b7bbc65b6cce1bb4561641be"
|
||||||
|
integrity sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==
|
||||||
|
|
||||||
"@tiptap/core@^3.10.7":
|
"@tiptap/core@^3.10.7":
|
||||||
version "3.16.0"
|
version "3.16.0"
|
||||||
resolved "https://registry.npmjs.org/@tiptap/core/-/core-3.16.0.tgz"
|
resolved "https://registry.npmjs.org/@tiptap/core/-/core-3.16.0.tgz"
|
||||||
@@ -1890,6 +1914,63 @@
|
|||||||
resolved "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz"
|
resolved "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz"
|
||||||
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
||||||
|
|
||||||
|
"@turf/boolean-point-in-polygon@^7.3.4":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.4.tgz#654a940939fecddf1887ca4c95bd5a2f07a42de8"
|
||||||
|
integrity sha512-v/4hfyY90Vz9cDgs2GwjQf+Lft8o7mNCLJOTz/iv8SHAIgMMX0czEoIaNVOJr7tBqPqwin1CGwsncrkf5C9n8Q==
|
||||||
|
dependencies:
|
||||||
|
"@turf/helpers" "7.3.4"
|
||||||
|
"@turf/invariant" "7.3.4"
|
||||||
|
"@types/geojson" "^7946.0.10"
|
||||||
|
point-in-polygon-hao "^1.1.0"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
|
"@turf/clone@7.3.4":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@turf/clone/-/clone-7.3.4.tgz#ae2d9ccd77730181aaa76874308140515e55ddaa"
|
||||||
|
integrity sha512-pwQ+RyQw986uu7IulY/18NRAebwZZScb084bvVqVkTrllwLSv4oVBqUxmUMiwtp+PNdiRGRFOvNyZqtRsiD+Jw==
|
||||||
|
dependencies:
|
||||||
|
"@turf/helpers" "7.3.4"
|
||||||
|
"@types/geojson" "^7946.0.10"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
|
"@turf/helpers@7.3.4":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-7.3.4.tgz#a8c918981599dddcf452421c7b307c5832d05f02"
|
||||||
|
integrity sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==
|
||||||
|
dependencies:
|
||||||
|
"@types/geojson" "^7946.0.10"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
|
"@turf/invariant@7.3.4":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-7.3.4.tgz#d81f448aa4fdda36047337a688517581e91c12f0"
|
||||||
|
integrity sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ==
|
||||||
|
dependencies:
|
||||||
|
"@turf/helpers" "7.3.4"
|
||||||
|
"@types/geojson" "^7946.0.10"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
|
"@turf/meta@7.3.4":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-7.3.4.tgz#8e917d29de9da96a0f95f3f16119ba9abde7dee6"
|
||||||
|
integrity sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw==
|
||||||
|
dependencies:
|
||||||
|
"@turf/helpers" "7.3.4"
|
||||||
|
"@types/geojson" "^7946.0.10"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
|
"@turf/projection@^7.2.0":
|
||||||
|
version "7.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@turf/projection/-/projection-7.3.4.tgz#05483cf34e711bf139fc22e236bed2068f1b5461"
|
||||||
|
integrity sha512-p91zOaLmzoBHzU/2H6Ot1tOhTmAom85n1P7I4Oo0V9xU8hmJXWfNnomLFf/6rnkKDIFZkncLQIBz4iIecZ61sA==
|
||||||
|
dependencies:
|
||||||
|
"@turf/clone" "7.3.4"
|
||||||
|
"@turf/helpers" "7.3.4"
|
||||||
|
"@turf/meta" "7.3.4"
|
||||||
|
"@types/geojson" "^7946.0.10"
|
||||||
|
tslib "^2.8.1"
|
||||||
|
|
||||||
"@types/babel__core@^7.20.5":
|
"@types/babel__core@^7.20.5":
|
||||||
version "7.20.5"
|
version "7.20.5"
|
||||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||||
@@ -1947,7 +2028,7 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
||||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||||
|
|
||||||
"@types/geojson@*", "@types/geojson@^7946.0.16":
|
"@types/geojson@*", "@types/geojson@^7946.0.10", "@types/geojson@^7946.0.16":
|
||||||
version "7946.0.16"
|
version "7946.0.16"
|
||||||
resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz"
|
resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz"
|
||||||
integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
|
integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
|
||||||
@@ -2057,11 +2138,16 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
|
|||||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||||
|
|
||||||
acorn@^8.0.0, acorn@^8.15.0:
|
acorn@^8.0.0:
|
||||||
version "8.15.0"
|
version "8.15.0"
|
||||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||||
|
|
||||||
|
acorn@^8.16.0:
|
||||||
|
version "8.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
|
||||||
|
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||||
|
|
||||||
adm-zip@^0.5.16:
|
adm-zip@^0.5.16:
|
||||||
version "0.5.16"
|
version "0.5.16"
|
||||||
resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz"
|
resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz"
|
||||||
@@ -2072,10 +2158,10 @@ agent-base@^7.1.0, agent-base@^7.1.2:
|
|||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
||||||
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
|
integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
|
||||||
|
|
||||||
ajv@^6.12.4:
|
ajv@^6.14.0:
|
||||||
version "6.12.6"
|
version "6.14.0"
|
||||||
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
|
||||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal "^3.1.1"
|
fast-deep-equal "^3.1.1"
|
||||||
fast-json-stable-stringify "^2.0.0"
|
fast-json-stable-stringify "^2.0.0"
|
||||||
@@ -2298,6 +2384,11 @@ balanced-match@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
balanced-match@^4.0.2:
|
||||||
|
version "4.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
|
||||||
|
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||||
|
|
||||||
bare-events@^2.5.4, bare-events@^2.7.0:
|
bare-events@^2.5.4, bare-events@^2.7.0:
|
||||||
version "2.8.2"
|
version "2.8.2"
|
||||||
resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz"
|
resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz"
|
||||||
@@ -2429,6 +2520,13 @@ brace-expansion@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
|
|
||||||
|
brace-expansion@^5.0.2:
|
||||||
|
version "5.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336"
|
||||||
|
integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==
|
||||||
|
dependencies:
|
||||||
|
balanced-match "^4.0.2"
|
||||||
|
|
||||||
braces@^3.0.3, braces@~3.0.2:
|
braces@^3.0.3, braces@~3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
|
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
|
||||||
@@ -2617,10 +2715,10 @@ chownr@^1.1.1:
|
|||||||
resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz"
|
resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz"
|
||||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||||
|
|
||||||
chromium-bidi@13.1.1:
|
chromium-bidi@14.0.0:
|
||||||
version "13.1.1"
|
version "14.0.0"
|
||||||
resolved "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.1.tgz"
|
resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-14.0.0.tgz#15a12ab083ae519a49a724e94994ca0a9ced9c8e"
|
||||||
integrity sha512-zB9MpoPd7VJwjowQqiW3FKOvQwffFMjQ8Iejp5ZW+sJaKLRhZX1sTxzl3Zt22TDB4zP0OOqs8lRoY7eAW5geyQ==
|
integrity sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==
|
||||||
dependencies:
|
dependencies:
|
||||||
mitt "^3.0.1"
|
mitt "^3.0.1"
|
||||||
zod "^3.24.1"
|
zod "^3.24.1"
|
||||||
@@ -2709,10 +2807,10 @@ comma-separated-tokens@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz"
|
resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz"
|
||||||
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
||||||
|
|
||||||
commander@^14.0.2:
|
commander@^14.0.3:
|
||||||
version "14.0.2"
|
version "14.0.3"
|
||||||
resolved "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.3.tgz#425d79b48f9af82fcd9e4fc1ea8af6c5ec07bbc2"
|
||||||
integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==
|
integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==
|
||||||
|
|
||||||
compute-scroll-into-view@^1.0.20:
|
compute-scroll-into-view@^1.0.20:
|
||||||
version "1.0.20"
|
version "1.0.20"
|
||||||
@@ -2977,10 +3075,10 @@ devlop@^1.0.0, devlop@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal "^2.0.0"
|
dequal "^2.0.0"
|
||||||
|
|
||||||
devtools-protocol@0.0.1566079:
|
devtools-protocol@0.0.1581282:
|
||||||
version "0.0.1566079"
|
version "0.0.1581282"
|
||||||
resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz"
|
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz#7f289b837e052ad04eb16e9575877801c2b3716c"
|
||||||
integrity sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==
|
integrity sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==
|
||||||
|
|
||||||
diff@^7.0.0:
|
diff@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
@@ -3377,10 +3475,10 @@ eslint-scope@5.1.1:
|
|||||||
esrecurse "^4.3.0"
|
esrecurse "^4.3.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-scope@^9.1.0:
|
eslint-scope@^9.1.2:
|
||||||
version "9.1.0"
|
version "9.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.0.tgz#dfcb41d6c0d73df6b977a50cf3e91c41ddb4154e"
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802"
|
||||||
integrity sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==
|
integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/esrecurse" "^4.3.1"
|
"@types/esrecurse" "^4.3.1"
|
||||||
"@types/estree" "^1.0.8"
|
"@types/estree" "^1.0.8"
|
||||||
@@ -3397,33 +3495,33 @@ eslint-visitor-keys@^3.4.3:
|
|||||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
|
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
|
||||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||||
|
|
||||||
eslint-visitor-keys@^5.0.0:
|
eslint-visitor-keys@^5.0.1:
|
||||||
version "5.0.0"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9"
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
|
||||||
integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==
|
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
|
||||||
|
|
||||||
eslint@10.0.0:
|
eslint@10.0.3:
|
||||||
version "10.0.0"
|
version "10.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.0.tgz#c93c36a96d91621d0fbb680db848ea11af56ab1e"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.0.3.tgz#360a7de7f2706eb8a32caa17ca983f0089efe694"
|
||||||
integrity sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==
|
integrity sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils" "^4.8.0"
|
"@eslint-community/eslint-utils" "^4.8.0"
|
||||||
"@eslint-community/regexpp" "^4.12.2"
|
"@eslint-community/regexpp" "^4.12.2"
|
||||||
"@eslint/config-array" "^0.23.0"
|
"@eslint/config-array" "^0.23.3"
|
||||||
"@eslint/config-helpers" "^0.5.2"
|
"@eslint/config-helpers" "^0.5.2"
|
||||||
"@eslint/core" "^1.1.0"
|
"@eslint/core" "^1.1.1"
|
||||||
"@eslint/plugin-kit" "^0.6.0"
|
"@eslint/plugin-kit" "^0.6.1"
|
||||||
"@humanfs/node" "^0.16.6"
|
"@humanfs/node" "^0.16.6"
|
||||||
"@humanwhocodes/module-importer" "^1.0.1"
|
"@humanwhocodes/module-importer" "^1.0.1"
|
||||||
"@humanwhocodes/retry" "^0.4.2"
|
"@humanwhocodes/retry" "^0.4.2"
|
||||||
"@types/estree" "^1.0.6"
|
"@types/estree" "^1.0.6"
|
||||||
ajv "^6.12.4"
|
ajv "^6.14.0"
|
||||||
cross-spawn "^7.0.6"
|
cross-spawn "^7.0.6"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
escape-string-regexp "^4.0.0"
|
escape-string-regexp "^4.0.0"
|
||||||
eslint-scope "^9.1.0"
|
eslint-scope "^9.1.2"
|
||||||
eslint-visitor-keys "^5.0.0"
|
eslint-visitor-keys "^5.0.1"
|
||||||
espree "^11.1.0"
|
espree "^11.1.1"
|
||||||
esquery "^1.7.0"
|
esquery "^1.7.0"
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
@@ -3434,7 +3532,7 @@ eslint@10.0.0:
|
|||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
is-glob "^4.0.0"
|
is-glob "^4.0.0"
|
||||||
json-stable-stringify-without-jsonify "^1.0.1"
|
json-stable-stringify-without-jsonify "^1.0.1"
|
||||||
minimatch "^10.1.1"
|
minimatch "^10.2.4"
|
||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
optionator "^0.9.3"
|
optionator "^0.9.3"
|
||||||
|
|
||||||
@@ -3443,14 +3541,14 @@ esmock@2.7.3:
|
|||||||
resolved "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz"
|
resolved "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz"
|
||||||
integrity sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==
|
integrity sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==
|
||||||
|
|
||||||
espree@^11.1.0:
|
espree@^11.1.1:
|
||||||
version "11.1.0"
|
version "11.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-11.1.0.tgz#7d0c82a69f8df670728dba256264b383fbf73e8f"
|
resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5"
|
||||||
integrity sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==
|
integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.15.0"
|
acorn "^8.16.0"
|
||||||
acorn-jsx "^5.3.2"
|
acorn-jsx "^5.3.2"
|
||||||
eslint-visitor-keys "^5.0.0"
|
eslint-visitor-keys "^5.0.1"
|
||||||
|
|
||||||
esprima@^4.0.1:
|
esprima@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
@@ -3608,6 +3706,11 @@ fast-levenshtein@^2.0.6:
|
|||||||
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
||||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||||
|
|
||||||
|
fast-sha256@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-sha256/-/fast-sha256-1.3.0.tgz#7916ba2054eeb255982608cccd0f6660c79b7ae6"
|
||||||
|
integrity sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==
|
||||||
|
|
||||||
fd-slicer@~1.1.0:
|
fd-slicer@~1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
|
||||||
@@ -3911,10 +4014,10 @@ glob@^7.0.0, glob@^7.1.3:
|
|||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
globals@^17.3.0:
|
globals@^17.4.0:
|
||||||
version "17.3.0"
|
version "17.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-17.4.0.tgz#33d7d297ed1536b388a0e2f4bcd0ff19c8ff91b5"
|
||||||
integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==
|
integrity sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==
|
||||||
|
|
||||||
globalthis@^1.0.4:
|
globalthis@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
@@ -4668,18 +4771,17 @@ linkifyjs@^4.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
|
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
|
||||||
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
|
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
|
||||||
|
|
||||||
lint-staged@16.2.7:
|
lint-staged@16.3.2:
|
||||||
version "16.2.7"
|
version "16.3.2"
|
||||||
resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz"
|
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.3.2.tgz#378b48c6c340d42eefcc8d13d198b61a562e63a9"
|
||||||
integrity sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==
|
integrity sha512-xKqhC2AeXLwiAHXguxBjuChoTTWFC6Pees0SHPwOpwlvI3BH7ZADFPddAdN3pgo3aiKgPUx/bxE78JfUnxQnlg==
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "^14.0.2"
|
commander "^14.0.3"
|
||||||
listr2 "^9.0.5"
|
listr2 "^9.0.5"
|
||||||
micromatch "^4.0.8"
|
micromatch "^4.0.8"
|
||||||
nano-spawn "^2.0.0"
|
|
||||||
pidtree "^0.6.0"
|
|
||||||
string-argv "^0.3.2"
|
string-argv "^0.3.2"
|
||||||
yaml "^2.8.1"
|
tinyexec "^1.0.2"
|
||||||
|
yaml "^2.8.2"
|
||||||
|
|
||||||
listr2@^9.0.5:
|
listr2@^9.0.5:
|
||||||
version "9.0.5"
|
version "9.0.5"
|
||||||
@@ -4776,10 +4878,10 @@ make-dir@^2.1.0:
|
|||||||
pify "^4.0.1"
|
pify "^4.0.1"
|
||||||
semver "^5.6.0"
|
semver "^5.6.0"
|
||||||
|
|
||||||
maplibre-gl@^5.18.0:
|
maplibre-gl@^5.19.0:
|
||||||
version "5.18.0"
|
version "5.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-5.18.0.tgz#0542080eb6e034f22f9750b8786d00b1642ea7e5"
|
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-5.19.0.tgz#70f6af711d61a4a7405c4b7bc8fffce5dd6c11b1"
|
||||||
integrity sha512-UtWxPBpHuFvEkM+5FVfcFG9ZKEWZQI6+PZkvLErr8Zs5ux+O7/KQ3JjSUvAfOlMeMgd/77qlHpOw0yHL7JU5cw==
|
integrity sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@mapbox/geojson-rewind" "^0.5.2"
|
"@mapbox/geojson-rewind" "^0.5.2"
|
||||||
"@mapbox/jsonlint-lines-primitives" "^2.0.2"
|
"@mapbox/jsonlint-lines-primitives" "^2.0.2"
|
||||||
@@ -5460,12 +5562,12 @@ mimic-response@^3.1.0:
|
|||||||
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz"
|
||||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||||
|
|
||||||
minimatch@^10.1.1:
|
minimatch@^10.2.1, minimatch@^10.2.4:
|
||||||
version "10.1.2"
|
version "10.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.2.tgz#6c3f289f9de66d628fa3feb1842804396a43d81c"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
|
||||||
integrity sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==
|
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@isaacs/brace-expansion" "^5.0.1"
|
brace-expansion "^5.0.2"
|
||||||
|
|
||||||
minimatch@^3.1.1, minimatch@^3.1.2:
|
minimatch@^3.1.1, minimatch@^3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
@@ -5546,12 +5648,7 @@ murmurhash-js@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz"
|
||||||
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
|
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
|
||||||
|
|
||||||
nano-spawn@^2.0.0:
|
nanoid@5.1.6, nanoid@^5.0.9:
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz"
|
|
||||||
integrity sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==
|
|
||||||
|
|
||||||
nanoid@5.1.6:
|
|
||||||
version "5.1.6"
|
version "5.1.6"
|
||||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz"
|
resolved "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz"
|
||||||
integrity sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==
|
integrity sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==
|
||||||
@@ -5629,15 +5726,15 @@ node-releases@^2.0.27:
|
|||||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz"
|
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz"
|
||||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||||
|
|
||||||
nodemon@^3.1.11:
|
nodemon@^3.1.14:
|
||||||
version "3.1.11"
|
version "3.1.14"
|
||||||
resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz"
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51"
|
||||||
integrity sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==
|
integrity sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar "^3.5.2"
|
chokidar "^3.5.2"
|
||||||
debug "^4"
|
debug "^4"
|
||||||
ignore-by-default "^1.0.1"
|
ignore-by-default "^1.0.1"
|
||||||
minimatch "^3.1.2"
|
minimatch "^10.2.1"
|
||||||
pstree.remy "^1.1.8"
|
pstree.remy "^1.1.8"
|
||||||
semver "^7.5.3"
|
semver "^7.5.3"
|
||||||
simple-update-notifier "^2.0.0"
|
simple-update-notifier "^2.0.0"
|
||||||
@@ -5936,21 +6033,28 @@ picomatch@^4.0.3:
|
|||||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||||
|
|
||||||
pidtree@^0.6.0:
|
|
||||||
version "0.6.0"
|
|
||||||
resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz"
|
|
||||||
integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==
|
|
||||||
|
|
||||||
pify@^4.0.1:
|
pify@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
|
resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
|
||||||
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
||||||
|
|
||||||
|
point-in-polygon-hao@^1.1.0:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz#8662abdcc84bcca230cc3ecbb0b0ab1a306f1bd6"
|
||||||
|
integrity sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==
|
||||||
|
dependencies:
|
||||||
|
robust-predicates "^3.0.2"
|
||||||
|
|
||||||
possible-typed-array-names@^1.0.0:
|
possible-typed-array-names@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz"
|
||||||
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
|
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
|
||||||
|
|
||||||
|
postal-mime@2.7.3:
|
||||||
|
version "2.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/postal-mime/-/postal-mime-2.7.3.tgz#358d92192656a262568ffc7a441a713131aa1272"
|
||||||
|
integrity sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==
|
||||||
|
|
||||||
postcss@^8.5.6:
|
postcss@^8.5.6:
|
||||||
version "8.5.6"
|
version "8.5.6"
|
||||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
|
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
|
||||||
@@ -6223,17 +6327,17 @@ punycode@^2.1.0:
|
|||||||
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
|
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
puppeteer-core@24.37.2:
|
puppeteer-core@24.38.0:
|
||||||
version "24.37.2"
|
version "24.38.0"
|
||||||
resolved "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.2.tgz"
|
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-24.38.0.tgz#456393366c84b9159f7f43aa1fcab4dc90116e11"
|
||||||
integrity sha512-nN8qwE3TGF2vA/+xemPxbesntTuqD9vCGOiZL2uh8HES3pPzLX20MyQjB42dH2rhQ3W3TljZ4ZaKZ0yX/abQuw==
|
integrity sha512-zB3S/tksIhgi2gZRndUe07AudBz5SXOB7hqG0kEa9/YXWrGwlVlYm3tZtwKgfRftBzbmLQl5iwHkQQl04n/mWw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@puppeteer/browsers" "2.12.0"
|
"@puppeteer/browsers" "2.13.0"
|
||||||
chromium-bidi "13.1.1"
|
chromium-bidi "14.0.0"
|
||||||
debug "^4.4.3"
|
debug "^4.4.3"
|
||||||
devtools-protocol "0.0.1566079"
|
devtools-protocol "0.0.1581282"
|
||||||
typed-query-selector "^2.12.0"
|
typed-query-selector "^2.12.1"
|
||||||
webdriver-bidi-protocol "0.4.0"
|
webdriver-bidi-protocol "0.4.1"
|
||||||
ws "^8.19.0"
|
ws "^8.19.0"
|
||||||
|
|
||||||
puppeteer-extra-plugin-stealth@^2.11.2:
|
puppeteer-extra-plugin-stealth@^2.11.2:
|
||||||
@@ -6283,17 +6387,17 @@ puppeteer-extra@^3.3.6:
|
|||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
deepmerge "^4.2.2"
|
deepmerge "^4.2.2"
|
||||||
|
|
||||||
puppeteer@^24.37.2:
|
puppeteer@^24.38.0:
|
||||||
version "24.37.2"
|
version "24.38.0"
|
||||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-24.37.2.tgz#059a486d1db6d16ea58c4456eba896b27946656f"
|
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-24.38.0.tgz#abf7665c73e408598860a94fc268e4ebe0ea21b2"
|
||||||
integrity sha512-FV1W/919ve0y0oiS/3Rp5XY4MUNUokpZOH/5M4MMDfrrvh6T9VbdKvAHrAFHBuCxvluDxhjra20W7Iz6HJUcIQ==
|
integrity sha512-abnJOBVoL9PQTLKSbYGm9mjNFyIPaTVj77J/6cS370dIQtcZMpx8wyZoAuBzR71Aoon6yvI71NEVFUsl3JU82g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@puppeteer/browsers" "2.12.0"
|
"@puppeteer/browsers" "2.13.0"
|
||||||
chromium-bidi "13.1.1"
|
chromium-bidi "14.0.0"
|
||||||
cosmiconfig "^9.0.0"
|
cosmiconfig "^9.0.0"
|
||||||
devtools-protocol "0.0.1566079"
|
devtools-protocol "0.0.1581282"
|
||||||
puppeteer-core "24.37.2"
|
puppeteer-core "24.38.0"
|
||||||
typed-query-selector "^2.12.0"
|
typed-query-selector "^2.12.1"
|
||||||
|
|
||||||
qs@^6.14.1:
|
qs@^6.14.1:
|
||||||
version "6.14.1"
|
version "6.14.1"
|
||||||
@@ -6394,17 +6498,17 @@ react-resizable@^3.0.5:
|
|||||||
prop-types "15.x"
|
prop-types "15.x"
|
||||||
react-draggable "^4.0.3"
|
react-draggable "^4.0.3"
|
||||||
|
|
||||||
react-router-dom@7.13.0:
|
react-router-dom@7.13.1:
|
||||||
version "7.13.0"
|
version "7.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz"
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.13.1.tgz#74c045acc333ca94612b889cd1b1e1ee9534dead"
|
||||||
integrity sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==
|
integrity sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
react-router "7.13.0"
|
react-router "7.13.1"
|
||||||
|
|
||||||
react-router@7.13.0:
|
react-router@7.13.1:
|
||||||
version "7.13.0"
|
version "7.13.1"
|
||||||
resolved "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.13.1.tgz#5e2b3ebafd6c78d9775e135474bf5060645077f7"
|
||||||
integrity sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==
|
integrity sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie "^1.0.1"
|
cookie "^1.0.1"
|
||||||
set-cookie-parser "^2.6.0"
|
set-cookie-parser "^2.6.0"
|
||||||
@@ -6621,6 +6725,14 @@ require-directory@^2.1.1:
|
|||||||
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
||||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||||
|
|
||||||
|
resend@^6.9.3:
|
||||||
|
version "6.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/resend/-/resend-6.9.3.tgz#4eb7f235b8f0049ee9f0208587939b8c953c6d5f"
|
||||||
|
integrity sha512-GRXjH9XZBJA+daH7bBVDuTShr22iWCxXA8P7t495G4dM/RC+d+3gHBK/6bz9K6Vpcq11zRQKmD+B+jECwQlyGQ==
|
||||||
|
dependencies:
|
||||||
|
postal-mime "2.7.3"
|
||||||
|
svix "1.84.1"
|
||||||
|
|
||||||
resolve-from@^4.0.0:
|
resolve-from@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||||
@@ -6678,6 +6790,11 @@ rimraf@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
robust-predicates@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||||
|
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
|
||||||
|
|
||||||
rollup@^4.43.0:
|
rollup@^4.43.0:
|
||||||
version "4.49.0"
|
version "4.49.0"
|
||||||
resolved "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz"
|
resolved "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz"
|
||||||
@@ -6787,11 +6904,6 @@ semver@^7.3.5, semver@^7.5.3:
|
|||||||
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
||||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||||
|
|
||||||
semver@^7.7.3:
|
|
||||||
version "7.7.3"
|
|
||||||
resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz"
|
|
||||||
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
|
|
||||||
|
|
||||||
semver@^7.7.4:
|
semver@^7.7.4:
|
||||||
version "7.7.4"
|
version "7.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||||
@@ -7031,6 +7143,14 @@ split-on-first@^3.0.0:
|
|||||||
resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz"
|
||||||
integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==
|
integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==
|
||||||
|
|
||||||
|
standardwebhooks@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/standardwebhooks/-/standardwebhooks-1.0.0.tgz#5faa23ceacbf9accd344361101d9e3033b64324f"
|
||||||
|
integrity sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==
|
||||||
|
dependencies:
|
||||||
|
"@stablelib/base64" "^1.0.0"
|
||||||
|
fast-sha256 "^1.3.0"
|
||||||
|
|
||||||
statuses@2.0.1:
|
statuses@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
|
||||||
@@ -7259,6 +7379,14 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
|
svix@1.84.1:
|
||||||
|
version "1.84.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/svix/-/svix-1.84.1.tgz#9e086455acf01143fe0f90c5f618393c3e3591cf"
|
||||||
|
integrity sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==
|
||||||
|
dependencies:
|
||||||
|
standardwebhooks "1.0.0"
|
||||||
|
uuid "^10.0.0"
|
||||||
|
|
||||||
tar-fs@^2.0.0:
|
tar-fs@^2.0.0:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz"
|
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz"
|
||||||
@@ -7312,6 +7440,11 @@ tiny-json-http@^7.0.2:
|
|||||||
resolved "https://registry.npmjs.org/tiny-json-http/-/tiny-json-http-7.5.1.tgz"
|
resolved "https://registry.npmjs.org/tiny-json-http/-/tiny-json-http-7.5.1.tgz"
|
||||||
integrity sha512-lB7qkBGpL3HR/8gidBu3MMfgfnDj2mlvK/eYXgSbO06gKphemLKGp/TgRTy/BKVD7nCbgIeCm41lMNayXO1f2w==
|
integrity sha512-lB7qkBGpL3HR/8gidBu3MMfgfnDj2mlvK/eYXgSbO06gKphemLKGp/TgRTy/BKVD7nCbgIeCm41lMNayXO1f2w==
|
||||||
|
|
||||||
|
tinyexec@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.2.tgz#bdd2737fe2ba40bd6f918ae26642f264b99ca251"
|
||||||
|
integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==
|
||||||
|
|
||||||
tinyglobby@^0.2.15:
|
tinyglobby@^0.2.15:
|
||||||
version "0.2.15"
|
version "0.2.15"
|
||||||
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
|
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
|
||||||
@@ -7359,7 +7492,7 @@ trouter@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
regexparam "^3.0.0"
|
regexparam "^3.0.0"
|
||||||
|
|
||||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.3.0:
|
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.3.0, tslib@^2.8.1:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||||
@@ -7437,10 +7570,10 @@ typed-array-length@^1.0.7:
|
|||||||
possible-typed-array-names "^1.0.0"
|
possible-typed-array-names "^1.0.0"
|
||||||
reflect.getprototypeof "^1.0.6"
|
reflect.getprototypeof "^1.0.6"
|
||||||
|
|
||||||
typed-query-selector@^2.12.0:
|
typed-query-selector@^2.12.1:
|
||||||
version "2.12.0"
|
version "2.12.1"
|
||||||
resolved "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz"
|
resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.1.tgz#04423bfb71b8f3aee3df1c29598ed6c7c8f55284"
|
||||||
integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==
|
integrity sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==
|
||||||
|
|
||||||
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
@@ -7603,6 +7736,11 @@ utility-types@^3.10.0:
|
|||||||
resolved "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz"
|
resolved "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz"
|
||||||
integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
|
integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
|
||||||
|
|
||||||
|
uuid@^10.0.0:
|
||||||
|
version "10.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||||
|
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||||
|
|
||||||
vfile-message@^4.0.0:
|
vfile-message@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz"
|
||||||
@@ -7643,10 +7781,15 @@ web-streams-polyfill@^3.0.3:
|
|||||||
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz"
|
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz"
|
||||||
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
|
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
|
||||||
|
|
||||||
webdriver-bidi-protocol@0.4.0:
|
webdriver-bidi-protocol@0.4.1:
|
||||||
version "0.4.0"
|
version "0.4.1"
|
||||||
resolved "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz"
|
resolved "https://registry.yarnpkg.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz#d411e7b8e158408d83bb166b0b4f1054fa3f077e"
|
||||||
integrity sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==
|
integrity sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==
|
||||||
|
|
||||||
|
wgs84@0.0.0:
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wgs84/-/wgs84-0.0.0.tgz#34fdc555917b6e57cf2a282ed043710c049cdc76"
|
||||||
|
integrity sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ==
|
||||||
|
|
||||||
whatwg-encoding@^3.1.1:
|
whatwg-encoding@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
@@ -7799,10 +7942,10 @@ yallist@^3.0.2:
|
|||||||
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
||||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||||
|
|
||||||
yaml@^2.8.1:
|
yaml@^2.8.2:
|
||||||
version "2.8.1"
|
version "2.8.2"
|
||||||
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5"
|
||||||
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
|
integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==
|
||||||
|
|
||||||
yargs-parser@^21.1.1:
|
yargs-parser@^21.1.1:
|
||||||
version "21.1.1"
|
version "21.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user