chore: run formatter (#145)

This commit is contained in:
Alexander Roidl
2025-07-23 08:47:26 +02:00
committed by GitHub
parent 314b1818d7
commit cca1463a68
19 changed files with 666 additions and 665 deletions

View File

@@ -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,
); );

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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('?'));

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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();
});
}); });
});
}); });

View File

@@ -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();
});
}); });
});
}); });

View File

@@ -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;
}); });
}); });
}); });

View File

@@ -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';

View File

@@ -23,5 +23,5 @@ root.render(
<App /> <App />
</LocaleProvider> </LocaleProvider>
</HashRouter> </HashRouter>
</Provider> </Provider>,
); );

View File

@@ -1,52 +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()) {
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 ones important, and I promise it will only appear once ;)</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 <p>
through my GitHub, but theres absolutely no obligation to do so.</p> Fredy is completely free (and will always remain free). If youd like, you can support me by donating through
<p>However, it would be a huge my GitHub, but theres absolutely no obligation to do so.
help if youd 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 youd 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>
);
} }

View File

@@ -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;

View File

@@ -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 {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>
</> </>
); );
} }

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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';