adding new dashboard view. Muchas wow

This commit is contained in:
orangecoding
2025-12-14 12:23:59 +01:00
parent 87b5673bf0
commit 87771655a8
31 changed files with 688 additions and 1047 deletions

View File

@@ -0,0 +1,23 @@
.chartCard {
/* Use provided background with slight transparency and a brighter mix */
background: color-mix(in oklab, var(--card-bg, rgb(70 72 78)) 20%, white 80%);
border-radius: .6rem;
border: 1px solid color-mix(in oklab, var(--card-bg, rgb(70 72 78)) 35%, white 65%);
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
/* Ensure base text has strong contrast */
color: var(--semi-color-text-0);
/* Semi Card header/title styling */
.semi-card-header .semi-card-header-title {
/* Derive a tinted title color with stronger contrast towards black */
color: color-mix(in oklab, var(--card-bg, rgb(70 72 78)) 60%, black 40%);
font-weight: 600;
}
&__no__data {
display: grid;
place-items: center;
height: 14rem;
opacity: .7;
}
}

View File

@@ -0,0 +1,92 @@
@import "DashboardCardColors.less";
.color-variant(@bg, @border, @text) {
background-color: @bg;
border: 1px solid @border;
color: @text;
}
.dashboard-card {
box-sizing: border-box;
padding: .8rem;
border-radius: .5rem;
border-width: 1px;
font-weight: 600;
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
/* Make all KPI boxes the same size regardless of content/font */
width: 100%;
max-width: none;
height: 10rem;
display: flex;
flex-direction: column;
&.blue {
.color-variant(@color-blue-bg, @color-blue-border, @color-blue-text);
}
&.orange {
.color-variant(@color-orange-bg, @color-orange-border, @color-orange-text);
}
&.green {
.color-variant(@color-green-bg, @color-green-border, @color-green-text);
}
&.purple {
.color-variant(@color-purple-bg, @color-purple-border, @color-purple-text);
}
&.gray {
.color-variant(@color-gray-bg, @color-gray-border, @color-gray-text);
}
&__header {
display: flex;
align-items: center;
gap: .6rem;
/* Keep header from growing content height */
min-height: 2rem;
overflow: hidden;
}
&__icon {
border-radius: .6rem;
display: grid;
place-items: center;
}
&__title {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__content {
margin-top: .4rem;
font-size: .7rem;
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
}
&__value {
margin: 0;
font-size: 1.5rem;
line-height: 1.1;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__desc {
opacity: .8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@@ -0,0 +1,19 @@
@color-blue-bg: rgba(0, 123, 255, 0.24);
@color-blue-border: #1E40AFFF;
@color-blue-text: #60a5fa;
@color-orange-bg: rgba(250, 91, 5, 0.12);
@color-orange-border: #d33601;
@color-orange-text: #FB923CFF;
@color-green-bg: rgba(38, 250, 5, 0.12);
@color-green-border: #00c316;
@color-green-text: #33f308;
@color-purple-bg: rgba(91, 3, 218, 0.38);
@color-purple-border: #7500c3;
@color-purple-text: #b15fff;
@color-gray-bg: rgba(110, 110, 110, 0.38);
@color-gray-border: #807f7f;
@color-gray-text: #bab9b9;

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
/*
* Copyright (c) 2025 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import React from 'react';
import './DashboardCard.less';
export default function KpiCard({
title,
icon,
value,
valueFontSize = '1.5rem',
description,
color = 'gray',
children,
}) {
return (
<div className={`dashboard-card ${color}`}>
<div className="dashboard-card__header">
<div className="dashboard-card__icon">{icon}</div>
<div className="dashboard-card__title">
<span>{title}</span>
</div>
</div>
<div className="dashboard-card__content">
<p className="dashboard-card__value" style={{ fontSize: valueFontSize }}>
{value}
{children}
</p>
{description && <span className="dashboard-card__desc">{description}</span>}
</div>
</div>
);
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2025 by Christian Kellner.
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import React from 'react';
import { Pie } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend, Title as ChartTitle } from 'chart.js';
import './ChartCard.less';
ChartJS.register(ArcElement, Tooltip, Legend, ChartTitle);
export default function PieChartCard({ data = [] }) {
const { labels, values } = React.useMemo(() => {
if (data && typeof data === 'object' && !Array.isArray(data)) {
const lbls = Array.isArray(data.labels) ? data.labels : [];
const vals = Array.isArray(data.values)
? data.values.map((v) => (Number.isFinite(Number(v)) ? Number(v) : 0))
: [];
return { labels: lbls, values: vals };
}
if (Array.isArray(data)) {
const lbls = data.map((d) => d?.type ?? 'Unknown');
const vals = data.map((d) => {
const v = Number(d?.value);
return Number.isFinite(v) ? v : 0;
});
return { labels: lbls, values: vals };
}
return { labels: [], values: [] };
}, [data]);
const palette = React.useMemo(
() => [
'#4e79a7',
'#f28e2b',
'#e15759',
'#76b7b2',
'#59a14f',
'#edc948',
'#b07aa1',
'#ff9da7',
'#9c755f',
'#bab0ab',
],
[],
);
const chartData = React.useMemo(
() => ({
labels,
datasets: [
{
data: values,
backgroundColor: labels.map((_, i) => palette[i % palette.length]),
borderColor: labels.map((_, i) => palette[i % palette.length]),
borderWidth: 1,
},
],
}),
[labels, values, palette],
);
const options = React.useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'right',
labels: {
color: () => '#fff',
},
},
title: { display: false },
tooltip: {
callbacks: {
label: (ctx) => {
const label = ctx.label || '';
const val = ctx.parsed !== undefined ? ctx.parsed : ctx.raw;
return `${label}: ${val}%`;
},
},
},
},
}),
[],
);
const isEmpty = !labels || labels.length === 0 || !values || values.length === 0;
return (
<>{isEmpty ? <div className="chartCard__no__data">No Data</div> : <Pie data={chartData} options={options} />}</>
);
}

View File

@@ -1,9 +1,10 @@
.navigate {
&__logout_Button {
&__footer {
align-items: center;
justify-content: center;
flex-direction: column;
gap: 0.5rem;
width: 100%;
display: flex;
}
}

View File

@@ -3,26 +3,34 @@
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import React from 'react';
import { Nav } from '@douyinfe/semi-ui';
import { IconStar, IconSetting, IconTerminal } from '@douyinfe/semi-icons';
import React, { useEffect, useState } from 'react';
import { Button, Nav } from '@douyinfe/semi-ui';
import { IconStar, IconSetting, IconTerminal, IconHistogram, IconSidebar } from '@douyinfe/semi-icons';
import logoWhite from '../../assets/logo_white.png';
import heart from '../../assets/heart.png';
import Logout from '../logout/Logout.jsx';
import { useLocation, useNavigate } from 'react-router-dom';
import './Navigate.less';
import { useScreenWidth } from '../../hooks/screenWidth.js';
import { useFeature } from '../../hooks/featureHook.js';
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 [collapsed, setCollapsed] = useState(width <= 850);
const watchlistFeature = useFeature('WATCHLIST_MANAGEMENT') || false;
useEffect(() => {
if (width <= 850) {
setCollapsed(true);
}
}, [width]);
const items = [
{ itemKey: '/dashboard', text: 'Dashboard', icon: <IconHistogram /> },
{ itemKey: '/jobs', text: 'Jobs', icon: <IconTerminal /> },
{ itemKey: '/listings', text: 'Listings', icon: <IconStar /> },
];
@@ -51,18 +59,21 @@ export default function Navigation({ isAdmin }) {
return (
<Nav
style={{ height: '100%', width: collapsed ? '' : '13.2rem' }}
style={{ height: '100%' }}
items={items}
isCollapsed={collapsed}
selectedKeys={[parsePathName(location.pathname)]}
onSelect={(key) => {
navigate(key.itemKey);
}}
header={<img src={logoWhite} width="180" alt="Fredy Logo" />}
header={<img src={collapsed ? heart : logoWhite} width={collapsed ? '80' : '160'} alt="Fredy Logo" />}
footer={
<div className="navigate__logout_Button">
<Nav.Footer className="navigate__footer">
<Logout text={!collapsed} />
</div>
<Button icon={<IconSidebar />} onClick={() => setCollapsed(!collapsed)}>
{!collapsed && 'Collapse'}
</Button>
</Nav.Footer>
}
/>
);

View File

@@ -8,14 +8,16 @@ import { Card } from '@douyinfe/semi-ui';
import './SegmentParts.less';
export const SegmentPart = ({ name, Icon = null, children, helpText }) => {
export const SegmentPart = ({ name, Icon = null, children, helpText = null }) => {
const { Meta } = Card;
return (
<Card
className="segmentParts"
title={
<Meta title={name} description={helpText} avatar={Icon == null ? null : <Icon size="extra-extra-small" />} />
(helpText || name) && (
<Meta title={name} description={helpText} avatar={Icon == null ? null : <Icon size="extra-extra-small" />} />
)
}
>
{children}

View File

@@ -1,6 +1,6 @@
.segmentParts {
border: 1px solid #323232 !important;
border-radius: 5px !important;
border-radius: .9rem !important;
color: rgba(var(--semi-grey-8), 1);
background: rgb(53, 54, 60);
margin: 2rem;

View File

@@ -6,7 +6,7 @@
import React from 'react';
import { Button, Empty, Table, Switch, Popover } from '@douyinfe/semi-ui';
import { IconAlertTriangle, IconDelete, IconDescend2, IconEdit, IconHistogram } from '@douyinfe/semi-icons';
import { IconAlertTriangle, IconDelete, IconDescend2, IconEdit } from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import './JobTable.less';
@@ -21,14 +21,7 @@ const empty = (
const getPopoverContent = (text) => <article className="jobPopoverContent">{text}</article>;
export default function JobTable({
jobs = {},
onJobRemoval,
onJobStatusChanged,
onJobEdit,
onJobInsight,
onListingRemoval,
} = {}) {
export default function JobTable({ jobs = {}, onJobRemoval, onJobStatusChanged, onJobEdit, onListingRemoval } = {}) {
return (
<Table
pagination={false}
@@ -98,14 +91,6 @@ export default function JobTable({
render: (_, job) => {
return (
<div className="interactions">
<Popover content={getPopoverContent('Job Insights')}>
<Button
type="primary"
icon={<IconHistogram />}
disabled={job.isOnlyShared}
onClick={() => onJobInsight(job.id)}
/>
</Popover>
<Popover content={getPopoverContent('Edit a Job')}>
<Button
type="secondary"