Merge branch 'main' of https://github.com/Wind-Explorer/ecoconnect
This commit is contained in:
@@ -13,11 +13,13 @@
|
|||||||
"@internationalized/date": "^3.5.5",
|
"@internationalized/date": "^3.5.5",
|
||||||
"@nextui-org/react": "^2.4.2",
|
"@nextui-org/react": "^2.4.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"chart.js": "^4.4.3",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"framer-motion": "^11.2.10",
|
"framer-motion": "^11.2.10",
|
||||||
"openai": "^4.53.2",
|
"openai": "^4.53.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
|
|||||||
27
client/pnpm-lock.yaml
generated
27
client/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ dependencies:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.2
|
specifier: ^1.7.2
|
||||||
version: 1.7.2
|
version: 1.7.2
|
||||||
|
chart.js:
|
||||||
|
specifier: ^4.4.3
|
||||||
|
version: 4.4.3
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.12
|
specifier: ^1.11.12
|
||||||
version: 1.11.12
|
version: 1.11.12
|
||||||
@@ -29,6 +32,9 @@ dependencies:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
|
react-chartjs-2:
|
||||||
|
specifier: ^5.2.0
|
||||||
|
version: 5.2.0(chart.js@4.4.3)(react@18.3.1)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
@@ -689,6 +695,10 @@ packages:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
|
/@kurkle/color@0.3.2:
|
||||||
|
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@nextui-org/accordion@2.0.35(@nextui-org/system@2.2.2)(@nextui-org/theme@2.2.6)(framer-motion@11.2.10)(react-dom@18.3.1)(react@18.3.1):
|
/@nextui-org/accordion@2.0.35(@nextui-org/system@2.2.2)(@nextui-org/theme@2.2.6)(framer-motion@11.2.10)(react-dom@18.3.1)(react@18.3.1):
|
||||||
resolution: {integrity: sha512-42T8DAgpICKORry5h1UCgAQ71QJ3dCzvqrnnJQco3LICeIER2JT/wEdpxHUVT893MkL6z6CFsJmWNfFJPk59kA==}
|
resolution: {integrity: sha512-42T8DAgpICKORry5h1UCgAQ71QJ3dCzvqrnnJQco3LICeIER2JT/wEdpxHUVT893MkL6z6CFsJmWNfFJPk59kA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3648,6 +3658,13 @@ packages:
|
|||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/chart.js@4.4.3:
|
||||||
|
resolution: {integrity: sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==}
|
||||||
|
engines: {pnpm: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
'@kurkle/color': 0.3.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/chokidar@3.6.0:
|
/chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@@ -4747,6 +4764,16 @@ packages:
|
|||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
/react-chartjs-2@5.2.0(chart.js@4.4.3)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==}
|
||||||
|
peerDependencies:
|
||||||
|
chart.js: ^4.1.1
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
chart.js: 4.4.3
|
||||||
|
react: 18.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-dom@18.3.1(react@18.3.1):
|
/react-dom@18.3.1(react@18.3.1):
|
||||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import HBContestPage from "./pages/HBContestPage";
|
|||||||
import HBFormPage from "./pages/HBFormPage";
|
import HBFormPage from "./pages/HBFormPage";
|
||||||
import DefaultLayout from "./layouts/default";
|
import DefaultLayout from "./layouts/default";
|
||||||
import AdministratorLayout from "./layouts/administrator";
|
import AdministratorLayout from "./layouts/administrator";
|
||||||
import UsersManagement from "./pages/UsersManagement";
|
import UsersManagement from "./pages/UsersManagementPage";
|
||||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||||
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
||||||
import RestrictedLayout from "./layouts/restricted";
|
import RestrictedLayout from "./layouts/restricted";
|
||||||
|
|||||||
106
client/src/components/UserCountGraph.tsx
Normal file
106
client/src/components/UserCountGraph.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import instance from "../security/http";
|
||||||
|
import config from "../config";
|
||||||
|
import { Line } from "react-chartjs-2";
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ChartOptions,
|
||||||
|
} from "chart.js";
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function UserCountGraph() {
|
||||||
|
const [userInformationCollection, setUserInformationCollection] = useState<
|
||||||
|
any[]
|
||||||
|
>([]);
|
||||||
|
const [chartData, setChartData] = useState<any>(null);
|
||||||
|
|
||||||
|
const retrieveUserInformationCollection = () => {
|
||||||
|
instance.get(`${config.serverAddress}/users/all`).then((values) => {
|
||||||
|
setUserInformationCollection(values.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
retrieveUserInformationCollection();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userInformationCollection.length > 0) {
|
||||||
|
const lastMonth = new Date();
|
||||||
|
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
||||||
|
const daysCount = new Array(31).fill(0); // 记录过去一个月每天的用户注册数量
|
||||||
|
|
||||||
|
userInformationCollection.forEach((user) => {
|
||||||
|
const createdAt = new Date(user.createdAt);
|
||||||
|
if (createdAt > lastMonth) {
|
||||||
|
const dayIndex = Math.floor(
|
||||||
|
(createdAt.getTime() - lastMonth.getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
daysCount[dayIndex]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = Array.from({ length: 31 }, (_, i) => {
|
||||||
|
const currentDate = new Date();
|
||||||
|
currentDate.setDate(currentDate.getDate() - (30 - i));
|
||||||
|
return currentDate.toISOString().split("T")[0]; // 返回日期字符串 YYYY-MM-DD
|
||||||
|
});
|
||||||
|
|
||||||
|
setChartData({
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "User Onboard",
|
||||||
|
data: daysCount,
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: "rgba(75,192,192,0.4)",
|
||||||
|
borderColor: "rgba(75,192,192,1)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userInformationCollection]);
|
||||||
|
|
||||||
|
// 定义图表选项
|
||||||
|
const options: ChartOptions<"line"> = {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: Math.max(
|
||||||
|
10,
|
||||||
|
Math.ceil(Math.max(...(chartData?.datasets[0]?.data || [10])))
|
||||||
|
), // 动态设置最大值,最低为10
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1, // 设置步长为1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{chartData ? (
|
||||||
|
<Line data={chartData} options={options} />
|
||||||
|
) : (
|
||||||
|
<p>Loading...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
client/src/components/UserTownCouncilDistributionChart.tsx
Normal file
106
client/src/components/UserTownCouncilDistributionChart.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import instance from "../security/http";
|
||||||
|
import config from "../config";
|
||||||
|
import { Pie } from "react-chartjs-2";
|
||||||
|
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
|
||||||
|
|
||||||
|
// Register Chart.js components
|
||||||
|
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||||
|
|
||||||
|
export default function UserTownCouncilDistributionChart() {
|
||||||
|
const [userInformationCollection, setUserInformationCollection] = useState<
|
||||||
|
any[]
|
||||||
|
>([]);
|
||||||
|
const [townCouncilInformation, setTownCouncilInformation] = useState<
|
||||||
|
string[]
|
||||||
|
>([]);
|
||||||
|
const [chartData, setChartData] = useState<any>(null);
|
||||||
|
|
||||||
|
const retrieveInformation = () => {
|
||||||
|
instance.get(`${config.serverAddress}/users/all`).then((values) => {
|
||||||
|
setUserInformationCollection(values.data);
|
||||||
|
});
|
||||||
|
instance
|
||||||
|
.get(`${config.serverAddress}/users/town-councils-metadata`)
|
||||||
|
.then((values) => {
|
||||||
|
setTownCouncilInformation(JSON.parse(values.data).townCouncils);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateChartData = () => {
|
||||||
|
const townCouncilCounts: { [key: string]: number } = {};
|
||||||
|
userInformationCollection.forEach((user: any) => {
|
||||||
|
const townCouncil = user.townCouncil || "Unknown";
|
||||||
|
if (townCouncilCounts[townCouncil]) {
|
||||||
|
townCouncilCounts[townCouncil]++;
|
||||||
|
} else {
|
||||||
|
townCouncilCounts[townCouncil] = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = Object.keys(townCouncilCounts);
|
||||||
|
const data = Object.values(townCouncilCounts);
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
backgroundColor: labels.map(
|
||||||
|
(label, index) => `hsl(${index * (360 / labels.length)}, 70%, 50%)`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
setChartData(chartData);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
retrieveInformation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
userInformationCollection.length > 0 &&
|
||||||
|
townCouncilInformation.length > 0
|
||||||
|
) {
|
||||||
|
generateChartData();
|
||||||
|
}
|
||||||
|
}, [userInformationCollection, townCouncilInformation]);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "left" as const,
|
||||||
|
labels: {
|
||||||
|
font: {
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
color: "black",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (tooltipItem: any) {
|
||||||
|
const dataset = tooltipItem.dataset;
|
||||||
|
const dataIndex = tooltipItem.dataIndex;
|
||||||
|
const value = dataset.data[dataIndex];
|
||||||
|
return `${chartData.labels[dataIndex]}: ${value}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{chartData ? (
|
||||||
|
<Pie data={chartData} options={options} />
|
||||||
|
) : (
|
||||||
|
<p>Loading...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import UserProfilePicture from "../components/UserProfilePicture";
|
|||||||
import { Button, Card } from "@nextui-org/react";
|
import { Button, Card } from "@nextui-org/react";
|
||||||
import { PencilSquareIcon } from "../icons";
|
import { PencilSquareIcon } from "../icons";
|
||||||
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
|
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
|
||||||
|
import UserCountGraph from "../components/UserCountGraph";
|
||||||
|
import UserTownCouncilDistributionChart from "../components/UserTownCouncilDistributionChart";
|
||||||
|
|
||||||
export default function AdministratorSpringboard() {
|
export default function AdministratorSpringboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -75,28 +77,18 @@ export default function AdministratorSpringboard() {
|
|||||||
<Card className="w-full bg-primary-500 p-8 text-white rounded-r-none">
|
<Card className="w-full bg-primary-500 p-8 text-white rounded-r-none">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<p className="font-bold text-4xl">Statistics Overview</p>
|
<p className="font-bold text-4xl">Statistics Overview</p>
|
||||||
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
|
<div className="w-full h-full bg-primary-600 rounded-2xl flex flex-col gap-4 p-6">
|
||||||
<div className="w-60 flex flex-col justify-between">
|
<p className="text-3xl">Users Onboard</p>
|
||||||
<div className="flex flex-col">
|
<div className="w-full p-4 bg-white rounded-xl">
|
||||||
<p className="text-2xl">User Count</p>
|
<UserCountGraph />
|
||||||
<p className="opacity-70">(past 30 days)</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg">
|
|
||||||
Total: <span className="font-bold">2139</span> users
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-full bg-white rounded-xl">
|
|
||||||
{/* TODO: Graph */}
|
|
||||||
<p className="text-black">GRAPH HERE</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
|
<div className="w-full h-full bg-primary-600 rounded-2xl flex flex-col gap-4 p-6">
|
||||||
<div className="w-60 flex flex-col justify-between">
|
<p className="text-3xl">Population Distribution</p>
|
||||||
<p className="text-2xl">Population Distribution</p>
|
<div className="w-full bg-white rounded-xl">
|
||||||
</div>
|
<div className="w-[600px] mx-auto">
|
||||||
<div className="w-full h-full bg-white rounded-xl">
|
<UserTownCouncilDistributionChart />
|
||||||
{/* TODO: Graph */}
|
</div>
|
||||||
<p className="text-black">GRAPH HERE</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export default function UsersManagement() {
|
|||||||
key: "phoneNumber",
|
key: "phoneNumber",
|
||||||
label: "TELEPHONE",
|
label: "TELEPHONE",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "townCouncil",
|
||||||
|
label: "TOWN COUNCIL",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "accountType",
|
key: "accountType",
|
||||||
label: "ACCOUNT TYPE",
|
label: "ACCOUNT TYPE",
|
||||||
Reference in New Issue
Block a user