diff --git a/README.md b/README.md
index 145ae3de..f3aee2ed 100755
--- a/README.md
+++ b/README.md
@@ -107,9 +107,9 @@ yarn run start:frontend # in another terminal
## 📸 Screenshots
-| Job Configuration | Job Analytics | Job Overview |
-|-------------------|--------------|--------------|
-|  |  |  |
+| Fredy Main Overview | Job Configuration | Found Listings |
+|--------------------------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------|
+|  |  |  |
------------------------------------------------------------------------
diff --git a/doc/screenshot1.png b/doc/screenshot1.png
index 1dd9a977..65859cb0 100644
Binary files a/doc/screenshot1.png and b/doc/screenshot1.png differ
diff --git a/doc/screenshot2.png b/doc/screenshot2.png
new file mode 100644
index 00000000..21e92b80
Binary files /dev/null and b/doc/screenshot2.png differ
diff --git a/doc/screenshot3.png b/doc/screenshot3.png
new file mode 100644
index 00000000..939d0d67
Binary files /dev/null and b/doc/screenshot3.png differ
diff --git a/doc/screenshot_2.png b/doc/screenshot_2.png
deleted file mode 100644
index 2658fa0f..00000000
Binary files a/doc/screenshot_2.png and /dev/null differ
diff --git a/doc/screenshot_3.png b/doc/screenshot_3.png
deleted file mode 100644
index c407c2d3..00000000
Binary files a/doc/screenshot_3.png and /dev/null differ
diff --git a/lib/api/routes/versionRouter.js b/lib/api/routes/versionRouter.js
index 4a685e64..9810fbcb 100644
--- a/lib/api/routes/versionRouter.js
+++ b/lib/api/routes/versionRouter.js
@@ -8,7 +8,14 @@ const versionRouter = service.newRouter();
versionRouter.get('/', async (req, res) => {
const versionPayload = await getCurrentVersionFromGithub();
- res.body = versionPayload == null ? { newVersion: false } : versionPayload;
+ const localFredyVersion = await getPackageVersion();
+ res.body =
+ versionPayload == null
+ ? {
+ newVersion: false,
+ localFredyVersion,
+ }
+ : versionPayload;
res.send();
});
diff --git a/package.json b/package.json
index 02ac9dce..f8e9500e 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fredy",
- "version": "12.3.2",
+ "version": "14.0.0",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"prepare": "husky",
@@ -62,7 +62,7 @@
"@visactor/react-vchart": "^2.0.5",
"@visactor/vchart": "^2.0.5",
"@visactor/vchart-semi-theme": "^1.12.2",
- "@vitejs/plugin-react": "5.0.3",
+ "@vitejs/plugin-react": "5.0.4",
"better-sqlite3": "^12.4.1",
"body-parser": "2.2.0",
"cheerio": "^1.1.2",
@@ -82,8 +82,8 @@
"query-string": "9.3.1",
"react": "18.3.1",
"react-dom": "18.3.1",
- "react-router": "7.9.2",
- "react-router-dom": "7.9.2",
+ "react-router": "7.9.3",
+ "react-router-dom": "7.9.3",
"restana": "5.1.0",
"semver": "^7.7.2",
"serve-static": "2.2.0",
@@ -97,7 +97,7 @@
"@babel/eslint-parser": "7.28.4",
"@babel/preset-env": "7.28.3",
"@babel/preset-react": "7.27.1",
- "chai": "6.0.1",
+ "chai": "6.2.0",
"eslint": "9.36.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
@@ -105,7 +105,7 @@
"history": "5.3.0",
"husky": "9.1.7",
"less": "4.4.1",
- "lint-staged": "16.2.1",
+ "lint-staged": "16.2.3",
"mocha": "11.7.2",
"nodemon": "^3.1.10",
"prettier": "3.6.2"
diff --git a/ui/src/App.jsx b/ui/src/App.jsx
index fbfab609..8efd5faa 100644
--- a/ui/src/App.jsx
+++ b/ui/src/App.jsx
@@ -8,18 +8,19 @@ import UserMutator from './views/user/mutation/UserMutator';
import JobInsight from './views/jobs/insights/JobInsight.jsx';
import { useActions, useSelector } from './services/state/store';
import { Routes, Route, Navigate } from 'react-router-dom';
-import Logout from './components/logout/Logout';
-import Logo from './components/logo/Logo';
-import Menu from './components/menu/Menu';
import Login from './views/login/Login';
import Users from './views/user/Users';
import Jobs from './views/jobs/Jobs';
import './App.less';
import TrackingModal from './components/tracking/TrackingModal.jsx';
-import { Banner } from '@douyinfe/semi-ui';
+import { Banner, Divider } from '@douyinfe/semi-ui';
import VersionBanner from './components/version/VersionBanner.jsx';
import Listings from './views/listings/Listings.jsx';
+import Navigation from './components/navigation/Navigation.jsx';
+import { Layout } from '@douyinfe/semi-ui';
+import FredyFooter from './components/footer/FredyFooter.jsx';
+import ProcessingTimes from './views/jobs/ProcessingTimes.jsx';
export default function FredyApp() {
const actions = useActions();
@@ -27,6 +28,7 @@ export default function FredyApp() {
const currentUser = useSelector((state) => state.user.currentUser);
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
const settings = useSelector((state) => state.generalSettings.settings);
+ const processingTimes = useSelector((state) => state.jobs.processingTimes);
useEffect(() => {
async function init() {
@@ -50,6 +52,7 @@ export default function FredyApp() {
};
const isAdmin = () => currentUser != null && currentUser.isAdmin;
+ const { Footer, Sider, Content } = Layout;
return loading ? null : needsLogin() ? (
@@ -57,71 +60,80 @@ export default function FredyApp() {
} />
) : (
-
-
-
-
-
- {versionUpdate?.newVersion &&
}
- {settings.demoMode && (
- <>
-
-
- >
- )}
- {settings.analyticsEnabled === null && !settings.demoMode &&
}
-
- } />
- } />
- } />
- } />
- } />
- } />
+
+
+
+
+
+
+ {versionUpdate?.newVersion && }
+ {settings.demoMode && (
+ <>
+
+
+ >
+ )}
+ {settings.analyticsEnabled === null && !settings.demoMode && }
+ {processingTimes != null && }
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
- {/* Permission-aware routes */}
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
+ {/* Permission-aware routes */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
- } />
-
-
-
+
} />
+
+
+
+
+
+
);
}
diff --git a/ui/src/App.less b/ui/src/App.less
index c3bc3011..b9e881e8 100644
--- a/ui/src/App.less
+++ b/ui/src/App.less
@@ -1,12 +1,9 @@
.app {
- display: flex;
- flex-direction: column;
+ height: 100%;
width: 100%;
- &__container {
- padding: 1rem 1rem;
- color: var(--semi-color-text-0);
- background-color: #232429;
+ &__content {
+ margin: 1rem;
}
}
diff --git a/ui/src/components/footer/FredyFooter.jsx b/ui/src/components/footer/FredyFooter.jsx
new file mode 100644
index 00000000..a5589ed3
--- /dev/null
+++ b/ui/src/components/footer/FredyFooter.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import './FredyFooter.less';
+import { useSelector } from '../../services/state/store.js';
+import { Typography } from '@douyinfe/semi-ui';
+
+export default function FredyFooter() {
+ const { Text } = Typography;
+ const version = useSelector((state) => state.versionUpdate.versionUpdate);
+ return (
+
+
+ Fredy V{version?.localFredyVersion || 'N/A'}
+
+
+ Made with ❤️
+
+
+ );
+}
diff --git a/ui/src/components/footer/FredyFooter.less b/ui/src/components/footer/FredyFooter.less
new file mode 100644
index 00000000..5909fa8e
--- /dev/null
+++ b/ui/src/components/footer/FredyFooter.less
@@ -0,0 +1,17 @@
+.fredyFooter {
+ background:rgb(53, 54, 60);
+ color: white;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ &__version {
+ padding-left: .5rem;
+ font-size: small;
+
+ }
+ &__copyRight {
+ padding-right: 1rem;
+
+ }
+}
\ No newline at end of file
diff --git a/ui/src/components/logout/Logout.jsx b/ui/src/components/logout/Logout.jsx
index 24d41850..04908ddf 100644
--- a/ui/src/components/logout/Logout.jsx
+++ b/ui/src/components/logout/Logout.jsx
@@ -2,19 +2,22 @@ import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import { xhrPost } from '../../services/xhr';
import { IconUser } from '@douyinfe/semi-icons';
-const Logout = function Logout() {
+
+const Logout = function Logout({ text }) {
return (
- }
- type="danger"
- theme="solid"
- onClick={async () => {
- await xhrPost('/api/login/logout');
- location.reload();
- }}
- >
- Logout
-
+
+ }
+ type="danger"
+ theme="solid"
+ onClick={async () => {
+ await xhrPost('/api/login/logout');
+ location.reload();
+ }}
+ >
+ {text && 'Logout'}
+
+
);
};
diff --git a/ui/src/components/menu/Menu.jsx b/ui/src/components/menu/Menu.jsx
deleted file mode 100644
index 45683ae0..00000000
--- a/ui/src/components/menu/Menu.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router-dom';
-import { Tabs, TabPane } from '@douyinfe/semi-ui';
-
-import { useLocation } from 'react-router-dom';
-import { IconUser, IconTerminal, IconSetting, IconArchive } from '@douyinfe/semi-icons';
-import './Menu.less';
-
-function parsePathName(name) {
- const split = name.split('/').filter((s) => s.length !== 0);
- return '/' + split[0];
-}
-
-const TopMenu = function TopMenu({ isAdmin }) {
- const navigate = useNavigate();
- const location = useLocation();
- return (
- navigate(key)}>
-
-
- Jobs
-
- }
- />
-
-
- Found listings
-
- }
- />
-
- {isAdmin && (
-
-
- User
-
- }
- />
- )}
-
- {isAdmin && (
-
-
- Settings
-
- }
- />
- )}
-
- );
-};
-
-export default TopMenu;
diff --git a/ui/src/components/menu/Menu.less b/ui/src/components/menu/Menu.less
deleted file mode 100644
index 34102907..00000000
--- a/ui/src/components/menu/Menu.less
+++ /dev/null
@@ -1,3 +0,0 @@
-.menu {
- margin-top: 3rem;
-}
diff --git a/ui/src/components/navigation/Navigate.less b/ui/src/components/navigation/Navigate.less
new file mode 100644
index 00000000..394b175a
--- /dev/null
+++ b/ui/src/components/navigation/Navigate.less
@@ -0,0 +1,9 @@
+.navigate {
+ &__logout_Button {
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ display: flex;
+
+ }
+}
\ No newline at end of file
diff --git a/ui/src/components/navigation/Navigation.jsx b/ui/src/components/navigation/Navigation.jsx
new file mode 100644
index 00000000..7c553a8f
--- /dev/null
+++ b/ui/src/components/navigation/Navigation.jsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Nav } from '@douyinfe/semi-ui';
+import { IconUser, IconStar, IconSetting, IconTerminal } from '@douyinfe/semi-icons';
+import logoWhite from '../../assets/logo_white.png';
+import Logout from '../logout/Logout.jsx';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+import './Navigate.less';
+import { useScreenWidth } from '../../hooks/screenWidth.js';
+
+export default function Navigation({ isAdmin }) {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const width = useScreenWidth();
+ const collapsed = width <= 850;
+
+ const items = [
+ { itemKey: '/jobs', text: 'Jobs', icon: },
+ { itemKey: '/listings', text: 'Found Listings', icon: },
+ ];
+
+ if (isAdmin) {
+ items.push({ itemKey: '/users', text: 'User Management', icon: });
+ items.push({ itemKey: '/generalSettings', text: 'Settings', icon: });
+ }
+
+ function parsePathName(name) {
+ const split = name.split('/').filter((s) => s.length !== 0);
+ return '/' + split[0];
+ }
+
+ return (
+