mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248e4d2562 | ||
|
|
7b8e961b49 | ||
|
|
f66ceccbb4 | ||
|
|
a3db725af6 | ||
|
|
0663bd945f | ||
|
|
bc355fb5fe | ||
|
|
797421f0d5 | ||
|
|
0b2b42fc75 |
94
CHANGELOG.md
94
CHANGELOG.md
@@ -1,94 +0,0 @@
|
||||
Newer release changelog see https://github.com/orangecoding/fredy/releases
|
||||
|
||||
---
|
||||
|
||||
###### [V5.5.0]
|
||||
|
||||
- Upgrading dependencies
|
||||
- fixing provider
|
||||
- allow multiple instances of 1 provider
|
||||
- **BREAKING**: Minimum node version is now 16
|
||||
|
||||
###### [V5.4.6]
|
||||
|
||||
- Adding Instana node.js monitoring
|
||||
-
|
||||
|
||||
###### [V5.4.5]
|
||||
|
||||
- Adding Instana node.js monitoring
|
||||
|
||||
###### [V5.4.4]
|
||||
|
||||
- Add support for Immo Südwest Presse (immo.swp.de)
|
||||
- Telegram: Use job name instead of ID and link in title
|
||||
- Fix race condition if user ID is in session but not in user store
|
||||
- Allow visiting the original provider URL
|
||||
|
||||
###### [V5.4.3]
|
||||
|
||||
- re-writing readme
|
||||
- improving docker build
|
||||
- using github's actions to build docker and test automatically
|
||||
|
||||
###### [V5.4.2]
|
||||
|
||||
- Fixing prod build
|
||||
|
||||
###### [V5.4.1]
|
||||
|
||||
- Upgrading dependencies
|
||||
- Provider urls are now automagically been changed to include the correct sort order for search results
|
||||
|
||||
```
|
||||
Note: It has been an point of confusion since the very beginning of Fredy, that people simply copied the url, but
|
||||
did not take care of sorting the search results by date. If this is not done, Fredy will most likely not see the latest
|
||||
results, thus cannot report them. This release fixes it by adding the necessary params (or replaces them).
|
||||
```
|
||||
|
||||
###### [V5.3.0]
|
||||
|
||||
- Upgrading dependencies
|
||||
- It's now possible to send mails to multiple receiver using comma separation for MailJet & Sendgrid
|
||||
- Fixing Immowelt scraping
|
||||
|
||||
###### [V5.2.0]
|
||||
|
||||
- Upgrading dependencies
|
||||
- Adding new similarity check layer (Duplicates are being removed now)
|
||||
- Adding paging for search results
|
||||
|
||||
###### [V5.1.0]
|
||||
|
||||
- Upgrading dependencies
|
||||
- NodeJS 12.13 is now the minimum supported version
|
||||
- Adding general settings as new configuration page to ui
|
||||
- Adding new feature working hours
|
||||
|
||||
###### [V5.0.0]
|
||||
|
||||
- Upgrading dependencies
|
||||
- NodeJS 12 is now the minimum supported version
|
||||
|
||||
###### [V4.0.0]
|
||||
|
||||
Bringing back Immoscout :tada:
|
||||
|
||||
###### [V3.0.0]
|
||||
|
||||
This is basically a re-write, your old config file will not be compatible anymore. Please re-created your search jobs
|
||||
on the new ui and use the values from your previous config file if needed.
|
||||
|
||||
```
|
||||
- We're getting rid of manual config changes, Fredy, now ships with a UI so that it's easy for you to create and edit jobs
|
||||
```
|
||||
|
||||
###### [V2.0.0]
|
||||
|
||||
```
|
||||
- Fredy can now run multiple search job on one instance
|
||||
- Changed lot's of the structure of Fredy to make this happen
|
||||
[BREAKING CHANGES]
|
||||
- The config has been changed, the config of V1.x will not work any longer
|
||||
- Sources have been renamed to provider
|
||||
```
|
||||
2
index.js
2
index.js
@@ -8,7 +8,6 @@ import { checkIfConfigIsAccessible, getProviders, refreshConfig } from './lib/ut
|
||||
import * as similarityCache from './lib/services/similarity-check/similarityCache.js';
|
||||
import { runMigrations } from './lib/services/storage/migrations/migrate.js';
|
||||
import { ensureDemoUserExists, ensureAdminUserExists } from './lib/services/storage/userStorage.js';
|
||||
import { cleanupDemoAtMidnight } from './lib/services/crons/demoCleanup-cron.js';
|
||||
import { initTrackerCron } from './lib/services/crons/tracker-cron.js';
|
||||
import logger from './lib/services/logger.js';
|
||||
import { initActiveCheckerCron } from './lib/services/crons/listing-alive-cron.js';
|
||||
@@ -54,7 +53,6 @@ await import('./lib/api/api.js');
|
||||
|
||||
if (settings.demoMode) {
|
||||
logger.info('Running in demo mode');
|
||||
cleanupDemoAtMidnight();
|
||||
}
|
||||
|
||||
ensureAdminUserExists();
|
||||
|
||||
@@ -11,10 +11,13 @@ import logger from '../../services/logger.js';
|
||||
import { bus } from '../../services/events/event-bus.js';
|
||||
import { isRunning as isJobRunning } from '../../services/jobs/run-state.js';
|
||||
import { addClient as addSseClient, removeClient } from '../../services/sse/sse-broker.js';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
|
||||
const service = restana();
|
||||
const jobRouter = service.newRouter();
|
||||
|
||||
const DEMO_JOB_NAME = 'Demo-Job';
|
||||
|
||||
function doesJobBelongsToUser(job, req) {
|
||||
const userId = req.session.currentUser;
|
||||
if (userId == null) {
|
||||
@@ -161,6 +164,7 @@ jobRouter.post('/:jobId/run', async (req, res) => {
|
||||
|
||||
jobRouter.post('/', async (req, res) => {
|
||||
const { provider, notificationAdapter, name, blacklist = [], jobId, enabled, shareWithUsers = [] } = req.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
let jobFromDb = jobStorage.getJob(jobId);
|
||||
|
||||
@@ -169,6 +173,11 @@ jobRouter.post('/', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.demoMode && jobFromDb && jobFromDb.name === DEMO_JOB_NAME) {
|
||||
res.send(new Error('Sorry, but you cannot change the Status of our Demo Job ;)'));
|
||||
return;
|
||||
}
|
||||
|
||||
jobStorage.upsertJob({
|
||||
userId: req.session.currentUser,
|
||||
jobId,
|
||||
@@ -188,8 +197,14 @@ jobRouter.post('/', async (req, res) => {
|
||||
|
||||
jobRouter.delete('', async (req, res) => {
|
||||
const { jobId } = req.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
if (settings.demoMode && job.name === DEMO_JOB_NAME) {
|
||||
res.send(new Error('Sorry, but you cannot remove the Demo Job ;)'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doesJobBelongsToUser(job, req)) {
|
||||
res.send(new Error('You are trying to remove a job that is not associated to your user'));
|
||||
} else {
|
||||
@@ -204,8 +219,15 @@ jobRouter.delete('', async (req, res) => {
|
||||
jobRouter.put('/:jobId/status', async (req, res) => {
|
||||
const { status } = req.body;
|
||||
const { jobId } = req.params;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
|
||||
if (settings.demoMode && job.name === DEMO_JOB_NAME) {
|
||||
res.send(new Error('Sorry, but you cannot change the Status of our Demo Job ;)'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doesJobBelongsToUser(job, req)) {
|
||||
res.send(new Error('You are trying change a job that is not associated to your user'));
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isAdmin as isAdminFn } from '../security.js';
|
||||
import logger from '../../services/logger.js';
|
||||
import { nullOrEmpty } from '../../utils.js';
|
||||
import { getJobs } from '../../services/storage/jobStorage.js';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
|
||||
const service = restana();
|
||||
|
||||
@@ -106,9 +107,15 @@ listingsRouter.post('/watch', async (req, res) => {
|
||||
});
|
||||
|
||||
listingsRouter.delete('/job', async (req, res) => {
|
||||
const { jobId } = req.body;
|
||||
const { jobId, hardDelete = false } = req.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
listingStorage.deleteListingsByJobId(jobId);
|
||||
if (settings.demoMode) {
|
||||
res.send(new Error('Sorry, but you cannot remove listings in demo mode ;)'));
|
||||
return;
|
||||
}
|
||||
|
||||
listingStorage.deleteListingsByJobId(jobId, hardDelete);
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
logger.error(error);
|
||||
@@ -117,10 +124,10 @@ listingsRouter.delete('/job', async (req, res) => {
|
||||
});
|
||||
|
||||
listingsRouter.delete('/', async (req, res) => {
|
||||
const { ids } = req.body;
|
||||
const { ids, hardDelete = false } = req.body;
|
||||
try {
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
listingStorage.deleteListingsById(ids);
|
||||
listingStorage.deleteListingsById(ids, hardDelete);
|
||||
}
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
import restana from 'restana';
|
||||
import SqliteConnection from '../../services/storage/SqliteConnection.js';
|
||||
import { upsertSettings } from '../../services/storage/settingsStorage.js';
|
||||
import { getSettings, upsertSettings } from '../../services/storage/settingsStorage.js';
|
||||
import { resetGeocoordinatesAndDistanceForUser } from '../../services/storage/listingsStorage.js';
|
||||
import { geocodeAddress } from '../../services/geocoding/geoCodingService.js';
|
||||
import { autocompleteAddress } from '../../services/geocoding/autocompleteService.js';
|
||||
import { calculateDistanceForUser } from '../../services/geocoding/distanceService.js';
|
||||
import { fromJson } from '../../utils.js';
|
||||
import { trackFeature } from '../../services/tracking/Tracker.js';
|
||||
import { FEATURES } from '../../features.js';
|
||||
import logger from '../../services/logger.js';
|
||||
import { runGeoCordTask } from '../../services/crons/geocoding-cron.js';
|
||||
|
||||
const service = restana();
|
||||
const userSettingsRouter = service.newRouter();
|
||||
@@ -43,6 +44,12 @@ userSettingsRouter.get('/autocomplete', async (req, res) => {
|
||||
userSettingsRouter.post('/home-address', async (req, res) => {
|
||||
const userId = req.session.currentUser;
|
||||
const { home_address } = req.body;
|
||||
const settings = await getSettings();
|
||||
|
||||
if (settings.demoMode) {
|
||||
res.send(new Error('In demo mode, it is not allowed to change the home address.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (home_address) {
|
||||
@@ -50,7 +57,9 @@ userSettingsRouter.post('/home-address', async (req, res) => {
|
||||
const coords = await geocodeAddress(home_address);
|
||||
if (coords && coords.lat !== -1) {
|
||||
upsertSettings({ home_address: { address: home_address, coords } }, userId);
|
||||
calculateDistanceForUser(userId);
|
||||
resetGeocoordinatesAndDistanceForUser(userId);
|
||||
//we do NOT wait for this to finish, as we don't want to block the response
|
||||
runGeoCordTask();
|
||||
res.send({ success: true, coords });
|
||||
} else {
|
||||
res.statusCode = 400;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 by Christian Kellner.
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import { removeJobsByUserId } from '../storage/jobStorage.js';
|
||||
import { getUsers } from '../storage/userStorage.js';
|
||||
import logger from '../logger.js';
|
||||
import cron from 'node-cron';
|
||||
import { getSettings } from '../storage/settingsStorage.js';
|
||||
|
||||
/**
|
||||
* if we are running in demo environment, we have to cleanup the db files (specifically the jobs table)
|
||||
*/
|
||||
export function cleanupDemoAtMidnight() {
|
||||
cron.schedule('0 0 * * *', cleanup);
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode) {
|
||||
const demoUser = getUsers(false).find((user) => user.username === 'demo');
|
||||
if (demoUser == null) {
|
||||
logger.error('Demo user not found, cannot remove Jobs');
|
||||
return Promise.resolve();
|
||||
}
|
||||
removeJobsByUserId(demoUser.id);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ import { getListingsToGeocode, updateListingGeocoordinates } from '../storage/li
|
||||
import { geocodeAddress, isGeocodingPaused } from '../geocoding/geoCodingService.js';
|
||||
import { getJobs } from '../storage/jobStorage.js';
|
||||
import { calculateDistanceForJob } from '../geocoding/distanceService.js';
|
||||
import { getSettings } from '../storage/settingsStorage.js';
|
||||
import logger from '../logger.js';
|
||||
|
||||
async function runTask() {
|
||||
export async function runGeoCordTask() {
|
||||
const listings = getListingsToGeocode();
|
||||
if (listings.length > 0) {
|
||||
for (const listing of listings) {
|
||||
@@ -32,8 +34,13 @@ async function runTask() {
|
||||
}
|
||||
|
||||
export async function initGeocodingCron() {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode) {
|
||||
logger.info('Do not start geo service as we are in demo mode');
|
||||
return;
|
||||
}
|
||||
// run directly on start
|
||||
await runTask();
|
||||
await runGeoCordTask();
|
||||
// then every 6 hours
|
||||
cron.schedule('0 */6 * * *', runTask);
|
||||
cron.schedule('0 */6 * * *', runGeoCordTask);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,19 @@
|
||||
|
||||
import cron from 'node-cron';
|
||||
import runActiveChecker from '../listings/listingActiveService.js';
|
||||
import logger from '../logger.js';
|
||||
import { getSettings } from '../storage/settingsStorage.js';
|
||||
|
||||
async function runTask() {
|
||||
await runActiveChecker();
|
||||
}
|
||||
|
||||
export async function initActiveCheckerCron() {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode) {
|
||||
logger.info('Do not start listing active checker as we are in demo mode');
|
||||
return;
|
||||
}
|
||||
//run directly on start
|
||||
await runTask();
|
||||
// then every day at 1 am
|
||||
|
||||
@@ -37,12 +37,12 @@ export const upsertJob = ({
|
||||
if (existing) {
|
||||
SqliteConnection.execute(
|
||||
`UPDATE jobs
|
||||
SET enabled = @enabled,
|
||||
name = @name,
|
||||
blacklist = @blacklist,
|
||||
provider = @provider,
|
||||
notification_adapter = @notification_adapter,
|
||||
shared_with_user = @shareWithUsers
|
||||
SET enabled = @enabled,
|
||||
name = @name,
|
||||
blacklist = @blacklist,
|
||||
provider = @provider,
|
||||
notification_adapter = @notification_adapter,
|
||||
shared_with_user = @shareWithUsers
|
||||
WHERE id = @id`,
|
||||
{
|
||||
id,
|
||||
@@ -87,10 +87,10 @@ export const getJob = (jobId) => {
|
||||
j.provider,
|
||||
j.shared_with_user,
|
||||
j.notification_adapter AS notificationAdapter,
|
||||
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id) AS numberOfFoundListings
|
||||
FROM jobs j
|
||||
WHERE j.id = @id
|
||||
LIMIT 1`,
|
||||
(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
|
||||
WHERE j.id = @id
|
||||
LIMIT 1`,
|
||||
{ id: jobId },
|
||||
)[0];
|
||||
if (!row) return null;
|
||||
@@ -150,9 +150,10 @@ export const getJobs = () => {
|
||||
j.provider,
|
||||
j.shared_with_user,
|
||||
j.notification_adapter AS notificationAdapter,
|
||||
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id) AS numberOfFoundListings
|
||||
FROM jobs j
|
||||
ORDER BY j.name IS NULL, j.name`,
|
||||
(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
|
||||
WHERE j.enabled = 1
|
||||
ORDER BY j.name IS NULL, j.name`,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
@@ -250,11 +251,11 @@ export const queryJobs = ({
|
||||
j.provider,
|
||||
j.shared_with_user,
|
||||
j.notification_adapter AS notificationAdapter,
|
||||
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id) 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
|
||||
${whereSql}
|
||||
${orderSql}
|
||||
LIMIT @limit OFFSET @offset`,
|
||||
${orderSql}
|
||||
LIMIT @limit OFFSET @offset`,
|
||||
params,
|
||||
);
|
||||
|
||||
|
||||
@@ -370,10 +370,18 @@ export const queryListings = ({
|
||||
* Delete all listings for a given job id.
|
||||
*
|
||||
* @param {string} jobId - The job identifier whose listings should be removed.
|
||||
* @returns {any} The result from SqliteConnection.execute (may contain changes count).
|
||||
* @param {boolean} [hardDelete=false] - Whether to hard delete from DB or just mark as deleted.
|
||||
* @returns {any} The result from SqliteConnection.execute.
|
||||
*/
|
||||
export const deleteListingsByJobId = (jobId) => {
|
||||
export const deleteListingsByJobId = (jobId, hardDelete = false) => {
|
||||
if (!jobId) return;
|
||||
if (hardDelete) {
|
||||
return SqliteConnection.execute(
|
||||
`DELETE FROM listings
|
||||
WHERE job_id = @jobId`,
|
||||
{ jobId },
|
||||
);
|
||||
}
|
||||
return SqliteConnection.execute(
|
||||
`UPDATE listings
|
||||
SET manually_deleted = 1
|
||||
@@ -386,11 +394,19 @@ export const deleteListingsByJobId = (jobId) => {
|
||||
* Delete listings by a list of listing IDs.
|
||||
*
|
||||
* @param {string[]} ids - Array of listing IDs to delete.
|
||||
* @param {boolean} [hardDelete=false] - Whether to hard delete from DB or just mark as deleted.
|
||||
* @returns {any} The result from SqliteConnection.execute.
|
||||
*/
|
||||
export const deleteListingsById = (ids) => {
|
||||
export const deleteListingsById = (ids, hardDelete = false) => {
|
||||
if (!Array.isArray(ids) || ids.length === 0) return;
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
if (hardDelete) {
|
||||
return SqliteConnection.execute(
|
||||
`DELETE FROM listings
|
||||
WHERE id IN (${placeholders})`,
|
||||
ids,
|
||||
);
|
||||
}
|
||||
return SqliteConnection.execute(
|
||||
`UPDATE listings
|
||||
SET manually_deleted = 1
|
||||
@@ -592,3 +608,23 @@ export const getListingById = (id, userId = null, isAdmin = false) => {
|
||||
)[0] || null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets geocoordinates and distance for all listings related to a user.
|
||||
*
|
||||
* @param {string} userId
|
||||
* @returns {void}
|
||||
*/
|
||||
export const resetGeocoordinatesAndDistanceForUser = (userId) => {
|
||||
SqliteConnection.execute(
|
||||
`UPDATE listings
|
||||
SET latitude = NULL,
|
||||
longitude = NULL,
|
||||
distance_to_destination = NULL
|
||||
WHERE job_id IN (
|
||||
SELECT id FROM jobs j
|
||||
WHERE j.user_id = @userId
|
||||
)`,
|
||||
{ userId },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as hasher from '../security/hash.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import SqliteConnection from './SqliteConnection.js';
|
||||
import { getSettings } from './settingsStorage.js';
|
||||
import { inDevMode } from '../../utils.js';
|
||||
|
||||
/**
|
||||
* Get all users.
|
||||
@@ -137,8 +138,12 @@ export const removeUser = (userId) => {
|
||||
export const ensureDemoUserExists = async () => {
|
||||
const settings = await getSettings();
|
||||
if (!settings.demoMode) {
|
||||
// Remove demo user (and cascade delete their jobs/listings)
|
||||
SqliteConnection.execute(`DELETE FROM users WHERE username = 'demo'`);
|
||||
if (!inDevMode()) {
|
||||
// Remove demo user (and cascade delete their jobs/listings)
|
||||
SqliteConnection.execute(`DELETE
|
||||
FROM users
|
||||
WHERE username = 'demo'`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Ensure demo user exists when demo mode is on
|
||||
|
||||
@@ -15,55 +15,67 @@ const deviceId = getUniqueId() || 'N/A';
|
||||
const version = await getPackageVersion();
|
||||
const FREDY_TRACKING_URL = 'https://fredy.orange-coding.net/tracking';
|
||||
|
||||
export const trackMainEvent = async () => {
|
||||
const staticTrackingData = {
|
||||
operatingSystem: os.platform(),
|
||||
osVersion: os.release(),
|
||||
arch: process.arch,
|
||||
language: process.env.LANG || 'en',
|
||||
nodeVersion: process.version || 'N/A',
|
||||
deviceId,
|
||||
version,
|
||||
};
|
||||
|
||||
const shouldTrack = async () => {
|
||||
const settings = await getSettings();
|
||||
return settings.analyticsEnabled && !inDevMode();
|
||||
};
|
||||
|
||||
const sendTrackingData = async (endpoint, payload) => {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
if (settings.analyticsEnabled && !inDevMode()) {
|
||||
const activeProvider = new Set();
|
||||
const activeAdapter = new Set();
|
||||
|
||||
const jobs = getJobs();
|
||||
|
||||
if (jobs != null && jobs.length > 0) {
|
||||
jobs.forEach((job) => {
|
||||
job.provider.forEach((provider) => activeProvider.add(provider.id));
|
||||
job.notificationAdapter.forEach((adapter) => activeAdapter.add(adapter.id));
|
||||
});
|
||||
|
||||
const trackingObj = enrichTrackingObject({
|
||||
adapter: Array.from(activeAdapter),
|
||||
provider: Array.from(activeProvider),
|
||||
});
|
||||
|
||||
await fetch(`${FREDY_TRACKING_URL}/main`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(trackingObj),
|
||||
});
|
||||
}
|
||||
const response = await fetch(`${FREDY_TRACKING_URL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: payload ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.warn(`Error sending tracking data to ${endpoint}. Status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error sending tracking data', error);
|
||||
logger.warn(`Error sending tracking data to ${endpoint}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
export const trackMainEvent = async () => {
|
||||
if (!(await shouldTrack())) return;
|
||||
|
||||
const activeProvider = new Set();
|
||||
const activeAdapter = new Set();
|
||||
|
||||
const jobs = getJobs();
|
||||
|
||||
if (jobs != null && jobs.length > 0) {
|
||||
jobs.forEach((job) => {
|
||||
job.provider.forEach((provider) => activeProvider.add(provider.id));
|
||||
job.notificationAdapter.forEach((adapter) => activeAdapter.add(adapter.id));
|
||||
});
|
||||
|
||||
const trackingObj = await enrichTrackingObject({
|
||||
adapter: Array.from(activeAdapter),
|
||||
provider: Array.from(activeProvider),
|
||||
});
|
||||
|
||||
await sendTrackingData('/main', trackingObj);
|
||||
}
|
||||
};
|
||||
|
||||
export const trackFeature = async (feature) => {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
if (settings.analyticsEnabled && !inDevMode()) {
|
||||
const trackingObj = await enrichTrackingObject({
|
||||
feature,
|
||||
});
|
||||
if (!(await shouldTrack())) return;
|
||||
|
||||
await fetch(`${FREDY_TRACKING_URL}/feature`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(trackingObj),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error tracking feature', error);
|
||||
}
|
||||
const trackingObj = await enrichTrackingObject({
|
||||
feature,
|
||||
});
|
||||
|
||||
await sendTrackingData('/feature', trackingObj);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -72,34 +84,17 @@ export const trackFeature = async (feature) => {
|
||||
export async function trackDemoAccessed() {
|
||||
const settings = await getSettings();
|
||||
if (settings.analyticsEnabled && !inDevMode() && settings.demoMode) {
|
||||
try {
|
||||
await fetch(`${FREDY_TRACKING_URL}/demo/accessed`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('Error sending tracking data', error);
|
||||
}
|
||||
const trackingObj = await enrichTrackingObject({});
|
||||
await sendTrackingData('/demo/accessed', trackingObj);
|
||||
}
|
||||
}
|
||||
|
||||
async function enrichTrackingObject(trackingObject) {
|
||||
const settings = await getSettings();
|
||||
const operatingSystem = os.platform();
|
||||
const osVersion = os.release();
|
||||
const arch = process.arch;
|
||||
const language = process.env.LANG || 'en';
|
||||
const nodeVersion = process.version || 'N/A';
|
||||
|
||||
return {
|
||||
...trackingObject,
|
||||
...staticTrackingData,
|
||||
isDemo: settings.demoMode,
|
||||
operatingSystem,
|
||||
osVersion,
|
||||
arch,
|
||||
nodeVersion,
|
||||
language,
|
||||
deviceId,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fredy",
|
||||
"version": "19.3.0",
|
||||
"version": "19.3.6",
|
||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@douyinfe/semi-ui": "2.90.13",
|
||||
"@douyinfe/semi-ui-19": "^2.90.13",
|
||||
"@sendgrid/mail": "8.1.6",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"@vitejs/plugin-react": "5.1.3",
|
||||
"adm-zip": "^0.5.16",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"body-parser": "2.2.2",
|
||||
@@ -95,12 +95,12 @@
|
||||
"slack": "11.0.2",
|
||||
"vite": "7.3.1",
|
||||
"x-var": "^3.0.1",
|
||||
"zustand": "^5.0.10"
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/eslint-parser": "7.28.6",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@babel/preset-react": "7.28.5",
|
||||
"chai": "6.2.2",
|
||||
"eslint": "9.39.2",
|
||||
|
||||
@@ -124,14 +124,7 @@ export default function FredyApp() {
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/userSettings"
|
||||
element={
|
||||
<PermissionAwareRoute currentUser={currentUser} adminOnly={false}>
|
||||
<UserSettings />
|
||||
</PermissionAwareRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/userSettings" element={<UserSettings />} />
|
||||
<Route
|
||||
path="/generalSettings"
|
||||
element={
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
padding: 24px;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px;
|
||||
|
||||
70
ui/src/components/ListingDeletionModal.jsx
Normal file
70
ui/src/components/ListingDeletionModal.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 React, { useState } from 'react';
|
||||
import { Modal, Radio, RadioGroup, Typography } from '@douyinfe/semi-ui-19';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ListingDeletionModal = ({
|
||||
visible,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
title = 'Delete Listings',
|
||||
showOptions = true,
|
||||
message = 'How would you like to delete the selected listing(s)?',
|
||||
}) => {
|
||||
const [deleteType, setDeleteType] = useState('soft');
|
||||
|
||||
const handleOk = () => {
|
||||
onConfirm(!showOptions || deleteType === 'hard');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={onCancel}
|
||||
okText="Confirm"
|
||||
cancelText="Cancel"
|
||||
style={{ maxWidth: '500px' }}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text>{message}</Text>
|
||||
</div>
|
||||
{showOptions && (
|
||||
<RadioGroup value={deleteType} onChange={(e) => setDeleteType(e.target.value)} style={{ width: '100%' }}>
|
||||
<Radio value="soft" style={{ alignItems: 'flex-start', width: '100%' }}>
|
||||
<div style={{ marginLeft: 8 }}>
|
||||
<Text strong>Mark as deleted (Soft Delete)</Text>
|
||||
<br />
|
||||
<Text type="secondary">
|
||||
Listings are kept in the database but marked as hidden. They will <b>not</b> re-appear during the next
|
||||
scraping session.
|
||||
</Text>
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="hard" style={{ marginTop: 16, alignItems: 'flex-start', width: '100%' }}>
|
||||
<div style={{ marginLeft: 8 }}>
|
||||
<Text strong>Remove from database (Hard Delete)</Text>
|
||||
<br />
|
||||
<Text type="secondary">
|
||||
Listings are completely removed from the database.
|
||||
<br />
|
||||
<Text type="warning">
|
||||
Consequence: They might re-appear when scraping the next time because Fredy won't know they were
|
||||
previously found.
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListingDeletionModal;
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
IconPlusCircle,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
||||
import { useActions, useSelector } from '../../../services/state/store.js';
|
||||
import { xhrDelete, xhrPut, xhrPost } from '../../../services/xhr.js';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -60,6 +61,9 @@ const JobGrid = () => {
|
||||
const [activityFilter, setActivityFilter] = useState(null);
|
||||
const [showFilterBar, setShowFilterBar] = useState(false);
|
||||
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [pendingDeletion, setPendingDeletion] = useState(null); // { type: 'job'|'listings', jobId }
|
||||
|
||||
const pendingJobIdRef = useRef(null);
|
||||
const evtSourceRef = useRef(null);
|
||||
|
||||
@@ -125,24 +129,35 @@ const JobGrid = () => {
|
||||
};
|
||||
}, [handleFilterChange]);
|
||||
|
||||
const onJobRemoval = async (jobId) => {
|
||||
try {
|
||||
await xhrDelete('/api/jobs', { jobId });
|
||||
Toast.success('Job successfully removed');
|
||||
loadData();
|
||||
actions.jobsData.getJobs(); // refresh select list too
|
||||
} catch (error) {
|
||||
Toast.error(error);
|
||||
}
|
||||
const onJobRemoval = (jobId) => {
|
||||
setPendingDeletion({ type: 'job', jobId });
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
const onListingRemoval = async (jobId) => {
|
||||
const onListingRemoval = (jobId) => {
|
||||
setPendingDeletion({ type: 'listings', jobId });
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
const confirmDeletion = async (hardDelete) => {
|
||||
const { type, jobId } = pendingDeletion;
|
||||
try {
|
||||
await xhrDelete('/api/listings/job', { jobId });
|
||||
Toast.success('Listings successfully removed');
|
||||
if (type === 'job') {
|
||||
await xhrDelete('/api/jobs', { jobId });
|
||||
Toast.success('Job and listings successfully removed');
|
||||
} else if (type === 'listings') {
|
||||
await xhrDelete('/api/listings/job', { jobId, hardDelete });
|
||||
Toast.success('Listings successfully removed');
|
||||
}
|
||||
loadData();
|
||||
if (type === 'job') {
|
||||
actions.jobsData.getJobs(); // refresh select list too
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.error(error);
|
||||
Toast.error(error.message || 'Error performing deletion');
|
||||
} finally {
|
||||
setDeleteModalVisible(false);
|
||||
setPendingDeletion(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -410,6 +425,21 @@ const JobGrid = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ListingDeletionModal
|
||||
visible={deleteModalVisible}
|
||||
title={pendingDeletion?.type === 'job' ? 'Delete Job' : 'Delete Listings'}
|
||||
showOptions={pendingDeletion?.type !== 'job'}
|
||||
message={
|
||||
pendingDeletion?.type === 'job'
|
||||
? 'Are you sure you want to delete this job? All associated listings will be removed from the database.'
|
||||
: 'How would you like to delete the selected listing(s)?'
|
||||
}
|
||||
onConfirm={confirmDeletion}
|
||||
onCancel={() => {
|
||||
setDeleteModalVisible(false);
|
||||
setPendingDeletion(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
IconEyeOpened,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
||||
import no_image from '../../../assets/no_image.jpg';
|
||||
import * as timeService from '../../../services/time/timeService.js';
|
||||
import { xhrDelete, xhrPost } from '../../../services/xhr.js';
|
||||
@@ -65,6 +66,9 @@ const ListingsGrid = () => {
|
||||
const [providerFilter, setProviderFilter] = useState(null);
|
||||
const [showFilterBar, setShowFilterBar] = useState(false);
|
||||
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [listingToDelete, setListingToDelete] = useState(null);
|
||||
|
||||
const loadData = () => {
|
||||
actions.listingsData.getListingsData({
|
||||
page,
|
||||
@@ -106,6 +110,19 @@ const ListingsGrid = () => {
|
||||
setPage(_page);
|
||||
};
|
||||
|
||||
const confirmDeletion = async (hardDelete) => {
|
||||
try {
|
||||
await xhrDelete('/api/listings/', { ids: [listingToDelete], hardDelete });
|
||||
Toast.success('Listing successfully removed');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
Toast.error(error.message || 'Error deleting listing');
|
||||
} finally {
|
||||
setDeleteModalVisible(false);
|
||||
setListingToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const cap = (val) => {
|
||||
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
|
||||
};
|
||||
@@ -312,15 +329,10 @@ const ListingsGrid = () => {
|
||||
title="Remove"
|
||||
type="danger"
|
||||
size="small"
|
||||
onClick={async (e) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await xhrDelete('/api/listings/', { ids: [item.id] });
|
||||
Toast.success('Listing(s) successfully removed');
|
||||
loadData();
|
||||
} catch (error) {
|
||||
Toast.error(error);
|
||||
}
|
||||
setListingToDelete(item.id);
|
||||
setDeleteModalVisible(true);
|
||||
}}
|
||||
icon={<IconDelete />}
|
||||
/>
|
||||
@@ -341,6 +353,14 @@ const ListingsGrid = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ListingDeletionModal
|
||||
visible={deleteModalVisible}
|
||||
onConfirm={confirmDeletion}
|
||||
onCancel={() => {
|
||||
setDeleteModalVisible(false);
|
||||
setListingToDelete(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,12 +8,12 @@ import { Card } from '@douyinfe/semi-ui-19';
|
||||
|
||||
import './SegmentParts.less';
|
||||
|
||||
export const SegmentPart = ({ name, Icon = null, children, helpText = null }) => {
|
||||
export const SegmentPart = ({ name, Icon = null, children, helpText = null, className = '' }) => {
|
||||
const { Meta } = Card;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="segmentParts"
|
||||
className={`segmentParts ${className}`}
|
||||
title={
|
||||
(helpText || name) && (
|
||||
<Meta title={name} description={helpText} avatar={Icon == null ? null : <Icon size="extra-extra-small" />} />
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { useSelector, useActions } from '../../services/state/store';
|
||||
import KpiCard from '../../components/cards/KpiCard.jsx';
|
||||
import PieChartCard from '../../components/cards/PieChartCard.jsx';
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
|
||||
import './Dashboard.less';
|
||||
import { SegmentPart } from '../../components/segment/SegmentPart.jsx';
|
||||
@@ -39,8 +38,6 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<Headline text="Dashboard" size={3} />
|
||||
|
||||
<Row gutter={[16, 16]} className="dashboard__row">
|
||||
<Col span={12} xs={24} sm={24} md={24} lg={24} xl={12}>
|
||||
<SegmentPart name="General" Icon={IconTerminal}>
|
||||
@@ -136,7 +133,14 @@ export default function Dashboard() {
|
||||
<KpiCard
|
||||
title="Avg. Price"
|
||||
color="purple"
|
||||
value={`${!kpis.avgPriceOfListings ? '---' : kpis.avgPriceOfListings} €`}
|
||||
value={`${
|
||||
!kpis.avgPriceOfListings
|
||||
? '---'
|
||||
: new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(kpis.avgPriceOfListings)
|
||||
}`}
|
||||
icon={<IconNoteMoney />}
|
||||
description="Avg. Price of listings"
|
||||
/>
|
||||
@@ -146,7 +150,12 @@ export default function Dashboard() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<SegmentPart name="Provider Insights" Icon={IconStar} helpText="Percentage of found listings over all providers">
|
||||
<SegmentPart
|
||||
name="Provider Insights"
|
||||
Icon={IconStar}
|
||||
helpText="Percentage of found listings over all providers"
|
||||
className="dashboard__provider-insights"
|
||||
>
|
||||
<PieChartCard data={pieData} />
|
||||
</SegmentPart>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
&__row {
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
@@ -7,4 +11,23 @@
|
||||
margin-bottom: 0; // Handled by Row gutter
|
||||
}
|
||||
}
|
||||
|
||||
&__provider-insights {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 !important;
|
||||
|
||||
.semi-card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-height: 300px;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ export default function ListingDetail() {
|
||||
>
|
||||
{listing.isWatched === 1 ? 'Watched' : 'Watch'}
|
||||
</Button>
|
||||
<Text link={{ href: listing.link }} icon={<IconLink />} underline>
|
||||
<Text link={{ href: listing.link, target: '_blank' }} icon={<IconLink />} underline>
|
||||
Open listing
|
||||
</Text>
|
||||
</Space>
|
||||
@@ -324,7 +324,12 @@ export default function ListingDetail() {
|
||||
<Row>
|
||||
<Col span={24} lg={12}>
|
||||
<div className="listing-detail__image-container">
|
||||
<Image src={listing.image_url || no_image} fallback={no_image} preview={true} />
|
||||
<Image
|
||||
src={listing.image_url}
|
||||
fallback={no_image}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
preview={true}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24} lg={12}>
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'react-range-slider-input/dist/style.css';
|
||||
import './Map.less';
|
||||
import { xhrDelete } from '../../services/xhr.js';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -85,6 +86,22 @@ export default function MapView() {
|
||||
const [showFilterBar, setShowFilterBar] = useState(false);
|
||||
const [distanceFilter, setDistanceFilter] = useState(0);
|
||||
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [listingToDelete, setListingToDelete] = useState(null);
|
||||
|
||||
const confirmListingDeletion = async (hardDelete) => {
|
||||
try {
|
||||
await xhrDelete('/api/listings/', { ids: [listingToDelete], hardDelete });
|
||||
Toast.success('Listing successfully removed');
|
||||
fetchListings();
|
||||
} catch (error) {
|
||||
Toast.error(error.message || 'Error deleting listing');
|
||||
} finally {
|
||||
setDeleteModalVisible(false);
|
||||
setListingToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPriceRange([0, getMaxPrice()]);
|
||||
}, [listings]);
|
||||
@@ -104,14 +121,9 @@ export default function MapView() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.deleteListing = async (id) => {
|
||||
try {
|
||||
await xhrDelete('/api/listings/', { ids: [id] });
|
||||
Toast.success('Listing successfully removed');
|
||||
fetchListings();
|
||||
} catch (error) {
|
||||
Toast.error(error.message || 'Error deleting listing');
|
||||
}
|
||||
window.deleteListing = (id) => {
|
||||
setListingToDelete(id);
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
window.viewDetails = (id) => {
|
||||
@@ -378,7 +390,10 @@ export default function MapView() {
|
||||
|
||||
const popupContent = `
|
||||
<div class="map-popup-content">
|
||||
<img src="${listing.image_url || no_image}" alt="${listing.title}" />
|
||||
<img
|
||||
src="${listing.image_url}"
|
||||
onerror="this.onerror=null;this.src='${no_image}'"
|
||||
/>
|
||||
<h4>${listing.title}</h4>
|
||||
<div class="info">
|
||||
<span><strong>Price:</strong> ${listing.price ? listing.price + ' €' : 'N/A'}</span>
|
||||
@@ -559,6 +574,14 @@ export default function MapView() {
|
||||
/>
|
||||
|
||||
<div ref={mapContainer} className="map-container" />
|
||||
<ListingDeletionModal
|
||||
visible={deleteModalVisible}
|
||||
onConfirm={confirmListingDeletion}
|
||||
onCancel={() => {
|
||||
setDeleteModalVisible(false);
|
||||
setListingToDelete(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,18 @@ export default function Login() {
|
||||
<div className="login__logoWrapper">
|
||||
<Logo width={250} white />
|
||||
</div>
|
||||
|
||||
{demoMode && (
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="This is the demo version of Fredy. Use 'demo' as both the username and password to log in."
|
||||
style={{ marginBottom: '1.5rem' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
{error && <Banner type="danger" closeIcon={null} description={error} style={{ marginBottom: '1rem' }} />}
|
||||
<div className="login__inputGroup">
|
||||
@@ -100,17 +112,6 @@ export default function Login() {
|
||||
<Button block type="primary" onClick={tryLogin} theme="solid" style={{ marginTop: '1rem' }}>
|
||||
Login
|
||||
</Button>
|
||||
|
||||
{demoMode && (
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
description="This is the demo version of Fredy. Use 'demo' as both the username and password to log in."
|
||||
style={{ marginTop: '1.5rem' }}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Divider, Button, AutoComplete, Toast, Typography, Banner } from '@douyinfe/semi-ui-19';
|
||||
import { Divider, Button, AutoComplete, Toast, Banner } from '@douyinfe/semi-ui-19';
|
||||
import { IconSave, IconHome } from '@douyinfe/semi-icons';
|
||||
import { useSelector, useActions } from '../../services/state/store';
|
||||
import { xhrGet, xhrPost } from '../../services/xhr';
|
||||
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const UserSettings = () => {
|
||||
const actions = useActions();
|
||||
const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
|
||||
@@ -33,7 +31,9 @@ const UserSettings = () => {
|
||||
if (response.status === 200) {
|
||||
setCoords(response.json.coords);
|
||||
await actions.userSettings.getUserSettings();
|
||||
Toast.success('Settings saved successfully');
|
||||
Toast.success(
|
||||
'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');
|
||||
}
|
||||
@@ -70,8 +70,6 @@ const UserSettings = () => {
|
||||
|
||||
return (
|
||||
<div className="user-settings">
|
||||
<Title heading={2}>User Specific Settings</Title>
|
||||
<Divider />
|
||||
<SegmentPart
|
||||
name="Distance claculation"
|
||||
Icon={IconHome}
|
||||
|
||||
301
yarn.lock
301
yarn.lock
@@ -20,25 +20,39 @@
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.6":
|
||||
"@babel/code-frame@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c"
|
||||
integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/compat-data@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz"
|
||||
integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==
|
||||
|
||||
"@babel/core@7.28.6", "@babel/core@^7.28.5":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz"
|
||||
integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==
|
||||
"@babel/compat-data@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d"
|
||||
integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==
|
||||
|
||||
"@babel/core@7.29.0", "@babel/core@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322"
|
||||
integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.28.6"
|
||||
"@babel/generator" "^7.28.6"
|
||||
"@babel/code-frame" "^7.29.0"
|
||||
"@babel/generator" "^7.29.0"
|
||||
"@babel/helper-compilation-targets" "^7.28.6"
|
||||
"@babel/helper-module-transforms" "^7.28.6"
|
||||
"@babel/helpers" "^7.28.6"
|
||||
"@babel/parser" "^7.28.6"
|
||||
"@babel/parser" "^7.29.0"
|
||||
"@babel/template" "^7.28.6"
|
||||
"@babel/traverse" "^7.28.6"
|
||||
"@babel/types" "^7.28.6"
|
||||
"@babel/traverse" "^7.29.0"
|
||||
"@babel/types" "^7.29.0"
|
||||
"@jridgewell/remapping" "^2.3.5"
|
||||
convert-source-map "^2.0.0"
|
||||
debug "^4.1.0"
|
||||
@@ -66,6 +80,17 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/generator@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.0.tgz#4cba5a76b3c71d8be31761b03329d5dc7768447f"
|
||||
integrity sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.29.0"
|
||||
"@babel/types" "^7.29.0"
|
||||
"@jridgewell/gen-mapping" "^0.3.12"
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3":
|
||||
version "7.27.3"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz"
|
||||
@@ -73,7 +98,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.27.3"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6":
|
||||
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz"
|
||||
integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==
|
||||
@@ -106,16 +131,16 @@
|
||||
regexpu-core "^6.3.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-define-polyfill-provider@^0.6.5":
|
||||
version "0.6.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz"
|
||||
integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==
|
||||
"@babel/helper-define-polyfill-provider@^0.6.6":
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz#714dfe33d8bd710f556df59953720f6eeb6c1a14"
|
||||
integrity sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==
|
||||
dependencies:
|
||||
"@babel/helper-compilation-targets" "^7.27.2"
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
debug "^4.4.1"
|
||||
"@babel/helper-compilation-targets" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
debug "^4.4.3"
|
||||
lodash.debounce "^4.0.8"
|
||||
resolve "^1.22.10"
|
||||
resolve "^1.22.11"
|
||||
|
||||
"@babel/helper-globals@^7.28.0":
|
||||
version "7.28.0"
|
||||
@@ -138,7 +163,7 @@
|
||||
"@babel/traverse" "^7.28.6"
|
||||
"@babel/types" "^7.28.6"
|
||||
|
||||
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3", "@babel/helper-module-transforms@^7.28.6":
|
||||
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz"
|
||||
integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==
|
||||
@@ -224,6 +249,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.6"
|
||||
|
||||
"@babel/parser@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6"
|
||||
integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==
|
||||
dependencies:
|
||||
"@babel/types" "^7.29.0"
|
||||
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz"
|
||||
@@ -304,14 +336,14 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-async-generator-functions@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz"
|
||||
integrity sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==
|
||||
"@babel/plugin-transform-async-generator-functions@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz#63ed829820298f0bf143d5a4a68fb8c06ffd742f"
|
||||
integrity sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
"@babel/helper-remap-async-to-generator" "^7.27.1"
|
||||
"@babel/traverse" "^7.28.6"
|
||||
"@babel/traverse" "^7.29.0"
|
||||
|
||||
"@babel/plugin-transform-async-to-generator@^7.28.6":
|
||||
version "7.28.6"
|
||||
@@ -395,10 +427,10 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz"
|
||||
integrity sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz#8014b8a6cfd0e7b92762724443bf0d2400f26df1"
|
||||
integrity sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==
|
||||
dependencies:
|
||||
"@babel/helper-create-regexp-features-plugin" "^7.28.5"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
@@ -493,15 +525,15 @@
|
||||
"@babel/helper-module-transforms" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
|
||||
"@babel/plugin-transform-modules-systemjs@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz"
|
||||
integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==
|
||||
"@babel/plugin-transform-modules-systemjs@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964"
|
||||
integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms" "^7.28.3"
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
"@babel/helper-module-transforms" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
"@babel/traverse" "^7.28.5"
|
||||
"@babel/traverse" "^7.29.0"
|
||||
|
||||
"@babel/plugin-transform-modules-umd@^7.27.1":
|
||||
version "7.27.1"
|
||||
@@ -511,13 +543,13 @@
|
||||
"@babel/helper-module-transforms" "^7.27.1"
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz"
|
||||
integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==
|
||||
"@babel/plugin-transform-named-capturing-groups-regex@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz#a26cd51e09c4718588fc4cce1c5d1c0152102d6a"
|
||||
integrity sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==
|
||||
dependencies:
|
||||
"@babel/helper-create-regexp-features-plugin" "^7.27.1"
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
"@babel/helper-create-regexp-features-plugin" "^7.28.5"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
|
||||
"@babel/plugin-transform-new-target@^7.27.1":
|
||||
version "7.27.1"
|
||||
@@ -652,10 +684,10 @@
|
||||
"@babel/helper-annotate-as-pure" "^7.27.1"
|
||||
"@babel/helper-plugin-utils" "^7.27.1"
|
||||
|
||||
"@babel/plugin-transform-regenerator@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz"
|
||||
integrity sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==
|
||||
"@babel/plugin-transform-regenerator@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b"
|
||||
integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
|
||||
@@ -741,12 +773,12 @@
|
||||
"@babel/helper-create-regexp-features-plugin" "^7.28.5"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
|
||||
"@babel/preset-env@7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz"
|
||||
integrity sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==
|
||||
"@babel/preset-env@7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.0.tgz#c55db400c515a303662faaefd2d87e796efa08d0"
|
||||
integrity sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.28.6"
|
||||
"@babel/compat-data" "^7.29.0"
|
||||
"@babel/helper-compilation-targets" "^7.28.6"
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
"@babel/helper-validator-option" "^7.27.1"
|
||||
@@ -760,7 +792,7 @@
|
||||
"@babel/plugin-syntax-import-attributes" "^7.28.6"
|
||||
"@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
|
||||
"@babel/plugin-transform-arrow-functions" "^7.27.1"
|
||||
"@babel/plugin-transform-async-generator-functions" "^7.28.6"
|
||||
"@babel/plugin-transform-async-generator-functions" "^7.29.0"
|
||||
"@babel/plugin-transform-async-to-generator" "^7.28.6"
|
||||
"@babel/plugin-transform-block-scoped-functions" "^7.27.1"
|
||||
"@babel/plugin-transform-block-scoping" "^7.28.6"
|
||||
@@ -771,7 +803,7 @@
|
||||
"@babel/plugin-transform-destructuring" "^7.28.5"
|
||||
"@babel/plugin-transform-dotall-regex" "^7.28.6"
|
||||
"@babel/plugin-transform-duplicate-keys" "^7.27.1"
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.28.6"
|
||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.0"
|
||||
"@babel/plugin-transform-dynamic-import" "^7.27.1"
|
||||
"@babel/plugin-transform-explicit-resource-management" "^7.28.6"
|
||||
"@babel/plugin-transform-exponentiation-operator" "^7.28.6"
|
||||
@@ -784,9 +816,9 @@
|
||||
"@babel/plugin-transform-member-expression-literals" "^7.27.1"
|
||||
"@babel/plugin-transform-modules-amd" "^7.27.1"
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.28.6"
|
||||
"@babel/plugin-transform-modules-systemjs" "^7.28.5"
|
||||
"@babel/plugin-transform-modules-systemjs" "^7.29.0"
|
||||
"@babel/plugin-transform-modules-umd" "^7.27.1"
|
||||
"@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1"
|
||||
"@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0"
|
||||
"@babel/plugin-transform-new-target" "^7.27.1"
|
||||
"@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6"
|
||||
"@babel/plugin-transform-numeric-separator" "^7.28.6"
|
||||
@@ -798,7 +830,7 @@
|
||||
"@babel/plugin-transform-private-methods" "^7.28.6"
|
||||
"@babel/plugin-transform-private-property-in-object" "^7.28.6"
|
||||
"@babel/plugin-transform-property-literals" "^7.27.1"
|
||||
"@babel/plugin-transform-regenerator" "^7.28.6"
|
||||
"@babel/plugin-transform-regenerator" "^7.29.0"
|
||||
"@babel/plugin-transform-regexp-modifiers" "^7.28.6"
|
||||
"@babel/plugin-transform-reserved-words" "^7.27.1"
|
||||
"@babel/plugin-transform-shorthand-properties" "^7.27.1"
|
||||
@@ -811,10 +843,10 @@
|
||||
"@babel/plugin-transform-unicode-regex" "^7.27.1"
|
||||
"@babel/plugin-transform-unicode-sets-regex" "^7.28.6"
|
||||
"@babel/preset-modules" "0.1.6-no-external-plugins"
|
||||
babel-plugin-polyfill-corejs2 "^0.4.14"
|
||||
babel-plugin-polyfill-corejs3 "^0.13.0"
|
||||
babel-plugin-polyfill-regenerator "^0.6.5"
|
||||
core-js-compat "^3.43.0"
|
||||
babel-plugin-polyfill-corejs2 "^0.4.15"
|
||||
babel-plugin-polyfill-corejs3 "^0.14.0"
|
||||
babel-plugin-polyfill-regenerator "^0.6.6"
|
||||
core-js-compat "^3.48.0"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/preset-modules@0.1.6-no-external-plugins":
|
||||
@@ -865,6 +897,19 @@
|
||||
"@babel/types" "^7.28.6"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/traverse@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a"
|
||||
integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.29.0"
|
||||
"@babel/generator" "^7.29.0"
|
||||
"@babel/helper-globals" "^7.28.0"
|
||||
"@babel/parser" "^7.29.0"
|
||||
"@babel/template" "^7.28.6"
|
||||
"@babel/types" "^7.29.0"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.4.4":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz"
|
||||
@@ -873,6 +918,14 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
|
||||
"@babel/types@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
|
||||
integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
|
||||
"@dnd-kit/accessibility@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz"
|
||||
@@ -1483,10 +1536,10 @@
|
||||
resolved "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz"
|
||||
integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-beta.53":
|
||||
version "1.0.0-beta.53"
|
||||
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz"
|
||||
integrity sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==
|
||||
"@rolldown/pluginutils@1.0.0-rc.2":
|
||||
version "1.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz#10324e74cb3396cb7b616042ea7e9e6aa7d8d458"
|
||||
integrity sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.49.0":
|
||||
version "4.49.0"
|
||||
@@ -1844,15 +1897,15 @@
|
||||
resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz"
|
||||
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
|
||||
|
||||
"@vitejs/plugin-react@5.1.2":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz"
|
||||
integrity sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==
|
||||
"@vitejs/plugin-react@5.1.3":
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz#05eba092cc89a6ff89668d3f62258e2c5a8a9122"
|
||||
integrity sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==
|
||||
dependencies:
|
||||
"@babel/core" "^7.28.5"
|
||||
"@babel/core" "^7.29.0"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.27.1"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.27.1"
|
||||
"@rolldown/pluginutils" "1.0.0-beta.53"
|
||||
"@rolldown/pluginutils" "1.0.0-rc.2"
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.18.0"
|
||||
|
||||
@@ -2068,29 +2121,29 @@ b4a@^1.6.4:
|
||||
resolved "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz"
|
||||
integrity sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==
|
||||
|
||||
babel-plugin-polyfill-corejs2@^0.4.14:
|
||||
version "0.4.14"
|
||||
resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz"
|
||||
integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==
|
||||
babel-plugin-polyfill-corejs2@^0.4.15:
|
||||
version "0.4.15"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz#808fa349686eea4741807cfaaa2aa3aa57ce120a"
|
||||
integrity sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.27.7"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.5"
|
||||
"@babel/compat-data" "^7.28.6"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.6"
|
||||
semver "^6.3.1"
|
||||
|
||||
babel-plugin-polyfill-corejs3@^0.13.0:
|
||||
version "0.13.0"
|
||||
resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz"
|
||||
integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==
|
||||
babel-plugin-polyfill-corejs3@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz#65b06cda48d6e447e1e926681f5a247c6ae2b9cf"
|
||||
integrity sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==
|
||||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.5"
|
||||
core-js-compat "^3.43.0"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.6"
|
||||
core-js-compat "^3.48.0"
|
||||
|
||||
babel-plugin-polyfill-regenerator@^0.6.5:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz"
|
||||
integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==
|
||||
babel-plugin-polyfill-regenerator@^0.6.6:
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz#69f5dd263cab933c42fe5ea05e83443b374bd4bf"
|
||||
integrity sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==
|
||||
dependencies:
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.5"
|
||||
"@babel/helper-define-polyfill-provider" "^0.6.6"
|
||||
|
||||
bail@^2.0.0:
|
||||
version "2.0.2"
|
||||
@@ -2149,6 +2202,11 @@ base64-js@^1.3.1:
|
||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.9.0:
|
||||
version "2.9.19"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488"
|
||||
integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==
|
||||
|
||||
basic-ftp@^5.0.2:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz"
|
||||
@@ -2240,7 +2298,7 @@ browser-stdout@^1.3.1:
|
||||
resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz"
|
||||
integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
|
||||
|
||||
browserslist@^4.24.0, browserslist@^4.25.3:
|
||||
browserslist@^4.24.0:
|
||||
version "4.25.3"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz"
|
||||
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
|
||||
@@ -2250,6 +2308,17 @@ browserslist@^4.24.0, browserslist@^4.25.3:
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.3"
|
||||
|
||||
browserslist@^4.28.1:
|
||||
version "4.28.1"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
|
||||
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
|
||||
dependencies:
|
||||
baseline-browser-mapping "^2.9.0"
|
||||
caniuse-lite "^1.0.30001759"
|
||||
electron-to-chromium "^1.5.263"
|
||||
node-releases "^2.0.27"
|
||||
update-browserslist-db "^1.2.0"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
|
||||
@@ -2309,6 +2378,11 @@ caniuse-lite@^1.0.30001735:
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz"
|
||||
integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==
|
||||
|
||||
caniuse-lite@^1.0.30001759:
|
||||
version "1.0.30001767"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz#0279c498e862efb067938bba0a0aabafe8d0b730"
|
||||
integrity sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
|
||||
@@ -2562,12 +2636,12 @@ copy-text-to-clipboard@^2.1.1:
|
||||
resolved "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-2.2.0.tgz"
|
||||
integrity sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ==
|
||||
|
||||
core-js-compat@^3.43.0:
|
||||
version "3.45.1"
|
||||
resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz"
|
||||
integrity sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==
|
||||
core-js-compat@^3.48.0:
|
||||
version "3.48.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6"
|
||||
integrity sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==
|
||||
dependencies:
|
||||
browserslist "^4.25.3"
|
||||
browserslist "^4.28.1"
|
||||
|
||||
core-js@^3.22.4:
|
||||
version "3.47.0"
|
||||
@@ -2670,7 +2744,7 @@ debug@3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.1, debug@^4.4.3:
|
||||
debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz"
|
||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||
@@ -2851,6 +2925,11 @@ electron-to-chromium@^1.5.204:
|
||||
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz"
|
||||
integrity sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==
|
||||
|
||||
electron-to-chromium@^1.5.263:
|
||||
version "1.5.283"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz#51d492c37c2d845a0dccb113fe594880c8616de8"
|
||||
integrity sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==
|
||||
|
||||
emoji-regex@^10.3.0:
|
||||
version "10.5.0"
|
||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz"
|
||||
@@ -4063,7 +4142,7 @@ is-callable@^1.2.7:
|
||||
resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz"
|
||||
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
|
||||
|
||||
is-core-module@^2.13.0, is-core-module@^2.16.0:
|
||||
is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1:
|
||||
version "2.16.1"
|
||||
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz"
|
||||
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
|
||||
@@ -5417,6 +5496,11 @@ node-releases@^2.0.19:
|
||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz"
|
||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||
|
||||
node-releases@^2.0.27:
|
||||
version "2.0.27"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||
|
||||
nodemon@^3.1.11:
|
||||
version "3.1.11"
|
||||
resolved "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz"
|
||||
@@ -6421,7 +6505,7 @@ resolve-protobuf-schema@^2.1.0:
|
||||
dependencies:
|
||||
protocol-buffers-schema "^3.3.1"
|
||||
|
||||
resolve@^1.1.6, resolve@^1.22.10:
|
||||
resolve@^1.1.6:
|
||||
version "1.22.10"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz"
|
||||
integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
|
||||
@@ -6430,6 +6514,15 @@ resolve@^1.1.6, resolve@^1.22.10:
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
resolve@^1.22.11:
|
||||
version "1.22.11"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
|
||||
integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
|
||||
dependencies:
|
||||
is-core-module "^2.16.1"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
resolve@^2.0.0-next.5:
|
||||
version "2.0.0-next.5"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz"
|
||||
@@ -7359,6 +7452,14 @@ update-browserslist-db@^1.1.3:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
update-browserslist-db@^1.2.0:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
|
||||
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
|
||||
dependencies:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
|
||||
@@ -7633,10 +7734,10 @@ zod@^3.24.1:
|
||||
resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz"
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
|
||||
zustand@^5.0.10:
|
||||
version "5.0.10"
|
||||
resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz"
|
||||
integrity sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==
|
||||
zustand@^5.0.11:
|
||||
version "5.0.11"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494"
|
||||
integrity sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==
|
||||
|
||||
zwitch@^2.0.0:
|
||||
version "2.0.4"
|
||||
|
||||
Reference in New Issue
Block a user