Compare commits

...

5 Commits

Author SHA1 Message Date
Christian Kellner
b3ae5f640c Update README.md 2024-11-20 22:23:05 +01:00
Christian Kellner
8f91267b5d sending tracking information (#116)
* Ability to send tracking information
2024-11-20 22:22:16 +01:00
Christian Kellner
3d59c0096d reverting config changes. accidentally pushed 2024-11-20 08:19:16 +01:00
Christian Kellner
dab6e4edf3 upgrading husky 2024-11-19 13:45:07 +01:00
Christian Kellner
e1c45f18e0 adding action for stale pr's 2024-11-06 16:16:16 +01:00
19 changed files with 846 additions and 728 deletions

21
.github/workflows/stales.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: "Close stale issues and PRs"
on:
schedule:
- cron: '0 0 * * *' # Daily
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
with:
days-before-stale: 30
days-before-close: 7
stale-issue-message: "This issue has been automatically marked as stale due to inactivity."
stale-pr-message: "This PR has been automatically marked as stale due to inactivity."
close-issue-message: "Closing this issue due to prolonged inactivity."
close-pr-message: "Closing this PR due to prolonged inactivity."
exempt-issue-labels: "keep-open"
exempt-pr-labels: "keep-open"
only: "pulls"

1
.husky/pre-commit Executable file
View File

@@ -0,0 +1 @@
npx lint-staged

View File

@@ -1,2 +0,0 @@
sudo: false
language: node_js

View File

@@ -86,14 +86,12 @@ The rest will be handled by _Fredy_. Keep in mind, the support is experimental.
If you need more than the 1000 API calls allowed per month, I'd suggest opting for a paid account... ScrapingAnt loves OpenSource, therefore they have decided to give all _Fredy_ users a 10% discount by using the code **FREDY10** (Disclaimer: I do not earn any money for recommending their service). If you need more than the 1000 API calls allowed per month, I'd suggest opting for a paid account... ScrapingAnt loves OpenSource, therefore they have decided to give all _Fredy_ users a 10% discount by using the code **FREDY10** (Disclaimer: I do not earn any money for recommending their service).
### 👐 Contributing # Analytics
Thanks to all the people who already contributed! Fredy is completely free (and will always remain free). However, it would be a huge help if youd allow me to collect some analytical data.
Before you freak out, let me explain...
<a href="https://github.com/orangecoding/fredy/graphs/contributors"> If you agree, Fredy will send a ping to my Mixpanel project each time it runs.
<img src="https://contrib.rocks/image?repo=orangecoding/fredy" /> The data includes: names of active adapters/providers, OS, architecture, Node version, and language. The information is entirely anonymous and helps me understand which adapters/providers are most frequently used.</p>
</a> **Thanks**🤘
See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md)
# Docker # Docker
Use the Dockerfile in this repository to build an image. Use the Dockerfile in this repository to build an image.
@@ -114,6 +112,15 @@ Put your config.json into a path of your choice, such as `/path/to/your/conf/`.
Example: `docker create --name fredy -v /path/to/your/conf/:/conf -p 9998:9998 fredy/fredy` Example: `docker create --name fredy -v /path/to/your/conf/:/conf -p 9998:9998 fredy/fredy`
### 👐 Contributing
Thanks to all the people who already contributed!
<a href="https://github.com/orangecoding/fredy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=orangecoding/fredy" />
</a>
See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md)
## Logs ## Logs
You can browse the logs with `docker logs fredy -f`. You can browse the logs with `docker logs fredy -f`.

View File

@@ -1 +1 @@
{"interval":"60","port":9998,"scrapingAnt":{"apiKey":"","proxy":"datacenter"},"workingHours":{"from":"","to":""}} {"interval":"60","port":9998,"scrapingAnt":{"apiKey":"","proxy":"datacenter"},"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null}

View File

@@ -6,6 +6,7 @@ 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';
//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');
@@ -25,6 +26,7 @@ setInterval(
(function exec() { (function exec() {
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now()); const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
if (isDuringWorkingHoursOrNotSet) { if (isDuringWorkingHoursOrNotSet) {
track();
config.lastRun = Date.now(); config.lastRun = Date.now();
jobStorage jobStorage
.getJobs() .getJobs()

View File

@@ -1,5 +1,5 @@
import restana from 'restana'; import restana from 'restana';
import { config, getDirName } from '../../utils.js'; import {config, getDirName, readConfigFromStorage, refreshConfig} from '../../utils.js';
import fs from 'fs'; import fs from 'fs';
const service = restana(); const service = restana();
const generalSettingsRouter = service.newRouter(); const generalSettingsRouter = service.newRouter();
@@ -10,7 +10,9 @@ generalSettingsRouter.get('/', async (req, res) => {
generalSettingsRouter.post('/', async (req, res) => { generalSettingsRouter.post('/', async (req, res) => {
const settings = req.body; const settings = req.body;
try { try {
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify(settings)); const currentConfig = await readConfigFromStorage();
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({...currentConfig, ...settings}));
await refreshConfig();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.send(new Error('Error while trying to write settings.')); res.send(new Error('Error while trying to write settings.'));

View File

@@ -5,6 +5,7 @@ import * as userStorage from '../../services/storage/userStorage.js';
import * as immoscoutProvider from '../../provider/immoscout.js'; import * as immoscoutProvider from '../../provider/immoscout.js';
import { config } from '../../utils.js'; import { config } from '../../utils.js';
import { isAdmin } from '../security.js'; import { isAdmin } from '../security.js';
import {isScrapingAntApiKeySet} from '../../services/scrapingAnt.js';
const service = restana(); const service = restana();
const jobRouter = service.newRouter(); const jobRouter = service.newRouter();
function doesJobBelongsToUser(job, req) { function doesJobBelongsToUser(job, req) {
@@ -25,8 +26,8 @@ jobRouter.get('/', async (req, res) => {
res.send(); res.send();
}); });
jobRouter.get('/processingTimes', async (req, res) => { jobRouter.get('/processingTimes', async (req, res) => {
let scrapingAntData = null; let scrapingAntData = {};
if (config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 0) { if (isScrapingAntApiKeySet()) {
try { try {
const response = await fetch(`https://api.scrapingant.com/v2/usage?x-api-key=${config.scrapingAnt.apiKey}`); const response = await fetch(`https://api.scrapingant.com/v2/usage?x-api-key=${config.scrapingAnt.apiKey}`);
scrapingAntData = await response.json(); scrapingAntData = await response.json();
@@ -38,6 +39,7 @@ jobRouter.get('/processingTimes', async (req, res) => {
interval: config.interval, interval: config.interval,
lastRun: config.lastRun || null, lastRun: config.lastRun || null,
scrapingAntData, scrapingAntData,
error: scrapingAntData?.detail == null ? null : scrapingAntData?.detail
}; };
res.send(); res.send();
}); });

8
lib/defaultConfig.js Normal file
View File

@@ -0,0 +1,8 @@
export const DEFAULT_CONFIG = {
'interval': '60',
'port': 9998,
'scrapingAnt': {'apiKey': '', 'proxy': 'datacenter'},
'workingHours': {'from': '', 'to': ''},
'demoMode': false,
'analyticsEnabled': null
};

View File

@@ -22,7 +22,7 @@ export const transformUrlForScrapingAnt = (url, id) => {
return url; return url;
}; };
export const isScrapingAntApiKeySet = () => { export const isScrapingAntApiKeySet = () => {
return config.scrapingAnt != null && config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 0; return config.scrapingAnt != null && config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 8;
}; };
export const makeUrlResidential = (url) => { export const makeUrlResidential = (url) => {
return url.replace('datacenter', 'residential'); return url.replace('datacenter', 'residential');

View File

@@ -0,0 +1,42 @@
import Mixpanel from 'mixpanel';
import {getJobs} from '../storage/jobStorage.js';
import {config} from '../../utils.js';
export const track = function () {
//only send tracking information if the user allowed to do so.
if (config.analyticsEnabled) {
const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e');
const activeProvider = new Set();
const activeAdapter = new Set();
const platform = process.platform;
const arch = process.arch;
const language = process.env.LANG || 'en';
const nodeVersion = process.version || 'N/A';
const jobs = getJobs();
if (jobs != null && jobs.length > 0) {
jobs.forEach(job => {
job.provider.forEach(provider => {
activeProvider.add(provider.id);
});
job.notificationAdapter.forEach(adapter => {
activeAdapter.add(adapter.id);
});
});
mixpanelTracker.track('fredy_tracking', {
adapter: Array.from(activeAdapter),
provider: Array.from(activeProvider),
isDemo: config.demoMode,
platform,
arch,
nodeVersion,
language
});
}
}
};

View File

@@ -2,6 +2,7 @@ import {dirname} from 'node:path';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
import {readFile} from 'fs/promises'; import {readFile} from 'fs/promises';
import {createHash} from 'crypto'; import {createHash} from 'crypto';
import {DEFAULT_CONFIG} from './defaultConfig.js';
function isOneOf(word, arr) { function isOneOf(word, arr) {
if (arr == null || arr.length === 0) { if (arr == null || arr.length === 0) {
@@ -52,7 +53,23 @@ function buildHash(...inputs) {
.digest('hex'); .digest('hex');
} }
const config = JSON.parse(await readFile(new URL('../conf/config.json', import.meta.url))); let config = {};
export async function readConfigFromStorage(){
return JSON.parse(await readFile(new URL('../conf/config.json', import.meta.url)));
}
export async function refreshConfig(){
try {
config = await readConfigFromStorage();
//backwards compatability...
config.analyticsEnabled ??= null;
config.demoMode ??= false;
} catch (error) {
config = {...DEFAULT_CONFIG};
console.error('Error reading config file', error);
}
}
await refreshConfig();
export {isOneOf}; export {isOneOf};
export {nullOrEmpty}; export {nullOrEmpty};

View File

@@ -1,6 +1,6 @@
{ {
"name": "fredy", "name": "fredy",
"version": "10.2.0", "version": "10.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 index.js", "start": "node index.js",
@@ -11,11 +11,6 @@
"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"
}, },
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"type": "module", "type": "module",
"lint-staged": { "lint-staged": {
"*.js": [ "*.js": [
@@ -55,7 +50,7 @@
"Firefox ESR" "Firefox ESR"
], ],
"dependencies": { "dependencies": {
"@douyinfe/semi-ui": "2.68.3", "@douyinfe/semi-ui": "2.69.2",
"@rematch/core": "2.2.0", "@rematch/core": "2.2.0",
"@rematch/loading": "2.1.2", "@rematch/loading": "2.1.2",
"@sendgrid/mail": "8.1.4", "@sendgrid/mail": "8.1.4",
@@ -69,6 +64,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"lowdb": "6.0.1", "lowdb": "6.0.1",
"markdown": "^0.5.0", "markdown": "^0.5.0",
"mixpanel": "^0.18.0",
"nanoid": "5.0.8", "nanoid": "5.0.8",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"node-mailjet": "6.0.6", "node-mailjet": "6.0.6",
@@ -84,7 +80,7 @@
"serve-static": "1.16.2", "serve-static": "1.16.2",
"slack": "11.0.2", "slack": "11.0.2",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
"vite": "5.4.10", "vite": "5.4.11",
"x-ray": "2.3.4" "x-ray": "2.3.4"
}, },
"devDependencies": { "devDependencies": {
@@ -98,9 +94,9 @@
"eslint-plugin-react": "7.37.2", "eslint-plugin-react": "7.37.2",
"esmock": "2.6.9", "esmock": "2.6.9",
"history": "5.3.0", "history": "5.3.0",
"husky": "4.3.8", "husky": "9.1.7",
"less": "4.2.0", "less": "4.2.0",
"lint-staged": "13.2.2", "lint-staged": "15.2.10",
"mocha": "10.8.2", "mocha": "10.8.2",
"prettier": "3.3.3", "prettier": "3.3.3",
"redux-logger": "3.0.6" "redux-logger": "3.0.6"

View File

@@ -17,11 +17,13 @@ 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';
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);
useEffect(() => { useEffect(() => {
async function init() { async function init() {
@@ -31,6 +33,7 @@ export default function FredyApp() {
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();
} }
setLoading(false); setLoading(false);
} }
@@ -59,6 +62,7 @@ export default function FredyApp() {
<Logout /> <Logout />
<Logo width={190} white /> <Logo width={190} white />
<Menu isAdmin={isAdmin()} /> <Menu isAdmin={isAdmin()} />
{settings.analyticsEnabled === null && <TrackingModal/>}
<Switch> <Switch>
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} /> <Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} />
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation} /> <Route name="Create new Job" path={'/jobs/new'} component={JobMutation} />

View File

@@ -0,0 +1,48 @@
import React from 'react';
import {Modal} from '@douyinfe/semi-ui';
import Logo from '../logo/Logo.jsx';
import {xhrPost} from '../../services/xhr.js';
import './TrackingModal.less';
const saveResponse = async (analyticsEnabled) => {
await xhrPost('/api/admin/generalSettings', {
analyticsEnabled
});
};
export default function TrackingModal() {
return <Modal
visible={true}
onOk={async () => {
await saveResponse(true);
location.reload();
}}
onCancel={async () => {
await saveResponse(false);
location.reload();
}}
maskClosable={false}
closable={false}
okText="Yes! I want to help"
cancelText="No, thanks"
>
<Logo white/>
<div className="trackingModal__description">
<p>Hey 👋</p>
<p>Fed up with popups? Yeah, me too. But this ones important, and I promise it will only appear once ;)</p>
<p>Fredy is completely free (and will always remain free). If youd like, you can support me by donating
through my GitHub, but theres absolutely no obligation to do so.</p>
<p>However, it would be a huge
help if youd allow me to collect some analytical data. Wait, before you click "no", let me explain. If
you
agree, Fredy will send a ping to my Mixpanel project each time it runs.</p>
<p>The data includes: names of
active adapters/providers, OS, architecture, Node version, and language. The information is entirely
anonymous and helps me understand which adapters/providers are most frequently used.</p>
<p>Thanks🤘</p>
</div>
</Modal>;
}

View File

@@ -0,0 +1,5 @@
.trackingModal {
&__description {
margin-top:10rem;
}
}

View File

@@ -2,13 +2,13 @@ import React from 'react';
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import { Divider, Input, Radio, TimePicker, Button, RadioGroup } from '@douyinfe/semi-ui'; import {Divider, Input, Radio, TimePicker, Button, RadioGroup, 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, IconKey, IconRefresh, IconSignal } from '@douyinfe/semi-icons'; import {IconSave, IconCalendar, IconKey, IconRefresh, IconSignal, IconLineChartStroked, IconSearch} from '@douyinfe/semi-icons';
import './GeneralSettings.less'; import './GeneralSettings.less';
function formatFromTimestamp(ts) { function formatFromTimestamp(ts) {
@@ -39,6 +39,9 @@ const GeneralSettings = function GeneralSettings() {
const [scrapingAntProxy, setScrapingAntProxy] = React.useState(''); const [scrapingAntProxy, setScrapingAntProxy] = 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 [analyticsEnabled, setAnalyticsEnabled] = React.useState(null);
React.useEffect(() => { React.useEffect(() => {
async function init() { async function init() {
await dispatch.generalSettings.getGeneralSettings(); await dispatch.generalSettings.getGeneralSettings();
@@ -56,6 +59,8 @@ const GeneralSettings = function GeneralSettings() {
setWorkingHourFrom(settings?.workingHours?.from); setWorkingHourFrom(settings?.workingHours?.from);
setWorkingHourTo(settings?.workingHours?.to); setWorkingHourTo(settings?.workingHours?.to);
setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter'); setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter');
setAnalyticsEnabled(settings?.analytics || false);
setDemoMode(settings?.demoMode || false);
} }
init(); init();
@@ -99,13 +104,15 @@ const GeneralSettings = function GeneralSettings() {
from: workingHourFrom, from: workingHourFrom,
to: workingHourTo, to: workingHourTo,
}, },
demoMode,
analyticsEnabled
}); });
} catch (exception) { } catch (exception) {
console.error(exception); console.error(exception);
throwMessage('Error while trying to store settings.', 'error'); throwMessage('Error while trying to store settings.', 'error');
return; return;
} }
throwMessage('Settings stored successfully. You MUST restart Fredy.', 'success'); throwMessage('Settings stored successfully.', 'success');
}; };
return ( return (
@@ -173,22 +180,27 @@ const GeneralSettings = function GeneralSettings() {
closeIcon={null} closeIcon={null}
title={ title={
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}> <div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
ScrapingAnt is needed to scrape Immoscout. ScrapingAnt itself is using 2 different types of proxies ScrapingAnt is needed to scrape Immoscout. ScrapingAnt itself is using 2
different types of proxies
</div> </div>
} }
style={{marginBottom: '1rem'}} style={{marginBottom: '1rem'}}
description={ description={
<div> <div>
<h4>Datacenter-Proxy</h4> <h4>Datacenter-Proxy</h4>
Proxy server located in one of the datacenters across the world. Datacenter proxies are slower and Proxy server located in one of the datacenters across the world. Datacenter
more likely to fail, but they are cheaper. A call with a datacenter proxy cost 10 credits. proxies are slower and
more likely to fail, but they are cheaper. A call with a datacenter proxy cost
10 credits.
<h4>Residential-Proxy</h4> <h4>Residential-Proxy</h4>
High-quality proxy server located in one of the real people houses across the world. Datacenter High-quality proxy server located in one of the real people houses across the
world. Datacenter
proxies are faster and more likely to success, but they are more expensive. proxies are faster and more likely to success, but they are more expensive.
<br/> <br/>
<br/> <br/>
<b> <b>
On the free tier, you have 10.000 credits, so chose your option wisely. Keep in mind, only On the free tier, you have 10.000 credits, so chose your option wisely. Keep
in mind, only
successful calls will be charged. successful calls will be charged.
</b> </b>
</div> </div>
@@ -199,7 +211,8 @@ const GeneralSettings = function GeneralSettings() {
<Radio name="datacenter" value="datacenter" checked={scrapingAntProxy === 'datacenter'}> <Radio name="datacenter" value="datacenter" checked={scrapingAntProxy === 'datacenter'}>
Datacenter proxy Datacenter proxy
</Radio> </Radio>
<Radio name="residential" value="residential" checked={scrapingAntProxy === 'residential'}> <Radio name="residential" value="residential"
checked={scrapingAntProxy === 'residential'}>
Residential proxy Residential proxy
</Radio> </Radio>
</RadioGroup> </RadioGroup>
@@ -231,6 +244,80 @@ const GeneralSettings = function GeneralSettings() {
/> />
</div> </div>
</SegmentPart> </SegmentPart>
<Divider margin="1rem"/>
<SegmentPart
name="Analytics"
helpText="Insights into the usage of Fredy."
Icon={IconLineChartStroked}
>
<Banner
fullMode={false}
type="info"
closeIcon={null}
title={
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
Explanation
</div>
}
style={{marginBottom: '1rem'}}
description={
<div>
Analytics are disabled by default. If you choose to enable them, we will begin tracking the following:<br/>
<ul>
<li>Name of active provider (e.g. Immoscout)</li>
<li>Name of active adapter (e.g. Console)</li>
<li>language</li>
<li>os</li>
<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
checked={analyticsEnabled}
onChange={(e) => setAnalyticsEnabled(e.target.checked)}
> Enabled
</Checkbox>
</SegmentPart>
<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>
}
/>
<Checkbox
checked={demoMode}
onChange={(e) => setDemoMode(e.target.checked)}
> Enabled
</Checkbox>
</SegmentPart>
<Divider margin="1rem"/> <Divider margin="1rem"/>
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave/>}> <Button type="primary" theme="solid" onClick={onStore} icon={<IconSave/>}>
Save Save

View File

@@ -1,9 +1,31 @@
import React from 'react'; import React from 'react';
import {format} from '../../services/time/timeService'; import {format} from '../../services/time/timeService';
import { Card, Descriptions, Divider } from '@douyinfe/semi-ui'; import {Banner, Card, Descriptions, Divider} from '@douyinfe/semi-ui';
import {IconBolt} from '@douyinfe/semi-icons'; import {IconBolt} from '@douyinfe/semi-icons';
export default function ProcessingTimes({ processingTimes }) {
export default function ProcessingTimes({processingTimes = {}}) {
const {Meta} = Card; const {Meta} = Card;
if (Object.keys(processingTimes).length === 0) {
return null;
}
if (processingTimes.error != null) {
return <Banner
fullMode={false}
type="danger"
closeIcon={null}
title={
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
Scraping Ant Error
</div>
}
style={{marginBottom: '1rem'}}
description={
<div>
{processingTimes.error}
</div>
}
/>;
}
return ( return (
<> <>
<Descriptions <Descriptions
@@ -26,7 +48,7 @@ export default function ProcessingTimes({ processingTimes }) {
)} )}
</Descriptions> </Descriptions>
{processingTimes.scrapingAntData != null && ( {(processingTimes.scrapingAntData != null && Object.keys(processingTimes.scrapingAntData).length > 0) &&(
<> <>
<Divider margin="1rem"/> <Divider margin="1rem"/>
<Card <Card
@@ -47,12 +69,13 @@ export default function ProcessingTimes({ processingTimes }) {
Credits: {processingTimes.scrapingAntData.remained_credits}/ Credits: {processingTimes.scrapingAntData.remained_credits}/
{processingTimes.scrapingAntData.plan_total_credits} {processingTimes.scrapingAntData.plan_total_credits}
</p> </p>
If you want to scrape Immoscout or Immonet more often, you have to purchase a premium account of{' '} If you want to scrape Immoscout or Immonet more often, you have to purchase a premium account
of{' '}
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer"> <a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
ScrapingAnt ScrapingAnt
</a> </a>
. You can use the code <b>FREDY10</b> to get 10% off. (No affiliation, we are <b>not</b> getting paid to . You can use the code <b>FREDY10</b> to get 10% off. (No affiliation, we are <b>not</b> getting
recommend ScrapingAnt.) paid by ScrapingAnt.)
</Card> </Card>
</> </>
)} )}

709
yarn.lock

File diff suppressed because it is too large Load Diff