mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206f768b41 | ||
|
|
2302f69ff3 | ||
|
|
9bb33e723a | ||
|
|
cca1463a68 | ||
|
|
314b1818d7 | ||
|
|
25cc7fb650 | ||
|
|
78df4b21a6 | ||
|
|
d89b078237 |
26
.github/workflows/check_source.yml
vendored
Normal file
26
.github/workflows/check_source.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Check the source code
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
jobs:
|
||||||
|
check_source_code:
|
||||||
|
name: Check the source code
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: yarn format:check
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint
|
||||||
2
.github/workflows/stales.yml
vendored
2
.github/workflows/stales.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: "Close stale issues and PRs"
|
name: Close stale issues and PRs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -13,8 +13,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- uses: actions/setup-node@v4
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -106,14 +106,14 @@ exports.config = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Running Tests
|
#### Running Tests
|
||||||
If you've written a new provider you are an awesome person. If you now write tests for it, you are even more awesome. And who doesn't want to be more awesome right?
|
If you've written a new provider you are an awesome person. If you now write tests for it, you are even more awesome. And who doesn't want to be more awesome, right?
|
||||||
|
|
||||||
#### Codestyle
|
#### Codestyle
|
||||||
I'm using Eslint to maintain quote style and quality. Do not skip it...
|
I'm using ESLint to maintain quote style and quality. Do not skip it...
|
||||||
|
|
||||||
##### To do before merging:
|
##### To-do before merging:
|
||||||
|
|
||||||
- executed tests? (`pnpm test`)
|
- Have you executed the tests? (`yarn test`)
|
||||||
- sure the changes are useful for everybody? Or is it maybe a custom modification just for your case?
|
- Are you sure the changes are useful for everybody? Or is it maybe a custom modification just for your case?
|
||||||
|
|
||||||
_Thanks!_ :heart:
|
_Thanks!_ :heart:
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
|||||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
# Copy lockfiles first to leverage cache for dependencies
|
# Copy lockfiles first to leverage cache for dependencies
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock .
|
||||||
|
|
||||||
# Set Yarn timeout, install dependencies and PM2 globally
|
# Set Yarn timeout, install dependencies and PM2 globally
|
||||||
RUN yarn config set network-timeout 600000 \
|
RUN yarn config set network-timeout 600000 \
|
||||||
&& yarn install --frozen-lockfile \
|
&& yarn --frozen-lockfile \
|
||||||
&& yarn global add pm2
|
&& yarn global add pm2
|
||||||
|
|
||||||
# Copy application source and build production assets
|
# Copy application source and build production assets
|
||||||
COPY . ./
|
COPY . .
|
||||||
RUN yarn run prod
|
RUN yarn build:frontend
|
||||||
|
|
||||||
# Prepare runtime directories and symlinks for data and config
|
# Prepare runtime directories and symlinks for data and config
|
||||||
RUN mkdir -p /db /conf \
|
RUN mkdir -p /db /conf \
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -1,6 +1,6 @@
|
|||||||
<img src="https://github.com/orangecoding/fredy/blob/master/doc/logo.png" width="400">
|
<img src="https://github.com/orangecoding/fredy/blob/master/doc/logo.png" width="400">
|
||||||
|
|
||||||
 [](https://github.com/orangecoding/fredy/actions/workflows/docker.yml)
|
 [](https://github.com/orangecoding/fredy/actions/workflows/docker.yml) 
|
||||||
|
|
||||||
Searching an apartment in Germany can be a frustrating task. Not any longer though, as _Fredy_ will take over and will only notify you once new listings have been found that match your requirements.
|
Searching an apartment in Germany can be a frustrating task. Not any longer though, as _Fredy_ will take over and will only notify you once new listings have been found that match your requirements.
|
||||||
|
|
||||||
@@ -23,9 +23,9 @@ If you want to try out _Fredy_, you can access the demo version [here](https://f
|
|||||||
- Make sure to use Node.js 20 or above
|
- Make sure to use Node.js 20 or above
|
||||||
- Run the following commands:
|
- Run the following commands:
|
||||||
```ssh
|
```ssh
|
||||||
yarn (or npm install)
|
yarn
|
||||||
yarn run prod
|
yarn run start:backend
|
||||||
yarn run start
|
yarn run start:frontend
|
||||||
```
|
```
|
||||||
_Fredy_ will start with the default port, set to `9998`. You can access _Fredy_ by opening your browser at `http://localhost:9998`. The default login is `admin`, both for username and password. You should change the password as soon as possible when you plan to run Fredy on a server.
|
_Fredy_ will start with the default port, set to `9998`. You can access _Fredy_ by opening your browser at `http://localhost:9998`. The default login is `admin`, both for username and password. You should change the password as soon as possible when you plan to run Fredy on a server.
|
||||||
|
|
||||||
@@ -61,14 +61,13 @@ As an administrator, you can create, edit and remove users from _Fredy_. Be care
|
|||||||
# Development
|
# Development
|
||||||
|
|
||||||
### Running Fredy in development mode
|
### Running Fredy in development mode
|
||||||
To run _Fredy_ in development mode, you need to run the backend & frontend separately.
|
|
||||||
Start the backend with:
|
Start the backend with:
|
||||||
```shell
|
```shell
|
||||||
yarn run start
|
yarn run start:backend:dev
|
||||||
```
|
```
|
||||||
For the frontend, run:
|
For the frontend, run:
|
||||||
```shell
|
```shell
|
||||||
yarn run dev
|
yarn run start:frontend:dev
|
||||||
```
|
```
|
||||||
You should now be able to access _Fredy_ from your browser. Check your Terminal to see what port the frontend is running on.
|
You should now be able to access _Fredy_ from your browser. Check your Terminal to see what port the frontend is running on.
|
||||||
|
|
||||||
|
|||||||
64
index.js
64
index.js
@@ -1,14 +1,14 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import {config} from './lib/utils.js';
|
import { config } from './lib/utils.js';
|
||||||
import * as similarityCache from './lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from './lib/services/similarity-check/similarityCache.js';
|
||||||
import { setLastJobExecution } from './lib/services/storage/listingsStorage.js';
|
import { setLastJobExecution } from './lib/services/storage/listingsStorage.js';
|
||||||
import * as jobStorage from './lib/services/storage/jobStorage.js';
|
import * as jobStorage from './lib/services/storage/jobStorage.js';
|
||||||
import FredyRuntime from './lib/FredyRuntime.js';
|
import FredyRuntime from './lib/FredyRuntime.js';
|
||||||
import { duringWorkingHoursOrNotSet } from './lib/utils.js';
|
import { duringWorkingHoursOrNotSet } from './lib/utils.js';
|
||||||
import './lib/api/api.js';
|
import './lib/api/api.js';
|
||||||
import {track} from './lib/services/tracking/Tracker.js';
|
import { track } from './lib/services/tracking/Tracker.js';
|
||||||
import {handleDemoUser} from './lib/services/storage/userStorage.js';
|
import { handleDemoUser } from './lib/services/storage/userStorage.js';
|
||||||
import {cleanupDemoAtMidnight} from './lib/services/demoCleanup.js';
|
import { cleanupDemoAtMidnight } from './lib/services/demoCleanup.js';
|
||||||
//if db folder does not exist, ensure to create it before loading anything else
|
//if db folder does not exist, ensure to create it before loading anything else
|
||||||
if (!fs.existsSync('./db')) {
|
if (!fs.existsSync('./db')) {
|
||||||
fs.mkdirSync('./db');
|
fs.mkdirSync('./db');
|
||||||
@@ -19,13 +19,13 @@ const provider = fs.readdirSync(path).filter((file) => file.endsWith('.js'));
|
|||||||
const INTERVAL = config.interval * 60 * 1000;
|
const INTERVAL = config.interval * 60 * 1000;
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`);
|
console.log(`Started Fredy successfully. Ui can be accessed via http://localhost:${config.port}`);
|
||||||
if(config.demoMode){
|
if (config.demoMode) {
|
||||||
console.info('Running in demo mode');
|
console.info('Running in demo mode');
|
||||||
cleanupDemoAtMidnight();
|
cleanupDemoAtMidnight();
|
||||||
}
|
}
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
const fetchedProvider = await Promise.all(
|
const fetchedProvider = await Promise.all(
|
||||||
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`))
|
provider.filter((provider) => provider.endsWith('.js')).map(async (pro) => import(`${path}/${pro}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
handleDemoUser();
|
handleDemoUser();
|
||||||
@@ -33,30 +33,30 @@ handleDemoUser();
|
|||||||
setInterval(
|
setInterval(
|
||||||
(function exec() {
|
(function exec() {
|
||||||
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
||||||
if(!config.demoMode) {
|
if (!config.demoMode) {
|
||||||
if (isDuringWorkingHoursOrNotSet) {
|
if (isDuringWorkingHoursOrNotSet) {
|
||||||
track();
|
track();
|
||||||
config.lastRun = Date.now();
|
config.lastRun = Date.now();
|
||||||
jobStorage
|
jobStorage
|
||||||
.getJobs()
|
.getJobs()
|
||||||
.filter((job) => job.enabled)
|
.filter((job) => job.enabled)
|
||||||
.forEach((job) => {
|
.forEach((job) => {
|
||||||
job.provider
|
job.provider
|
||||||
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
|
.filter((p) => fetchedProvider.find((fp) => fp.metaInformation.id === p.id) != null)
|
||||||
.forEach(async (prov) => {
|
.forEach(async (prov) => {
|
||||||
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
|
const pro = fetchedProvider.find((fp) => fp.metaInformation.id === prov.id);
|
||||||
pro.init(prov, job.blacklist);
|
pro.init(prov, job.blacklist);
|
||||||
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
|
await new FredyRuntime(pro.config, job.notificationAdapter, prov.id, job.id, similarityCache).execute();
|
||||||
setLastJobExecution(job.id);
|
setLastJobExecution(job.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.debug('Working hours set. Skipping as outside of working hours.');
|
console.debug('Working hours set. Skipping as outside of working hours.');
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exec;
|
return exec;
|
||||||
})(),
|
})(),
|
||||||
INTERVAL
|
INTERVAL,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import restana from 'restana';
|
|||||||
import files from 'serve-static';
|
import files from 'serve-static';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getDirName } from '../utils.js';
|
import { getDirName } from '../utils.js';
|
||||||
import {demoRouter} from './routes/demoRouter.js';
|
import { demoRouter } from './routes/demoRouter.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 = config.port || 9998;
|
const PORT = config.port || 9998;
|
||||||
|
|||||||
@@ -22,16 +22,18 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
const { token, chatId } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
const { token, chatId } = notificationConfig.find((adapter) => adapter.id === config.id).fields;
|
||||||
const job = getJob(jobKey);
|
const job = getJob(jobKey);
|
||||||
const jobName = job == null ? jobKey : job.name;
|
const jobName = job == null ? jobKey : job.name;
|
||||||
//we have to split messages into chunk, because otherwise messages are going to become too big and will fail
|
// we have to split messages into chunks, because otherwise messages are going to become too big and will fail
|
||||||
const chunks = arrayChunks(newListings, MAX_ENTITIES_PER_CHUNK);
|
const chunks = arrayChunks(newListings, MAX_ENTITIES_PER_CHUNK);
|
||||||
const promises = chunks.map((chunk) => {
|
const promises = chunks.map((chunk) => {
|
||||||
let message = `<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings:\n\n`;
|
const messageParagraphs = [];
|
||||||
message += chunk.map(
|
|
||||||
|
messageParagraphs.push(`<i>${jobName}</i> (${serviceName}) found <b>${newListings.length}</b> new listings:`);
|
||||||
|
messageParagraphs.push(...chunk.map(
|
||||||
(o) =>
|
(o) =>
|
||||||
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, ''), 45).trim()}</b></a>\n` +
|
`<a href='${o.link}'><b>${shorten(o.title.replace(/\*/g, ''), 45).trim()}</b></a>\n` +
|
||||||
[o.address, o.price, o.size].join(' | ') +
|
[o.address, o.price, o.size].join(' | ')
|
||||||
'\n\n',
|
));
|
||||||
);
|
|
||||||
/**
|
/**
|
||||||
* This is to not break the rate limit. It is to only send 1 message per second
|
* This is to not break the rate limit. It is to only send 1 message per second
|
||||||
*/
|
*/
|
||||||
@@ -41,7 +43,7 @@ export const send = ({ serviceName, newListings, notificationConfig, jobKey }) =
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
text: message,
|
text: messageParagraphs.join('\n\n'),
|
||||||
parse_mode: 'HTML',
|
parse_mode: 'HTML',
|
||||||
disable_web_page_preview: true,
|
disable_web_page_preview: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const adapter = await Promise.all(
|
|||||||
fs
|
fs
|
||||||
.readdirSync('./lib/notification/adapter')
|
.readdirSync('./lib/notification/adapter')
|
||||||
.filter((file) => file.endsWith('.js'))
|
.filter((file) => file.endsWith('.js'))
|
||||||
.map(async (integPath) => await import(`${path}/${integPath}`))
|
.map(async (integPath) => await import(`${path}/${integPath}`)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (adapter.length === 0) {
|
if (adapter.length === 0) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import utils, {buildHash} from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
function shortenLink(link) {
|
function shortenLink(link) {
|
||||||
return link.substring(0, link.indexOf('?'));
|
return link.substring(0, link.indexOf('?'));
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import utils, { buildHash } from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
import { convertWebToMobile } from '../services/immoscout/immoscout-web-translater.js';
|
import { convertWebToMobile } from '../services/immoscout/immoscout-web-translator.js';
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
async function getListings(url) {
|
async function getListings(url) {
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
import utils, {buildHash} from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
let appliedBlacklistedDistricts = [];
|
let appliedBlacklistedDistricts = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const size = o.size || '--- m²';
|
const size = o.size || '--- m²';
|
||||||
const id = buildHash(o.id, o.price);
|
const id = buildHash(o.id, o.price);
|
||||||
const link = `https://www.kleinanzeigen.de${o.link}`;
|
const link = `https://www.kleinanzeigen.de${o.link}`;
|
||||||
return Object.assign(o, {id, size, link});
|
return Object.assign(o, { id, size, link });
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||||
const isBlacklistedDistrict =
|
const isBlacklistedDistrict =
|
||||||
appliedBlacklistedDistricts.length === 0 ? false : utils.isOneOf(o.description, appliedBlacklistedDistricts);
|
appliedBlacklistedDistricts.length === 0 ? false : utils.isOneOf(o.description, appliedBlacklistedDistricts);
|
||||||
return o.title != null && !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted;
|
return o.title != null && !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '#srchrslt-adtable .ad-listitem ',
|
crawlContainer: '#srchrslt-adtable .ad-listitem ',
|
||||||
//sort by date is standard oO
|
//sort by date is standard oO
|
||||||
sortByDateParam: null,
|
sortByDateParam: null,
|
||||||
waitForSelector: 'body',
|
waitForSelector: 'body',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '.aditem@data-adid | int',
|
id: '.aditem@data-adid | int',
|
||||||
price: '.aditem-main--middle--price-shipping--price | removeNewline | trim',
|
price: '.aditem-main--middle--price-shipping--price | removeNewline | trim',
|
||||||
size: '.aditem-main .text-module-end | removeNewline | trim',
|
size: '.aditem-main .text-module-end | removeNewline | trim',
|
||||||
title: '.aditem-main .text-module-begin a | removeNewline | trim',
|
title: '.aditem-main .text-module-begin a | removeNewline | trim',
|
||||||
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
|
link: '.aditem-main .text-module-begin a@href | removeNewline | trim',
|
||||||
description: '.aditem-main .aditem-main--middle--description | removeNewline | trim',
|
description: '.aditem-main .aditem-main--middle--description | removeNewline | trim',
|
||||||
address: '.aditem-main--top--left | trim | removeNewline',
|
address: '.aditem-main--top--left | trim | removeNewline',
|
||||||
},
|
},
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Ebay Kleinanzeigen',
|
name: 'Ebay Kleinanzeigen',
|
||||||
baseUrl: 'https://www.kleinanzeigen.de/',
|
baseUrl: 'https://www.kleinanzeigen.de/',
|
||||||
id: 'kleinanzeigen',
|
id: 'kleinanzeigen',
|
||||||
};
|
};
|
||||||
export const init = (sourceConfig, blacklist, blacklistedDistricts) => {
|
export const init = (sourceConfig, blacklist, blacklistedDistricts) => {
|
||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = sourceConfig.url;
|
config.url = sourceConfig.url;
|
||||||
appliedBlacklistedDistricts = blacklistedDistricts || [];
|
appliedBlacklistedDistricts = blacklistedDistricts || [];
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
};
|
};
|
||||||
export {config};
|
export { config };
|
||||||
|
|||||||
@@ -1,44 +1,46 @@
|
|||||||
import utils, {buildHash} from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function nullOrEmpty(val) {
|
function nullOrEmpty(val) {
|
||||||
return val == null || val.length === 0;
|
return val == null || val.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const link = nullOrEmpty(o.link) ? 'NO LINK' : `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
const link = nullOrEmpty(o.link)
|
||||||
const id = buildHash(o.link, o.price);
|
? 'NO LINK'
|
||||||
return Object.assign(o, {id, link});
|
: `https://www.neubaukompass.de${o.link.substring(o.link.indexOf('/neubau'))}`;
|
||||||
|
const id = buildHash(o.link, o.price);
|
||||||
|
return Object.assign(o, { id, link });
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
return !utils.isOneOf(o.title, appliedBlackList);
|
return !utils.isOneOf(o.title, appliedBlackList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '.col-12.mb-4',
|
crawlContainer: '.col-12.mb-4',
|
||||||
sortByDateParam: 'Sortierung=Id&Richtung=DESC',
|
sortByDateParam: 'Sortierung=Id&Richtung=DESC',
|
||||||
waitForSelector: '.nbk-section',
|
waitForSelector: '.nbk-section',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: 'a@href',
|
id: 'a@href',
|
||||||
title: 'a@title | removeNewline | trim',
|
title: 'a@title | removeNewline | trim',
|
||||||
link: 'a@href',
|
link: 'a@href',
|
||||||
address: '.nbk-project-card__description | removeNewline | trim',
|
address: '.nbk-project-card__description | removeNewline | trim',
|
||||||
price: '.nbk-project-card__spec-item .nbk-project-card__spec-value | removeNewline | trim',
|
price: '.nbk-project-card__spec-item .nbk-project-card__spec-value | removeNewline | trim',
|
||||||
},
|
},
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
};
|
};
|
||||||
export const init = (sourceConfig, blacklist) => {
|
export const init = (sourceConfig, blacklist) => {
|
||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = sourceConfig.url;
|
config.url = sourceConfig.url;
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Neubau Kompass',
|
name: 'Neubau Kompass',
|
||||||
baseUrl: 'https://www.neubaukompass.de/',
|
baseUrl: 'https://www.neubaukompass.de/',
|
||||||
id: 'neubauKompass',
|
id: 'neubauKompass',
|
||||||
};
|
};
|
||||||
export {config};
|
export { config };
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
import utils, {buildHash} from '../utils.js';
|
import utils, { buildHash } from '../utils.js';
|
||||||
|
|
||||||
let appliedBlackList = [];
|
let appliedBlackList = [];
|
||||||
|
|
||||||
function normalize(o) {
|
function normalize(o) {
|
||||||
const id = buildHash(o.id, o.price);
|
const id = buildHash(o.id, o.price);
|
||||||
const link = `https://www.wg-gesucht.de${o.link}`;
|
const link = `https://www.wg-gesucht.de${o.link}`;
|
||||||
return Object.assign(o, { id, link });
|
return Object.assign(o, { id, link });
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlacklist(o) {
|
function applyBlacklist(o) {
|
||||||
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
const titleNotBlacklisted = !utils.isOneOf(o.title, appliedBlackList);
|
||||||
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
const descNotBlacklisted = !utils.isOneOf(o.description, appliedBlackList);
|
||||||
return o.id != null && titleNotBlacklisted && descNotBlacklisted;
|
return o.id != null && titleNotBlacklisted && descNotBlacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
url: null,
|
url: null,
|
||||||
crawlContainer: '#main_column .wgg_card',
|
crawlContainer: '#main_column .wgg_card',
|
||||||
sortByDateParam: 'sort_column=0&sort_order=0',
|
sortByDateParam: 'sort_column=0&sort_order=0',
|
||||||
waitForSelector: 'body',
|
waitForSelector: 'body',
|
||||||
crawlFields: {
|
crawlFields: {
|
||||||
id: '@data-id',
|
id: '@data-id',
|
||||||
details: '.row .noprint .col-xs-11 |removeNewline |trim',
|
details: '.row .noprint .col-xs-11 |removeNewline |trim',
|
||||||
price: '.middle .col-xs-3 |removeNewline |trim',
|
price: '.middle .col-xs-3 |removeNewline |trim',
|
||||||
size: '.middle .text-right |removeNewline |trim',
|
size: '.middle .text-right |removeNewline |trim',
|
||||||
title: '.truncate_title a |removeNewline |trim',
|
title: '.truncate_title a |removeNewline |trim',
|
||||||
link: '.truncate_title a@href',
|
link: '.truncate_title a@href',
|
||||||
},
|
},
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
filter: applyBlacklist,
|
filter: applyBlacklist,
|
||||||
};
|
};
|
||||||
export const init = (sourceConfig, blacklist) => {
|
export const init = (sourceConfig, blacklist) => {
|
||||||
config.enabled = sourceConfig.enabled;
|
config.enabled = sourceConfig.enabled;
|
||||||
config.url = sourceConfig.url;
|
config.url = sourceConfig.url;
|
||||||
appliedBlackList = blacklist || [];
|
appliedBlackList = blacklist || [];
|
||||||
};
|
};
|
||||||
export const metaInformation = {
|
export const metaInformation = {
|
||||||
name: 'Wg gesucht',
|
name: 'Wg gesucht',
|
||||||
baseUrl: 'https://www.wg-gesucht.de/',
|
baseUrl: 'https://www.wg-gesucht.de/',
|
||||||
id: 'wgGesucht',
|
id: 'wgGesucht',
|
||||||
};
|
};
|
||||||
export {config};
|
export { config };
|
||||||
|
|||||||
28
package.json
28
package.json
@@ -1,21 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "11.2.5",
|
"version": "11.3.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": {
|
||||||
"start": "node prod.js",
|
"prepare": "husky",
|
||||||
"dev": "yarn && rm -rf ./ui/public/* && vite",
|
"start:backend": "x-var NODE_ENV=production node index.js",
|
||||||
"ui": "rm -rf ./ui/public/* && vite",
|
"start:backend:dev": "nodemon --watch index.js --watch lib",
|
||||||
"prod": "yarn && vite build --emptyOutDir",
|
"start:frontend": "vite -m production",
|
||||||
"format": "prettier --write lib/**/*.js ui/src/**/*.jsx test/**/*.js *.js --single-quote --print-width 120",
|
"start:frontend:dev": "vite",
|
||||||
|
"build:frontend": "vite build",
|
||||||
|
"format": "prettier --write lib/**/*.js ui/src/**/*.jsx test/**/*.js *.js",
|
||||||
|
"format:check": "prettier --check lib/**/*.js ui/src/**/*.jsx test/**/*.js *.js",
|
||||||
"test": "mocha --loader=esmock --timeout 3000000 test/**/*.test.js",
|
"test": "mocha --loader=esmock --timeout 3000000 test/**/*.test.js",
|
||||||
"lint": "eslint ./index.js ./lib/**/*.js ./test/**/*.js ./ui/src/**/*.jsx"
|
"lint": "eslint index.js lib/**/*.js test/**/*.js ui/src/**/*.jsx",
|
||||||
|
"lint:fix": "yarn lint --fix"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": [
|
"*.{js,jsx}": [
|
||||||
"eslint ./index.js ./lib/**/*.js ./test/**/*.js",
|
"yarn lint",
|
||||||
"prettier --single-quote --print-width 120 --write"
|
"yarn format"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -85,7 +89,8 @@
|
|||||||
"serve-static": "2.2.0",
|
"serve-static": "2.2.0",
|
||||||
"slack": "11.0.2",
|
"slack": "11.0.2",
|
||||||
"string-similarity": "^4.0.4",
|
"string-similarity": "^4.0.4",
|
||||||
"vite": "7.0.5"
|
"vite": "7.0.5",
|
||||||
|
"x-var": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.27.3",
|
"@babel/core": "7.27.3",
|
||||||
@@ -102,6 +107,7 @@
|
|||||||
"less": "4.4.0",
|
"less": "4.4.0",
|
||||||
"lint-staged": "15.5.2",
|
"lint-staged": "15.5.2",
|
||||||
"mocha": "10.8.2",
|
"mocha": "10.8.2",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"redux-logger": "3.0.6"
|
"redux-logger": "3.0.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ curl -H "User-Agent: ImmoScout24_1410_30_._" \
|
|||||||
|
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
The parameters between web and mobile are very different which is why we have to translate them. Please see `immoscout-web-translator.js`.
|
The parameters between web and mobile are very different which is why we have to translate them. Please see [/lib/services/immoscout/immoscout-web-translator.js](https://github.com/orangecoding/fredy/blob/master/lib/services/immoscout/immoscout-web-translator.js).
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import {get} from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import {mockFredy, providerConfig} from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import {expect} from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/immonet.js';
|
import * as provider from '../../lib/provider/immonet.js';
|
||||||
|
|
||||||
describe('#immonet testsuite()', () => {
|
describe('#immonet testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
});
|
});
|
||||||
provider.init(providerConfig.immonet, [], []);
|
provider.init(providerConfig.immonet, [], []);
|
||||||
it('should test immonet provider', async () => {
|
it('should test immonet 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, 'immonet', similarityCache);
|
const fredy = new Fredy(provider.config, null, provider.metaInformation.id, 'immonet', 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();
|
||||||
expect(notificationObj).to.be.a('object');
|
expect(notificationObj).to.be.a('object');
|
||||||
expect(notificationObj.serviceName).to.equal('immonet');
|
expect(notificationObj.serviceName).to.equal('immonet');
|
||||||
notificationObj.payload.forEach((notify) => {
|
notificationObj.payload.forEach((notify) => {
|
||||||
/** check the actual structure **/
|
/** check the actual structure **/
|
||||||
expect(notify.id).to.be.a('string');
|
expect(notify.id).to.be.a('string');
|
||||||
expect(notify.price).to.be.a('string');
|
expect(notify.price).to.be.a('string');
|
||||||
expect(notify.size).to.be.a('string');
|
expect(notify.size).to.be.a('string');
|
||||||
expect(notify.title).to.be.a('string');
|
expect(notify.title).to.be.a('string');
|
||||||
expect(notify.link).to.be.a('string');
|
expect(notify.link).to.be.a('string');
|
||||||
expect(notify.address).to.be.a('string');
|
expect(notify.address).to.be.a('string');
|
||||||
|
|
||||||
expect(notify.size).that.does.include('m²');
|
expect(notify.size).that.does.include('m²');
|
||||||
expect(notify.title).to.be.not.empty;
|
expect(notify.title).to.be.not.empty;
|
||||||
expect(notify.address).to.be.not.empty;
|
expect(notify.address).to.be.not.empty;
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
import * as similarityCache from '../../lib/services/similarity-check/similarityCache.js';
|
||||||
import {get} from '../mocks/mockNotification.js';
|
import { get } from '../mocks/mockNotification.js';
|
||||||
import {mockFredy, providerConfig} from '../utils.js';
|
import { mockFredy, providerConfig } from '../utils.js';
|
||||||
import {expect} from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as provider from '../../lib/provider/neubauKompass.js';
|
import * as provider from '../../lib/provider/neubauKompass.js';
|
||||||
|
|
||||||
describe('#neubauKompass testsuite()', () => {
|
describe('#neubauKompass testsuite()', () => {
|
||||||
after(() => {
|
after(() => {
|
||||||
similarityCache.stopCacheCleanup();
|
similarityCache.stopCacheCleanup();
|
||||||
});
|
});
|
||||||
provider.init(providerConfig.neubauKompass, [], []);
|
provider.init(providerConfig.neubauKompass, [], []);
|
||||||
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, 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();
|
||||||
expect(notificationObj.serviceName).to.equal('neubauKompass');
|
expect(notificationObj.serviceName).to.equal('neubauKompass');
|
||||||
notificationObj.payload.forEach((notify) => {
|
notificationObj.payload.forEach((notify) => {
|
||||||
expect(notify).to.be.a('object');
|
expect(notify).to.be.a('object');
|
||||||
/** check the actual structure **/
|
/** check the actual structure **/
|
||||||
expect(notify.id).to.be.a('string');
|
expect(notify.id).to.be.a('string');
|
||||||
expect(notify.title).to.be.a('string');
|
expect(notify.title).to.be.a('string');
|
||||||
expect(notify.link).to.be.a('string');
|
expect(notify.link).to.be.a('string');
|
||||||
expect(notify.address).to.be.a('string');
|
expect(notify.address).to.be.a('string');
|
||||||
/** check the values if possible **/
|
/** check the values if possible **/
|
||||||
expect(notify.title).to.be.not.empty;
|
expect(notify.title).to.be.not.empty;
|
||||||
expect(notify.link).that.does.include('https://www.neubaukompass.de');
|
expect(notify.link).that.does.include('https://www.neubaukompass.de');
|
||||||
expect(notify.address).to.be.not.empty;
|
expect(notify.address).to.be.not.empty;
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import {buildHash} from '../../lib/utils.js';
|
import { buildHash } from '../../lib/utils.js';
|
||||||
|
|
||||||
describe('utilsCheck', () => {
|
describe('utilsCheck', () => {
|
||||||
describe('#utilsCheck()', () => {
|
describe('#utilsCheck()', () => {
|
||||||
it('should be null when null input', () => {
|
it('should be null when null input', () => {
|
||||||
expect(buildHash(null)).to.be.null;
|
expect(buildHash(null)).to.be.null;
|
||||||
});
|
});
|
||||||
it('should be null when null empty', () => {
|
it('should be null when null empty', () => {
|
||||||
expect(buildHash('')).to.be.null;
|
expect(buildHash('')).to.be.null;
|
||||||
});
|
});
|
||||||
it('should return a value', () => {
|
it('should return a value', () => {
|
||||||
expect(buildHash('bla', '', null)).to.be.a.string;
|
expect(buildHash('bla', '', null)).to.be.a.string;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
168
ui/src/App.jsx
168
ui/src/App.jsx
@@ -1,4 +1,4 @@
|
|||||||
import React, {useEffect} from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import InsufficientPermission from './components/permission/InsufficientPermission';
|
import InsufficientPermission from './components/permission/InsufficientPermission';
|
||||||
import PermissionAwareRoute from './components/permission/PermissionAwareRoute';
|
import PermissionAwareRoute from './components/permission/PermissionAwareRoute';
|
||||||
@@ -6,106 +6,108 @@ import GeneralSettings from './views/generalSettings/GeneralSettings';
|
|||||||
import JobMutation from './views/jobs/mutation/JobMutation';
|
import JobMutation from './views/jobs/mutation/JobMutation';
|
||||||
import UserMutator from './views/user/mutation/UserMutator';
|
import UserMutator from './views/user/mutation/UserMutator';
|
||||||
import JobInsight from './views/jobs/insights/JobInsight.jsx';
|
import JobInsight from './views/jobs/insights/JobInsight.jsx';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import {Switch, Redirect} from 'react-router-dom';
|
import { Switch, Redirect } from 'react-router-dom';
|
||||||
import Logout from './components/logout/Logout';
|
import Logout from './components/logout/Logout';
|
||||||
import Logo from './components/logo/Logo';
|
import Logo from './components/logo/Logo';
|
||||||
import Menu from './components/menu/Menu';
|
import Menu from './components/menu/Menu';
|
||||||
import Login from './views/login/Login';
|
import Login from './views/login/Login';
|
||||||
import Users from './views/user/Users';
|
import Users from './views/user/Users';
|
||||||
import Jobs from './views/jobs/Jobs';
|
import Jobs from './views/jobs/Jobs';
|
||||||
import {Route} from 'react-router';
|
import { Route } from 'react-router';
|
||||||
|
|
||||||
import './App.less';
|
import './App.less';
|
||||||
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
||||||
import {Banner} from '@douyinfe/semi-ui';
|
import { Banner } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
export default function FredyApp() {
|
export default function FredyApp() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const currentUser = useSelector((state) => state.user.currentUser);
|
const currentUser = useSelector((state) => state.user.currentUser);
|
||||||
const settings = useSelector((state) => state.generalSettings.settings);
|
const settings = useSelector((state) => state.generalSettings.settings);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
await dispatch.user.getCurrentUser();
|
await dispatch.user.getCurrentUser();
|
||||||
if (!needsLogin()) {
|
if (!needsLogin()) {
|
||||||
await dispatch.provider.getProvider();
|
await dispatch.provider.getProvider();
|
||||||
await dispatch.jobs.getJobs();
|
await dispatch.jobs.getJobs();
|
||||||
await dispatch.jobs.getProcessingTimes();
|
await dispatch.jobs.getProcessingTimes();
|
||||||
await dispatch.notificationAdapter.getAdapter();
|
await dispatch.notificationAdapter.getAdapter();
|
||||||
await dispatch.generalSettings.getGeneralSettings();
|
await dispatch.generalSettings.getGeneralSettings();
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, [currentUser?.userId]);
|
}, [currentUser?.userId]);
|
||||||
|
|
||||||
const needsLogin = () => {
|
const needsLogin = () => {
|
||||||
return currentUser == null || Object.keys(currentUser).length === 0;
|
return currentUser == null || Object.keys(currentUser).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAdmin = () => currentUser != null && currentUser.isAdmin;
|
const isAdmin = () => currentUser != null && currentUser.isAdmin;
|
||||||
|
|
||||||
const login = () => (
|
const login = () => (
|
||||||
|
<Switch>
|
||||||
|
<Route name="Login" path={'/login'} component={Login} />
|
||||||
|
<Redirect from="*" to={'/login'} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
|
||||||
|
return loading ? null : needsLogin() ? (
|
||||||
|
login()
|
||||||
|
) : (
|
||||||
|
<div className="app">
|
||||||
|
<div className="app__container">
|
||||||
|
<Logout />
|
||||||
|
<Logo width={190} white />
|
||||||
|
<Menu isAdmin={isAdmin()} />
|
||||||
|
|
||||||
|
{settings.demoMode && (
|
||||||
|
<>
|
||||||
|
<Banner
|
||||||
|
fullMode={true}
|
||||||
|
type="info"
|
||||||
|
bordered
|
||||||
|
closeIcon={null}
|
||||||
|
description="You're currently viewing the demo version of Fredy. Jobs won't scrape websites, and any changes you make will be reverted at midnight."
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{settings.analyticsEnabled === null && !settings.demoMode && <TrackingModal />}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route name="Login" path={'/login'} component={Login}/>
|
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} />
|
||||||
<Redirect from="*" to={'/login'}/>
|
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation} />
|
||||||
|
<Route name="Edit a Job" path={'/jobs/edit/:jobId'} component={JobMutation} />
|
||||||
|
<Route name="Insights into a Job" path={'/jobs/insights/:jobId'} component={JobInsight} />
|
||||||
|
<Route name="Job overview" path={'/jobs'} component={Jobs} />
|
||||||
|
<PermissionAwareRoute
|
||||||
|
name="Create new User"
|
||||||
|
path="/users/new"
|
||||||
|
component={<UserMutator />}
|
||||||
|
currentUser={currentUser}
|
||||||
|
/>
|
||||||
|
<PermissionAwareRoute
|
||||||
|
name="Edit a user"
|
||||||
|
path="/users/edit/:userId"
|
||||||
|
component={<UserMutator />}
|
||||||
|
currentUser={currentUser}
|
||||||
|
/>
|
||||||
|
<PermissionAwareRoute name="Users" path="/users" component={<Users />} currentUser={currentUser} />
|
||||||
|
<PermissionAwareRoute
|
||||||
|
name="General Settings"
|
||||||
|
path="/generalSettings"
|
||||||
|
component={<GeneralSettings />}
|
||||||
|
currentUser={currentUser}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Redirect from="/" to={'/jobs'} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
return loading ? null : needsLogin() ? (
|
);
|
||||||
login()
|
|
||||||
) : (
|
|
||||||
<div className="app">
|
|
||||||
<div className="app__container">
|
|
||||||
<Logout/>
|
|
||||||
<Logo width={190} white/>
|
|
||||||
<Menu isAdmin={isAdmin()}/>
|
|
||||||
|
|
||||||
{settings.demoMode && (
|
|
||||||
<>
|
|
||||||
<Banner fullMode={true}
|
|
||||||
type="info"
|
|
||||||
bordered
|
|
||||||
closeIcon={null}
|
|
||||||
description="You're currently viewing the demo version of Fredy. Jobs won't scrape websites, and any changes you make will be reverted at midnight."
|
|
||||||
/>
|
|
||||||
<br/>
|
|
||||||
</>)}
|
|
||||||
{(settings.analyticsEnabled === null && !settings.demoMode) && <TrackingModal/>}
|
|
||||||
<Switch>
|
|
||||||
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission}/>
|
|
||||||
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation}/>
|
|
||||||
<Route name="Edit a Job" path={'/jobs/edit/:jobId'} component={JobMutation}/>
|
|
||||||
<Route name="Insights into a Job" path={'/jobs/insights/:jobId'} component={JobInsight}/>
|
|
||||||
<Route name="Job overview" path={'/jobs'} component={Jobs}/>
|
|
||||||
<PermissionAwareRoute
|
|
||||||
name="Create new User"
|
|
||||||
path="/users/new"
|
|
||||||
component={<UserMutator/>}
|
|
||||||
currentUser={currentUser}
|
|
||||||
/>
|
|
||||||
<PermissionAwareRoute
|
|
||||||
name="Edit a user"
|
|
||||||
path="/users/edit/:userId"
|
|
||||||
component={<UserMutator/>}
|
|
||||||
currentUser={currentUser}
|
|
||||||
/>
|
|
||||||
<PermissionAwareRoute name="Users" path="/users" component={<Users/>} currentUser={currentUser}/>
|
|
||||||
<PermissionAwareRoute
|
|
||||||
name="General Settings"
|
|
||||||
path="/generalSettings"
|
|
||||||
component={<GeneralSettings/>}
|
|
||||||
currentUser={currentUser}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Redirect from="/" to={'/jobs'}/>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FredyApp.displayName = 'FredyApp';
|
FredyApp.displayName = 'FredyApp';
|
||||||
|
|||||||
@@ -41,3 +41,7 @@ a:active {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semi-icon:not(.semi-tabs-bar .semi-tabs-tab .semi-icon) {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ root.render(
|
|||||||
<App />
|
<App />
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,53 +1,56 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Modal} from '@douyinfe/semi-ui';
|
import { Modal } from '@douyinfe/semi-ui';
|
||||||
import Logo from '../logo/Logo.jsx';
|
import Logo from '../logo/Logo.jsx';
|
||||||
import {xhrPost} from '../../services/xhr.js';
|
import { xhrPost } from '../../services/xhr.js';
|
||||||
|
|
||||||
import './TrackingModal.less';
|
import './TrackingModal.less';
|
||||||
import inDevelopment from '../../services/developmentMode.js';
|
import inDevelopment from '../../services/developmentMode.js';
|
||||||
|
|
||||||
const saveResponse = async (analyticsEnabled) => {
|
const saveResponse = async (analyticsEnabled) => {
|
||||||
await xhrPost('/api/admin/generalSettings', {
|
await xhrPost('/api/admin/generalSettings', {
|
||||||
analyticsEnabled
|
analyticsEnabled,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TrackingModal() {
|
export default function TrackingModal() {
|
||||||
if(inDevelopment()){
|
if (inDevelopment()) {
|
||||||
console.log("FFFUUUCCCKKK")
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return <Modal
|
return (
|
||||||
visible={true}
|
<Modal
|
||||||
onOk={async () => {
|
visible={true}
|
||||||
await saveResponse(true);
|
onOk={async () => {
|
||||||
location.reload();
|
await saveResponse(true);
|
||||||
}}
|
location.reload();
|
||||||
onCancel={async () => {
|
}}
|
||||||
await saveResponse(false);
|
onCancel={async () => {
|
||||||
location.reload();
|
await saveResponse(false);
|
||||||
}}
|
location.reload();
|
||||||
maskClosable={false}
|
}}
|
||||||
closable={false}
|
maskClosable={false}
|
||||||
okText="Yes! I want to help"
|
closable={false}
|
||||||
cancelText="No, thanks"
|
okText="Yes! I want to help"
|
||||||
|
cancelText="No, thanks"
|
||||||
>
|
>
|
||||||
<Logo white/>
|
<Logo white />
|
||||||
<div className="trackingModal__description">
|
<div className="trackingModal__description">
|
||||||
<p>Hey 👋</p>
|
<p>Hey 👋</p>
|
||||||
<p>Fed up with popups? Yeah, me too. But this one’s important, and I promise it will only appear once ;)</p>
|
<p>Fed up with popups? Yeah, me too. But this one’s important, and I promise it will only appear once ;)</p>
|
||||||
<p>Fredy is completely free (and will always remain free). If you’d like, you can support me by donating
|
<p>
|
||||||
through my GitHub, but there’s absolutely no obligation to do so.</p>
|
Fredy is completely free (and will always remain free). If you’d like, you can support me by donating through
|
||||||
<p>However, it would be a huge
|
my GitHub, but there’s absolutely no obligation to do so.
|
||||||
help if you’d allow me to collect some analytical data. Wait, before you click "no", let me explain. If
|
</p>
|
||||||
you
|
<p>
|
||||||
agree, Fredy will send a ping to my Mixpanel project each time it runs.</p>
|
However, it would be a huge help if you’d allow me to collect some analytical data. Wait, before you click
|
||||||
<p>The data includes: names of
|
"no", let me explain. If you agree, Fredy will send a ping to my Mixpanel project each time it runs.
|
||||||
active adapters/providers, OS, architecture, Node version, and language. The information is entirely
|
</p>
|
||||||
anonymous and helps me understand which adapters/providers are most frequently used.</p>
|
<p>
|
||||||
<p>Thanks🤘</p>
|
The data includes: names of active adapters/providers, OS, architecture, Node version, and language. The
|
||||||
</div>
|
information is entirely anonymous and helps me understand which adapters/providers are most frequently used.
|
||||||
</Modal>;
|
</p>
|
||||||
|
<p>Thanks🤘</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,261 +1,251 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import {Divider, TimePicker, Button, Checkbox} from '@douyinfe/semi-ui';
|
import { Divider, TimePicker, Button, Checkbox } from '@douyinfe/semi-ui';
|
||||||
import {InputNumber} from '@douyinfe/semi-ui';
|
import { InputNumber } from '@douyinfe/semi-ui';
|
||||||
import Headline from '../../components/headline/Headline';
|
import Headline from '../../components/headline/Headline';
|
||||||
import {xhrPost} from '../../services/xhr';
|
import { xhrPost } from '../../services/xhr';
|
||||||
import {SegmentPart} from '../../components/segment/SegmentPart';
|
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||||
import {Banner, Toast} from '@douyinfe/semi-ui';
|
import { Banner, Toast } from '@douyinfe/semi-ui';
|
||||||
import {IconSave, IconCalendar, IconRefresh, IconSignal, IconLineChartStroked, IconSearch} from '@douyinfe/semi-icons';
|
import {
|
||||||
|
IconSave,
|
||||||
|
IconCalendar,
|
||||||
|
IconRefresh,
|
||||||
|
IconSignal,
|
||||||
|
IconLineChartStroked,
|
||||||
|
IconSearch,
|
||||||
|
} from '@douyinfe/semi-icons';
|
||||||
import './GeneralSettings.less';
|
import './GeneralSettings.less';
|
||||||
|
|
||||||
function formatFromTimestamp(ts) {
|
function formatFromTimestamp(ts) {
|
||||||
const date = new Date(ts);
|
const date = new Date(ts);
|
||||||
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
|
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFromTBackend(time) {
|
function formatFromTBackend(time) {
|
||||||
if (time == null || time.length === 0) {
|
if (time == null || time.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const split = time.split(':');
|
const split = time.split(':');
|
||||||
date.setHours(split[0]);
|
date.setHours(split[0]);
|
||||||
date.setMinutes(split[1]);
|
date.setMinutes(split[1]);
|
||||||
return date.getTime();
|
return date.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
const GeneralSettings = function GeneralSettings() {
|
const GeneralSettings = function GeneralSettings() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
|
||||||
const settings = useSelector((state) => state.generalSettings.settings);
|
const settings = useSelector((state) => state.generalSettings.settings);
|
||||||
|
|
||||||
const [interval, setInterval] = React.useState('');
|
const [interval, setInterval] = React.useState('');
|
||||||
const [port, setPort] = React.useState('');
|
const [port, setPort] = React.useState('');
|
||||||
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
|
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
|
||||||
const [workingHourTo, setWorkingHourTo] = React.useState(null);
|
const [workingHourTo, setWorkingHourTo] = React.useState(null);
|
||||||
const [demoMode, setDemoMode] = React.useState(null);
|
const [demoMode, setDemoMode] = React.useState(null);
|
||||||
const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null);
|
const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
await dispatch.generalSettings.getGeneralSettings();
|
await dispatch.generalSettings.getGeneralSettings();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
setInterval(settings?.interval);
|
setInterval(settings?.interval);
|
||||||
setPort(settings?.port);
|
setPort(settings?.port);
|
||||||
setWorkingHourFrom(settings?.workingHours?.from);
|
setWorkingHourFrom(settings?.workingHours?.from);
|
||||||
setWorkingHourTo(settings?.workingHours?.to);
|
setWorkingHourTo(settings?.workingHours?.to);
|
||||||
setAnalyticsEnabled(settings?.analyticsEnabled || false);
|
setAnalyticsEnabled(settings?.analyticsEnabled || false);
|
||||||
setDemoMode(settings?.demoMode || false);
|
setDemoMode(settings?.demoMode || false);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
|
|
||||||
const nullOrEmpty = (val) => val == null || val.length === 0;
|
const nullOrEmpty = (val) => val == null || val.length === 0;
|
||||||
|
|
||||||
const throwMessage = (message, type) => {
|
const throwMessage = (message, type) => {
|
||||||
if (type === 'error') {
|
if (type === 'error') {
|
||||||
Toast.error(message);
|
Toast.error(message);
|
||||||
} else {
|
} else {
|
||||||
Toast.success(message);
|
Toast.success(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStore = async () => {
|
const onStore = async () => {
|
||||||
if (nullOrEmpty(interval)) {
|
if (nullOrEmpty(interval)) {
|
||||||
throwMessage('Interval may not be empty.', 'error');
|
throwMessage('Interval may not be empty.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nullOrEmpty(port)) {
|
if (nullOrEmpty(port)) {
|
||||||
throwMessage('Port may not be empty.', 'error');
|
throwMessage('Port may not be empty.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) ||
|
(!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) ||
|
||||||
(nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo))
|
(nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo))
|
||||||
) {
|
) {
|
||||||
throwMessage('Working hours to and from must be set if either to or from has been set before.', 'error');
|
throwMessage('Working hours to and from must be set if either to or from has been set before.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await xhrPost('/api/admin/generalSettings', {
|
await xhrPost('/api/admin/generalSettings', {
|
||||||
interval,
|
interval,
|
||||||
port,
|
port,
|
||||||
workingHours: {
|
workingHours: {
|
||||||
from: workingHourFrom,
|
from: workingHourFrom,
|
||||||
to: workingHourTo,
|
to: workingHourTo,
|
||||||
},
|
},
|
||||||
demoMode,
|
demoMode,
|
||||||
analyticsEnabled
|
analyticsEnabled,
|
||||||
});
|
});
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
console.error(exception);
|
console.error(exception);
|
||||||
if(exception?.json?.message != null){
|
if (exception?.json?.message != null) {
|
||||||
throwMessage(exception.json.message, 'error');
|
throwMessage(exception.json.message, 'error');
|
||||||
}else {
|
} else {
|
||||||
throwMessage('Error while trying to store settings.', 'error');
|
throwMessage('Error while trying to store settings.', 'error');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success');
|
throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success');
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
location.reload();
|
location.reload();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Headline text="General Settings"/>
|
<Headline text="General Settings" />
|
||||||
<div>
|
<div>
|
||||||
<SegmentPart
|
<SegmentPart
|
||||||
name="Interval"
|
name="Interval"
|
||||||
helpText="Interval in minutes for running queries against the configured services."
|
helpText="Interval in minutes for running queries against the configured services."
|
||||||
Icon={IconRefresh}
|
Icon={IconRefresh}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={0}
|
min={0}
|
||||||
max={1440}
|
max={1440}
|
||||||
placeholder="Interval in minutes"
|
placeholder="Interval in minutes"
|
||||||
value={interval}
|
value={interval}
|
||||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||||
onChange={(value) => setInterval(value)}
|
onChange={(value) => setInterval(value)}
|
||||||
suffix={'minutes'}
|
suffix={'minutes'}
|
||||||
/>
|
/>
|
||||||
</SegmentPart>
|
</SegmentPart>
|
||||||
<Divider margin="1rem"/>
|
<Divider margin="1rem" />
|
||||||
<SegmentPart name="Port" helpText="Port on which Fredy is running." Icon={IconSignal}>
|
<SegmentPart name="Port" helpText="Port on which Fredy is running." Icon={IconSignal}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={0}
|
min={0}
|
||||||
max={99999}
|
max={99999}
|
||||||
placeholder="Port"
|
placeholder="Port"
|
||||||
value={port}
|
value={port}
|
||||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||||
onChange={(value) => setPort(value)}
|
onChange={(value) => setPort(value)}
|
||||||
/>
|
/>
|
||||||
</SegmentPart>
|
</SegmentPart>
|
||||||
<Divider margin="1rem"/>
|
<Divider margin="1rem" />
|
||||||
<SegmentPart
|
<SegmentPart
|
||||||
name="Working hours"
|
name="Working hours"
|
||||||
helpText="During this hours, Fredy will search for new apartments. If nothing is configured, Fredy will search around the clock."
|
helpText="During this hours, Fredy will search for new apartments. If nothing is configured, Fredy will search around the clock."
|
||||||
Icon={IconCalendar}
|
Icon={IconCalendar}
|
||||||
>
|
>
|
||||||
<div className="generalSettings__timePickerContainer">
|
<div className="generalSettings__timePickerContainer">
|
||||||
<TimePicker
|
<TimePicker
|
||||||
format={'HH:mm'}
|
format={'HH:mm'}
|
||||||
insetLabel="From"
|
insetLabel="From"
|
||||||
value={formatFromTBackend(workingHourFrom)}
|
value={formatFromTBackend(workingHourFrom)}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setWorkingHourFrom(val == null ? null : formatFromTimestamp(val));
|
setWorkingHourFrom(val == null ? null : formatFromTimestamp(val));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
format={'HH:mm'}
|
format={'HH:mm'}
|
||||||
insetLabel="Until"
|
insetLabel="Until"
|
||||||
value={formatFromTBackend(workingHourTo)}
|
value={formatFromTBackend(workingHourTo)}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setWorkingHourTo(val == null ? null : formatFromTimestamp(val));
|
setWorkingHourTo(val == null ? null : formatFromTimestamp(val));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SegmentPart>
|
</SegmentPart>
|
||||||
<Divider margin="1rem"/>
|
<Divider margin="1rem" />
|
||||||
|
|
||||||
<SegmentPart
|
<SegmentPart name="Analytics" helpText="Insights into the usage of Fredy." Icon={IconLineChartStroked}>
|
||||||
name="Analytics"
|
<Banner
|
||||||
helpText="Insights into the usage of Fredy."
|
fullMode={false}
|
||||||
Icon={IconLineChartStroked}
|
type="info"
|
||||||
>
|
closeIcon={null}
|
||||||
<Banner
|
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Explanation</div>}
|
||||||
fullMode={false}
|
style={{ marginBottom: '1rem' }}
|
||||||
type="info"
|
description={
|
||||||
closeIcon={null}
|
<div>
|
||||||
title={
|
Analytics are disabled by default. If you choose to enable them, we will begin tracking the
|
||||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
following:
|
||||||
Explanation
|
<br />
|
||||||
</div>
|
<ul>
|
||||||
}
|
<li>Name of active provider (e.g. Immoscout)</li>
|
||||||
style={{marginBottom: '1rem'}}
|
<li>Name of active adapter (e.g. Console)</li>
|
||||||
description={
|
<li>language</li>
|
||||||
<div>
|
<li>os</li>
|
||||||
Analytics are disabled by default. If you choose to enable them, we will begin tracking the following:<br/>
|
<li>node version</li>
|
||||||
<ul>
|
<li>arch</li>
|
||||||
<li>Name of active provider (e.g. Immoscout)</li>
|
</ul>
|
||||||
<li>Name of active adapter (e.g. Console)</li>
|
The data is sent anonymously and helps me understand which providers or adapters are being used the
|
||||||
<li>language</li>
|
most. In the end it helps me to improve fredy.
|
||||||
<li>os</li>
|
</div>
|
||||||
<li>node version</li>
|
}
|
||||||
<li>arch</li>
|
/>
|
||||||
</ul>
|
|
||||||
The data is sent anonymously and helps me understand which providers or adapters are being used the most. In the end it helps me to improve fredy.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox checked={analyticsEnabled} onChange={(e) => setAnalyticsEnabled(e.target.checked)}>
|
||||||
checked={analyticsEnabled}
|
{' '}
|
||||||
onChange={(e) => setAnalyticsEnabled(e.target.checked)}
|
Enabled
|
||||||
> Enabled
|
</Checkbox>
|
||||||
</Checkbox>
|
</SegmentPart>
|
||||||
|
|
||||||
</SegmentPart>
|
<Divider margin="1rem" />
|
||||||
|
|
||||||
<Divider margin="1rem"/>
|
<SegmentPart name="Demo Mode" helpText="If enabled, Fredy runs in demo mode." Icon={IconSearch}>
|
||||||
|
<Banner
|
||||||
|
fullMode={false}
|
||||||
|
type="info"
|
||||||
|
closeIcon={null}
|
||||||
|
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Explanation</div>}
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also
|
||||||
|
all database files will be set back to the default values at midnight.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<SegmentPart
|
<Checkbox checked={demoMode} onChange={(e) => setDemoMode(e.target.checked)}>
|
||||||
name="Demo Mode"
|
{' '}
|
||||||
helpText="If enabled, Fredy runs in demo mode."
|
Enabled
|
||||||
Icon={IconSearch}
|
</Checkbox>
|
||||||
>
|
</SegmentPart>
|
||||||
<Banner
|
|
||||||
fullMode={false}
|
|
||||||
type="info"
|
|
||||||
closeIcon={null}
|
|
||||||
title={
|
|
||||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
|
||||||
Explanation
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
style={{marginBottom: '1rem'}}
|
|
||||||
description={
|
|
||||||
<div>
|
|
||||||
In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also
|
|
||||||
all database files will be set back to the default values at midnight.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
<Divider margin="1rem" />
|
||||||
checked={demoMode}
|
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave />}>
|
||||||
onChange={(e) => setDemoMode(e.target.checked)}
|
Save
|
||||||
> Enabled
|
</Button>
|
||||||
</Checkbox>
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
</SegmentPart>
|
)}
|
||||||
|
</div>
|
||||||
<Divider margin="1rem"/>
|
);
|
||||||
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave/>}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GeneralSettings;
|
export default GeneralSettings;
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {format} from '../../services/time/timeService';
|
import { format } from '../../services/time/timeService';
|
||||||
import {Banner, Descriptions} from '@douyinfe/semi-ui';
|
import { Descriptions } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
export default function ProcessingTimes({processingTimes = {}}) {
|
export default function ProcessingTimes({ processingTimes = {} }) {
|
||||||
if (Object.keys(processingTimes).length === 0) {
|
if (Object.keys(processingTimes).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
row
|
row
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#35363c',
|
backgroundColor: '#35363c',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
||||||
{processingTimes.lastRun && (
|
{processingTimes.lastRun && (
|
||||||
<>
|
<>
|
||||||
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
||||||
<Descriptions.Item itemKey="Next run">
|
<Descriptions.Item itemKey="Next run">
|
||||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const validate = (selectedAdapter) => {
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
results.push('A number field cannot contain anything else and must be > 0.');
|
results.push('A number field cannot contain anything else and must be > 0.');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export default function NotificationAdapterMutator({
|
|||||||
id: selectedAdapter.id,
|
id: selectedAdapter.id,
|
||||||
name: selectedAdapter.name,
|
name: selectedAdapter.name,
|
||||||
fields: selectedAdapter.fields || {},
|
fields: selectedAdapter.fields || {},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedAdapter(null);
|
setSelectedAdapter(null);
|
||||||
@@ -114,7 +114,7 @@ export default function NotificationAdapterMutator({
|
|||||||
setSuccessMessage('It seems like it worked! Please check your service.');
|
setSuccessMessage('It seems like it worked! Please check your service.');
|
||||||
})
|
})
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
setValidationMessage(`This did not work :-( I've received the following error: ${error.json.message}`)
|
setValidationMessage(`This did not work :-( I've received the following error: ${error.json.message}`),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ export default function NotificationAdapterMutator({
|
|||||||
.filter((option) =>
|
.filter((option) =>
|
||||||
editNotificationAdapter != null
|
editNotificationAdapter != null
|
||||||
? true
|
? true
|
||||||
: selected.find((selectedOption) => selectedOption.id === option.key) == null
|
: selected.find((selectedOption) => selectedOption.id === option.key) == null,
|
||||||
)
|
)
|
||||||
.sort(sortAdapter)}
|
.sort(sortAdapter)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function ProviderMutator({ onVisibilityChanged, visible = false,
|
|||||||
url: providerUrl,
|
url: providerUrl,
|
||||||
id: selectedProvider.id,
|
id: selectedProvider.id,
|
||||||
name: selectedProvider.name,
|
name: selectedProvider.name,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
setProviderUrl(null);
|
setProviderUrl(null);
|
||||||
setSelectedProvider(null);
|
setSelectedProvider(null);
|
||||||
@@ -101,7 +101,7 @@ export default function ProviderMutator({ onVisibilityChanged, visible = false,
|
|||||||
description={
|
description={
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Currently, our Immoscout implementation does not drawing shapes on a map. Use a radius instead.
|
Currently, our Immoscout implementation does not support drawing shapes on a map. Use a radius instead.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,105 @@
|
|||||||
import React, {useEffect} from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import cityBackground from '../../assets/city_background.jpg';
|
import cityBackground from '../../assets/city_background.jpg';
|
||||||
import Logo from '../../components/logo/Logo';
|
import Logo from '../../components/logo/Logo';
|
||||||
import {xhrPost} from '../../services/xhr';
|
import { xhrPost } from '../../services/xhr';
|
||||||
import {useHistory} from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import {Input, Button, Banner} from '@douyinfe/semi-ui';
|
import { Input, Button, Banner } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
import './login.less';
|
import './login.less';
|
||||||
import {IconUser, IconLock} from '@douyinfe/semi-icons';
|
import { IconUser, IconLock } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [username, setUserName] = React.useState('');
|
const [username, setUserName] = React.useState('');
|
||||||
const [password, setPassword] = React.useState('');
|
const [password, setPassword] = React.useState('');
|
||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
const demoMode = useSelector((state) => state.demoMode.demoMode || false);
|
const demoMode = useSelector((state) => state.demoMode.demoMode || false);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function init() {
|
async function init() {
|
||||||
await dispatch.demoMode.getDemoMode();
|
await dispatch.demoMode.getDemoMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const tryLogin = async () => {
|
const tryLogin = async () => {
|
||||||
if (username.length === 0 || password.length === 0) {
|
if (username.length === 0 || password.length === 0) {
|
||||||
setError('Username and password are mandatory.');
|
setError('Username and password are mandatory.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await xhrPost('/api/login', {
|
await xhrPost('/api/login', {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
setError('Login not successful...');
|
setError('Login not successful...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await dispatch.user.getCurrentUser();
|
await dispatch.user.getCurrentUser();
|
||||||
history.push('/jobs');
|
history.push('/jobs');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login">
|
<div className="login">
|
||||||
<div className="login__bgImage" style={{background: `url("${cityBackground}")`}}/>
|
<div className="login__bgImage" style={{ background: `url("${cityBackground}")` }} />
|
||||||
<Logo/>
|
<Logo />
|
||||||
<form>
|
<form>
|
||||||
<div className="login__loginWrapper">
|
<div className="login__loginWrapper">
|
||||||
{error && <Banner type="danger" closeIcon={null} description={error}/>}
|
{error && <Banner type="danger" closeIcon={null} description={error} />}
|
||||||
<Input
|
<Input
|
||||||
size="large"
|
size="large"
|
||||||
prefix={<IconUser/>}
|
prefix={<IconUser />}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={username}
|
value={username}
|
||||||
showClear
|
showClear
|
||||||
style={{marginTop: error ? '1rem' : '4rem'}}
|
style={{ marginTop: error ? '1rem' : '4rem' }}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChange={(value) => setUserName(value)}
|
onChange={(value) => setUserName(value)}
|
||||||
onKeyPress={async (e) => {
|
onKeyPress={async (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
await tryLogin();
|
await tryLogin();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
size="large"
|
size="large"
|
||||||
mode="password"
|
mode="password"
|
||||||
prefix={<IconLock/>}
|
prefix={<IconLock />}
|
||||||
value={password}
|
value={password}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
style={{marginTop: '2rem'}}
|
style={{ marginTop: '2rem' }}
|
||||||
onChange={(value) => setPassword(value)}
|
onChange={(value) => setPassword(value)}
|
||||||
onKeyPress={async (e) => {
|
onKeyPress={async (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
await tryLogin();
|
await tryLogin();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="primary" onClick={tryLogin} theme="solid" style={{marginTop: '3rem'}}>
|
<Button type="primary" onClick={tryLogin} theme="solid" style={{ marginTop: '3rem' }}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
<br/>
|
<br />
|
||||||
{demoMode && <Banner fullMode={true}
|
{demoMode && (
|
||||||
type="info"
|
<Banner
|
||||||
bordered
|
fullMode={true}
|
||||||
closeIcon={null}
|
type="info"
|
||||||
description="This is the demo version of Fredy. Use 'demo' as both the username and password to log in."
|
bordered
|
||||||
/>}
|
closeIcon={null}
|
||||||
</div>
|
description="This is the demo version of Fredy. Use 'demo' as both the username and password to log in."
|
||||||
</form>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Login.displayName = 'Login';
|
Login.displayName = 'Login';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 9999999,
|
chunkSizeWarningLimit: 9999999,
|
||||||
outDir: './ui/public',
|
outDir: './ui/public',
|
||||||
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
99
yarn.lock
99
yarn.lock
@@ -2242,7 +2242,7 @@ cheerio@^1.1.0:
|
|||||||
undici "^7.10.0"
|
undici "^7.10.0"
|
||||||
whatwg-mimetype "^4.0.0"
|
whatwg-mimetype "^4.0.0"
|
||||||
|
|
||||||
chokidar@^3.5.3:
|
chokidar@^3.5.2, chokidar@^3.5.3:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||||
@@ -2511,7 +2511,7 @@ debug@3.2.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
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.0, debug@^4.4.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.0, debug@^4.4.1:
|
||||||
version "4.4.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
|
||||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||||
@@ -2675,6 +2675,11 @@ domutils@^3.0.1, domutils@^3.2.1, domutils@^3.2.2:
|
|||||||
domelementtype "^2.3.0"
|
domelementtype "^2.3.0"
|
||||||
domhandler "^5.0.3"
|
domhandler "^5.0.3"
|
||||||
|
|
||||||
|
dotenv@^16.4.5:
|
||||||
|
version "16.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020"
|
||||||
|
integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==
|
||||||
|
|
||||||
dunder-proto@^1.0.0, dunder-proto@^1.0.1:
|
dunder-proto@^1.0.0, dunder-proto@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||||
@@ -3512,7 +3517,7 @@ glob-parent@~5.1.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob "^4.0.1"
|
is-glob "^4.0.1"
|
||||||
|
|
||||||
glob@^7.1.3:
|
glob@^7.0.0, glob@^7.1.3:
|
||||||
version "7.2.3"
|
version "7.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||||
@@ -3587,6 +3592,11 @@ has-bigints@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
|
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe"
|
||||||
integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==
|
integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==
|
||||||
|
|
||||||
|
has-flag@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||||
|
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
|
||||||
|
|
||||||
has-flag@^4.0.0:
|
has-flag@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
@@ -3783,6 +3793,11 @@ ieee754@^1.1.13:
|
|||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||||
|
|
||||||
|
ignore-by-default@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||||
|
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
||||||
|
|
||||||
ignore@^5.2.0:
|
ignore@^5.2.0:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||||
@@ -3838,6 +3853,11 @@ internal-slot@^1.1.0:
|
|||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
side-channel "^1.1.0"
|
side-channel "^1.1.0"
|
||||||
|
|
||||||
|
interpret@^1.0.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
|
||||||
|
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
|
||||||
|
|
||||||
ip-address@^9.0.5:
|
ip-address@^9.0.5:
|
||||||
version "9.0.5"
|
version "9.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
|
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
|
||||||
@@ -5245,6 +5265,22 @@ node-releases@^2.0.19:
|
|||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||||
|
|
||||||
|
nodemon@^3.1.10:
|
||||||
|
version "3.1.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1"
|
||||||
|
integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==
|
||||||
|
dependencies:
|
||||||
|
chokidar "^3.5.2"
|
||||||
|
debug "^4"
|
||||||
|
ignore-by-default "^1.0.1"
|
||||||
|
minimatch "^3.1.2"
|
||||||
|
pstree.remy "^1.1.8"
|
||||||
|
semver "^7.5.3"
|
||||||
|
simple-update-notifier "^2.0.0"
|
||||||
|
supports-color "^5.5.0"
|
||||||
|
touch "^3.1.0"
|
||||||
|
undefsafe "^2.0.5"
|
||||||
|
|
||||||
nopt@~2.1.1:
|
nopt@~2.1.1:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af"
|
||||||
@@ -5644,6 +5680,11 @@ prr@~1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
|
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
|
||||||
integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
|
integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
|
||||||
|
|
||||||
|
pstree.remy@^1.1.8:
|
||||||
|
version "1.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
||||||
|
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
|
||||||
|
|
||||||
pump@^3.0.0:
|
pump@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
|
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
|
||||||
@@ -5883,6 +5924,13 @@ readdirp@~3.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
|
rechoir@^0.6.2:
|
||||||
|
version "0.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
||||||
|
integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==
|
||||||
|
dependencies:
|
||||||
|
resolve "^1.1.6"
|
||||||
|
|
||||||
recma-build-jsx@^1.0.0:
|
recma-build-jsx@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz#c02f29e047e103d2fab2054954e1761b8ea253c4"
|
resolved "https://registry.yarnpkg.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz#c02f29e047e103d2fab2054954e1761b8ea253c4"
|
||||||
@@ -6081,7 +6129,7 @@ resolve-pathname@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
||||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||||
|
|
||||||
resolve@^1.22.10:
|
resolve@^1.1.6, resolve@^1.22.10:
|
||||||
version "1.22.10"
|
version "1.22.10"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
|
||||||
integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
|
integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
|
||||||
@@ -6234,7 +6282,7 @@ semver@^6.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
semver@^7.3.5, semver@^7.7.2:
|
semver@^7.3.5, semver@^7.5.3, semver@^7.7.2:
|
||||||
version "7.7.2"
|
version "7.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||||
@@ -6331,6 +6379,15 @@ shebang-regex@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||||
|
|
||||||
|
shelljs@^0.8.5:
|
||||||
|
version "0.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
|
||||||
|
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.0.0"
|
||||||
|
interpret "^1.0.0"
|
||||||
|
rechoir "^0.6.2"
|
||||||
|
|
||||||
side-channel-list@^1.0.0:
|
side-channel-list@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||||
@@ -6390,6 +6447,13 @@ simple-get@^4.0.0:
|
|||||||
once "^1.3.1"
|
once "^1.3.1"
|
||||||
simple-concat "^1.0.0"
|
simple-concat "^1.0.0"
|
||||||
|
|
||||||
|
simple-update-notifier@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb"
|
||||||
|
integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==
|
||||||
|
dependencies:
|
||||||
|
semver "^7.5.3"
|
||||||
|
|
||||||
slack@11.0.2:
|
slack@11.0.2:
|
||||||
version "11.0.2"
|
version "11.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/slack/-/slack-11.0.2.tgz#30f68527c5d1712b7faa3141db7716f89ac6e911"
|
resolved "https://registry.yarnpkg.com/slack/-/slack-11.0.2.tgz#30f68527c5d1712b7faa3141db7716f89ac6e911"
|
||||||
@@ -6630,6 +6694,13 @@ style-to-object@1.0.8:
|
|||||||
dependencies:
|
dependencies:
|
||||||
inline-style-parser "0.2.4"
|
inline-style-parser "0.2.4"
|
||||||
|
|
||||||
|
supports-color@^5.5.0:
|
||||||
|
version "5.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||||
|
dependencies:
|
||||||
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
supports-color@^7.1.0:
|
supports-color@^7.1.0:
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||||
@@ -6737,6 +6808,11 @@ toidentifier@1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
|
|
||||||
|
touch@^3.1.0:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
|
||||||
|
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
|
||||||
|
|
||||||
trim-lines@^3.0.0:
|
trim-lines@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
||||||
@@ -6857,6 +6933,11 @@ unbox-primitive@^1.1.0:
|
|||||||
has-symbols "^1.1.0"
|
has-symbols "^1.1.0"
|
||||||
which-boxed-primitive "^1.1.1"
|
which-boxed-primitive "^1.1.1"
|
||||||
|
|
||||||
|
undefsafe@^2.0.5:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
|
||||||
|
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
|
||||||
|
|
||||||
undici-types@~6.21.0:
|
undici-types@~6.21.0:
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
||||||
@@ -7148,6 +7229,14 @@ ws@^8.18.3:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
|
||||||
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
||||||
|
|
||||||
|
x-var@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/x-var/-/x-var-2.1.0.tgz#9143461ad050b83a8043987ebb263606a1e8274f"
|
||||||
|
integrity sha512-EResegCrATlvIVNwrSt5wb4ip6XzUkjGp9cfr8nNcmfZB8Swg1NiesfcHBdvCs4Ed45cbWADeHcio0ZebJFYuQ==
|
||||||
|
dependencies:
|
||||||
|
dotenv "^16.4.5"
|
||||||
|
shelljs "^0.8.5"
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
|
|||||||
Reference in New Issue
Block a user