diff --git a/app/pufflings/family/[id]/child/[childId]/dashboard/page.tsx b/app/pufflings/family/[id]/child/[childId]/dashboard/page.tsx index d84986e..9b64244 100644 --- a/app/pufflings/family/[id]/child/[childId]/dashboard/page.tsx +++ b/app/pufflings/family/[id]/child/[childId]/dashboard/page.tsx @@ -1,5 +1,6 @@ 'use server'; +import FeedChart from '@/app/ui/charts/feedChart'; import DeleteButton from '@/app/ui/deleteButton'; import Recents from '@/app/ui/recents'; import { getChild } from '@/lib/child'; @@ -23,7 +24,7 @@ const Dashboard = async ({ const childInfo = await getChild(parseInt(childId)); return ( - <> +
+
- +
); }; diff --git a/app/ui/charts/feedChart.tsx b/app/ui/charts/feedChart.tsx new file mode 100644 index 0000000..f567aa9 --- /dev/null +++ b/app/ui/charts/feedChart.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { ResponsiveLine, Line } from '@nivo/line'; +import { getFeedChart } from '@/lib/feed'; +import { Radio, RadioGroup } from '@headlessui/react'; +import Loading from '@/app/loading'; +import { Spinner } from '@nextui-org/spinner'; + +const filterOptions = [ + { name: 'Last 7 days', value: 7 }, + { name: 'Last 30 days', value: 30 }, + { name: 'All-time', value: 100000 }, +]; + +const FeedChart = ({ childId }: { childId: string }) => { + const [isLoading, setIsLoading] = useState(true); + const [feedChartData, setFeedChartData] = useState(); + const [filter, setFilter] = useState(filterOptions[2]); + + useEffect(() => { + fetchFeedChart(); + }, [filter]); + + const getFilterDate = () => { + const days = filter.value; + const date = new Date(); + date.setDate(date.getDate() - days); + return date.toISOString(); + }; + + const fetchFeedChart = async () => { + const feedData = await getFeedChart(childId, getFilterDate()); + setFeedChartData(feedData as any); + setIsLoading(false); + }; + + const updateTable = async (filter: any) => { + setIsLoading(true); + setFilter(filter); + }; + + if (!feedChartData) { + return ; + } + + return ( +
+
+
+
+

Feed Chart

+ + {filterOptions.map((option) => ( + + {isLoading && option.name == filter.name ? ( + + ) : ( + option.name + )} + + ))} + +
+
+
+
+ +
+
+ ); +}; + +export default FeedChart; diff --git a/lib/feed.ts b/lib/feed.ts index 2b27482..3bc2c1d 100644 --- a/lib/feed.ts +++ b/lib/feed.ts @@ -152,3 +152,31 @@ export const getLastFeed = async (childId: number) => { take: 1, }); }; + +export const getFeedChart = async (childId: string, filter: string) => { + const results = await prisma.feed.findMany({ + where: { + child_id: parseInt(childId), + start_time: { gte: new Date(filter) }, + }, + orderBy: { + start_time: 'asc', + }, + select: { + start_time: true, + amount: true, + }, + }); + + return [ + { + id: 'feed', + data: results.map((result) => { + return { + x: result.start_time, + y: result.amount?.toNumber(), + }; + }), + }, + ]; +}; diff --git a/package-lock.json b/package-lock.json index 877f28d..b5d0a47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", + "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@nextui-org/dropdown": "^2.1.31", "@nextui-org/system": "^2.2.6", "@nextui-org/theme": "^2.2.11", + "@nivo/bar": "^0.88.0", "@nivo/core": "^0.88.0", "@nivo/line": "^0.88.0", "@prisma/client": "^5.18.0", @@ -1031,6 +1033,59 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@formatjs/ecma402-abstract": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz", @@ -1139,6 +1194,25 @@ "react": ">=16.3" } }, + "node_modules/@headlessui/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", + "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@tanstack/react-virtual": "^3.8.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/@heroicons/react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", @@ -2306,6 +2380,30 @@ "react": ">= 16.14.0 < 19.0.0" } }, + "node_modules/@nivo/bar": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.88.0.tgz", + "integrity": "sha512-wckwuHWeCikxGvvdRfGL+dVFsUD9uHk1r9s7bWUfOD+p8BWhxtYqfXpHolEfgGg3UyPaHtpGA7P4zgE5vgo7gQ==", + "license": "MIT", + "dependencies": { + "@nivo/annotations": "0.88.0", + "@nivo/axes": "0.88.0", + "@nivo/colors": "0.88.0", + "@nivo/core": "0.88.0", + "@nivo/legends": "0.88.0", + "@nivo/scales": "0.88.0", + "@nivo/tooltip": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, "node_modules/@nivo/colors": { "version": "0.88.0", "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.88.0.tgz", @@ -3208,6 +3306,33 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.1.tgz", + "integrity": "sha512-orn2QNe5tF6SqjucHJ6cKTKcRDe3GG7bcYqPNn72Yejj7noECdzgAyRfGt2pGDPemhYim3d1HIR/dgruCnLfUA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.10.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz", + "integrity": "sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -13478,6 +13603,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", diff --git a/package.json b/package.json index 6ca3fe1..f1893b7 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "@fortawesome/free-regular-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", + "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.2.0", "@nextui-org/dropdown": "^2.1.31", "@nextui-org/system": "^2.2.6", "@nextui-org/theme": "^2.2.11", + "@nivo/bar": "^0.88.0", "@nivo/core": "^0.88.0", "@nivo/line": "^0.88.0", "@prisma/client": "^5.18.0",