mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Stats Page
Added stats page, fixed auto init
This commit is contained in:
@@ -100,6 +100,36 @@ router.post("/getLibraryItems", async (req, res) => {
|
||||
console.log(`ENDPOINT CALLED: /getLibraryItems: `);
|
||||
});
|
||||
|
||||
router.get("/getHistory", async (req, res) => {
|
||||
try{
|
||||
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT * FROM jf_playback_activity order by "ActivityDateInserted" desc`
|
||||
);
|
||||
|
||||
const groupedResults = {};
|
||||
rows.forEach(row => {
|
||||
if (groupedResults[row.NowPlayingItemId+row.EpisodeId]) {
|
||||
groupedResults[row.NowPlayingItemId+row.EpisodeId].results.push(row);
|
||||
} else {
|
||||
groupedResults[row.NowPlayingItemId+row.EpisodeId] = {
|
||||
...row,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
res.send(Object.values(groupedResults));
|
||||
|
||||
|
||||
}catch(error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
router.get("/runWatchdog", async (req, res) => {
|
||||
let message='Watchdog Started';
|
||||
if(!process.env.WatchdogRunning )
|
||||
|
||||
@@ -71,7 +71,6 @@ async function initDB()
|
||||
pool.query(sql, (err, res) => {
|
||||
if (err) throw err;
|
||||
console.log('Database and table created');
|
||||
pool.end();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
-- Dumped from database version 15.2 (Debian 15.2-1.pgdg110+1)
|
||||
-- Dumped by pg_dump version 15.1
|
||||
|
||||
-- Started on 2023-03-26 14:21:37 UTC
|
||||
-- Started on 2023-04-01 09:50:04 UTC
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
@@ -19,7 +19,7 @@ SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- TOC entry 251 (class 1255 OID 49412)
|
||||
-- TOC entry 252 (class 1255 OID 49412)
|
||||
-- Name: fs_last_library_activity(text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -57,7 +57,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_last_library_activity(libraryid text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 247 (class 1255 OID 49383)
|
||||
-- TOC entry 248 (class 1255 OID 49383)
|
||||
-- Name: fs_last_user_activity(text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -93,7 +93,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_last_user_activity(userid text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 245 (class 1255 OID 49411)
|
||||
-- TOC entry 246 (class 1255 OID 49411)
|
||||
-- Name: fs_library_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -145,7 +145,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_active_user(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 249 (class 1255 OID 49386)
|
||||
-- TOC entry 250 (class 1255 OID 49386)
|
||||
-- Name: fs_most_played_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -186,7 +186,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_played_items(days integer, itemtype text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 250 (class 1255 OID 49394)
|
||||
-- TOC entry 251 (class 1255 OID 49394)
|
||||
-- Name: fs_most_popular_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -256,7 +256,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_used_clients(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 248 (class 1255 OID 49385)
|
||||
-- TOC entry 249 (class 1255 OID 49385)
|
||||
-- Name: fs_most_viewed_libraries(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -300,7 +300,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_viewed_libraries(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 246 (class 1255 OID 49364)
|
||||
-- TOC entry 247 (class 1255 OID 49364)
|
||||
-- Name: fs_user_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -324,6 +324,32 @@ $$;
|
||||
|
||||
ALTER FUNCTION public.fs_user_stats(hours integer, userid text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 245 (class 1255 OID 49418)
|
||||
-- Name: fs_watch_stats_over_time(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_watch_stats_over_time(days integer) RETURNS TABLE("Date" date, "Count" bigint, "Library" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
|
||||
COUNT(*) AS "Count",
|
||||
l."Name" as "Library"
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
||||
JOIN jf_libraries l ON i."ParentId" = l."Id"
|
||||
WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
|
||||
GROUP BY l."Name", DATE_TRUNC('day', a."ActivityDateInserted")
|
||||
ORDER BY "Date";
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_watch_stats_over_time(days integer) OWNER TO postgres;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
@@ -662,7 +688,7 @@ CREATE VIEW public.js_library_stats_overview AS
|
||||
ALTER TABLE public.js_library_stats_overview OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 3236 (class 2606 OID 16401)
|
||||
-- TOC entry 3237 (class 2606 OID 16401)
|
||||
-- Name: app_config app_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -671,7 +697,7 @@ ALTER TABLE ONLY public.app_config
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3238 (class 2606 OID 16419)
|
||||
-- TOC entry 3239 (class 2606 OID 16419)
|
||||
-- Name: jf_libraries jf_libraries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -680,7 +706,7 @@ ALTER TABLE ONLY public.jf_libraries
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3244 (class 2606 OID 24912)
|
||||
-- TOC entry 3245 (class 2606 OID 24912)
|
||||
-- Name: jf_library_episodes jf_library_episodes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -689,7 +715,7 @@ ALTER TABLE ONLY public.jf_library_episodes
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3240 (class 2606 OID 24605)
|
||||
-- TOC entry 3241 (class 2606 OID 24605)
|
||||
-- Name: jf_library_items jf_library_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -698,7 +724,7 @@ ALTER TABLE ONLY public.jf_library_items
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3242 (class 2606 OID 24737)
|
||||
-- TOC entry 3243 (class 2606 OID 24737)
|
||||
-- Name: jf_library_seasons jf_library_seasons_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -707,7 +733,7 @@ ALTER TABLE ONLY public.jf_library_seasons
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3246 (class 2606 OID 41737)
|
||||
-- TOC entry 3247 (class 2606 OID 41737)
|
||||
-- Name: jf_users jf_users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -716,7 +742,7 @@ ALTER TABLE ONLY public.jf_users
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3390 (class 2618 OID 25163)
|
||||
-- TOC entry 3391 (class 2618 OID 25163)
|
||||
-- Name: jf_library_count_view _RETURN; Type: RULE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -736,7 +762,7 @@ CREATE OR REPLACE VIEW public.jf_library_count_view AS
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3247 (class 2606 OID 24617)
|
||||
-- TOC entry 3248 (class 2606 OID 24617)
|
||||
-- Name: jf_library_items jf_library_items_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -745,15 +771,15 @@ ALTER TABLE ONLY public.jf_library_items
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3398 (class 0 OID 0)
|
||||
-- Dependencies: 3247
|
||||
-- TOC entry 3399 (class 0 OID 0)
|
||||
-- Dependencies: 3248
|
||||
-- Name: CONSTRAINT jf_library_items_fkey ON jf_library_items; Type: COMMENT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items IS 'jf_library';
|
||||
|
||||
|
||||
-- Completed on 2023-03-26 14:21:38 UTC
|
||||
-- Completed on 2023-04-01 09:50:05 UTC
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
|
||||
@@ -20,10 +20,10 @@ app.use('/api', apiRouter); // mount the API router at /api
|
||||
app.use('/sync', syncRouter); // mount the API router at /sync
|
||||
app.use('/stats', statsRouter); // mount the API router at /stats
|
||||
|
||||
app.listen(PORT, () => {
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`Server listening on http://${LISTEN_IP}:${PORT}`);
|
||||
try{
|
||||
db.initDB();
|
||||
await db.initDB();
|
||||
ActivityMonitor.ActivityMonitor(1000);
|
||||
}catch(error)
|
||||
{
|
||||
|
||||
@@ -280,6 +280,47 @@ router.post("/getLibraryLastPlayed", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getViewsOverTime", async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
let _days = days;
|
||||
if (days=== undefined) {
|
||||
_days = 30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_watch_stats_over_time('${_days}')`
|
||||
);
|
||||
|
||||
|
||||
const reorganizedData = {};
|
||||
|
||||
rows.forEach((item) => {
|
||||
const id = item.Library;
|
||||
const count = item.Count;
|
||||
const date = new Date(item.Date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: '2-digit'
|
||||
});
|
||||
|
||||
if (!reorganizedData[id]) {
|
||||
reorganizedData[id] = {
|
||||
id,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
|
||||
reorganizedData[id].data.push({ x: date, y: count });
|
||||
});
|
||||
const finalData = Object.values(reorganizedData);
|
||||
res.send(finalData);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -8,24 +8,26 @@ async function ActivityMonitor(interval) {
|
||||
console.log("Activity Interval: " + interval);
|
||||
|
||||
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
|
||||
|
||||
if(config.length===0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const base_url = config[0].JF_HOST;
|
||||
const apiKey = config[0].JF_API_KEY;
|
||||
|
||||
if (base_url === null || config[0].JF_API_KEY === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
|
||||
|
||||
if(config.length===0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const base_url = config[0].JF_HOST;
|
||||
const apiKey = config[0].JF_API_KEY;
|
||||
|
||||
if (base_url === null || config[0].JF_API_KEY === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${base_url}/Sessions`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
@@ -165,7 +167,7 @@ async function ActivityMonitor(interval) {
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
// console.log(error);
|
||||
return [];
|
||||
}
|
||||
}, interval);
|
||||
|
||||
1169
package-lock.json
generated
1169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,10 @@
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/material": "^5.11.10",
|
||||
"@nivo/api": "^0.74.1",
|
||||
"@nivo/bar": "^0.80.0",
|
||||
"@nivo/core": "^0.80.0",
|
||||
"@nivo/line": "^0.80.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -18,6 +22,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.9.0",
|
||||
"pg-promise": "^11.3.0",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "^18.2.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -23,6 +23,9 @@ body
|
||||
h1{
|
||||
color: white;
|
||||
font-weight: lighter;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import Loading from './pages/components/general/loading';
|
||||
import Setup from './pages/setup';
|
||||
|
||||
|
||||
import Navbar from './pages/components/general/navbar';
|
||||
// import Navbar from './pages/components/general/navbar';
|
||||
import Home from './pages/home';
|
||||
import Settings from './pages/settings';
|
||||
import Users from './pages/users';
|
||||
@@ -24,6 +24,8 @@ import ErrorPage from './pages/components/general/error';
|
||||
|
||||
|
||||
import Testing from './pages/testing';
|
||||
import Activity from './pages/activity';
|
||||
import Statistics from './pages/statistics';
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -71,7 +73,7 @@ if (!config || config.apiKey ==null) {
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Navbar />
|
||||
{/* <Navbar /> */}
|
||||
<div>
|
||||
<main>
|
||||
<Routes>
|
||||
@@ -81,6 +83,8 @@ if (!config || config.apiKey ==null) {
|
||||
<Route path="/users/:UserId" element={<UserInfo />} />
|
||||
<Route path="/libraries" element={<Libraries />} />
|
||||
<Route path="/libraries/:LibraryId" element={<LibraryInfo />} />
|
||||
<Route path="/statistics" element={<Statistics />} />
|
||||
<Route path="/activity" element={<Activity />} />
|
||||
<Route path="/testing" element={<Testing />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -20,24 +20,24 @@ code {
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
body {
|
||||
overflow: auto; /* show scrollbar when needed */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar {
|
||||
body::-webkit-scrollbar {
|
||||
width: 10px; /* set scrollbar width */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-track {
|
||||
body::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* set track color */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb {
|
||||
body::-webkit-scrollbar-thumb {
|
||||
background-color: #8888884d; /* set thumb color */
|
||||
border-radius: 5px; /* round corners */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb:hover {
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #88888883; /* set thumb color */
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import Navbar from './pages/components/general/navbar';
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Navbar/>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import HomeFillIcon from 'remixicon-react/HomeFillIcon';
|
||||
// import FileListFillIcon from 'remixicon-react/FileListFillIcon';
|
||||
// import BarChartFillIcon from 'remixicon-react/BarChartFillIcon';
|
||||
import BarChartFillIcon from 'remixicon-react/BarChartFillIcon';
|
||||
import HistoryFillIcon from 'remixicon-react/HistoryFillIcon';
|
||||
import SettingsFillIcon from 'remixicon-react/SettingsFillIcon';
|
||||
import GalleryFillIcon from 'remixicon-react/GalleryFillIcon';
|
||||
import UserFillIcon from 'remixicon-react/UserFillIcon';
|
||||
|
||||
import ReactjsFillIcon from 'remixicon-react/ReactjsFillIcon';
|
||||
|
||||
// import ReactjsFillIcon from 'remixicon-react/ReactjsFillIcon';
|
||||
|
||||
export const navData = [
|
||||
{
|
||||
@@ -18,27 +20,42 @@ export const navData = [
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
icon: <UserFillIcon />,
|
||||
text: "Users",
|
||||
link: "users"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: <GalleryFillIcon />,
|
||||
text: "Libraries",
|
||||
link: "libraries"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: <UserFillIcon />,
|
||||
text: "Users",
|
||||
link: "users"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: <ReactjsFillIcon />,
|
||||
text: "Component Testing Playground",
|
||||
link: "testing"
|
||||
icon: <HistoryFillIcon />,
|
||||
text: "Activity",
|
||||
link: "activity"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: <BarChartFillIcon />,
|
||||
text: "Statistics",
|
||||
link: "statistics"
|
||||
},
|
||||
|
||||
{
|
||||
id: 6,
|
||||
icon: <SettingsFillIcon />,
|
||||
text: "Settings",
|
||||
link: "settings"
|
||||
}
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
// {
|
||||
// id: 5,
|
||||
// icon: <ReactjsFillIcon />,
|
||||
// text: "Component Testing Playground",
|
||||
// link: "testing"
|
||||
// }
|
||||
// ,
|
||||
@@ -1,75 +1,98 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import API from "../classes/jellyfin-api";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import "./css/activity.css";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
import ActivityTable from "./components/activity/activity-table";
|
||||
import Loading from "./components/general/loading";
|
||||
|
||||
function Activity() {
|
||||
const [data, setData] = useState([]);
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
const [itemCount,setItemCount] = useState(10);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let _api = new API();
|
||||
|
||||
const fetchData = () => {
|
||||
_api.getActivityData(15).then((ActivityData) => {
|
||||
if (data && data.length > 0) {
|
||||
const newDataOnly = ActivityData.Items.filter((item) => {
|
||||
return !data.some((existingItem) => existingItem.Id === item.Id);
|
||||
});
|
||||
setData([
|
||||
...newDataOnly,
|
||||
...data.slice(0, data.length - newDataOnly.length),
|
||||
]);
|
||||
} else {
|
||||
setData(ActivityData.Items);
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
const fetchLibraries = () => {
|
||||
const url = `/api/getHistory`;
|
||||
axios
|
||||
.get(url)
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 60);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data]);
|
||||
}, [data, config]);
|
||||
|
||||
const options = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
||||
if (data.length === 0) {
|
||||
return (<div>
|
||||
<div className="Heading">
|
||||
<h1>Activity</h1>
|
||||
</div>
|
||||
<div className="Activity">
|
||||
<h1>No Activity to display</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="Heading">
|
||||
<h1>Activity</h1>
|
||||
<div className="pagination-range">
|
||||
<div className="header">Items</div>
|
||||
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value);}}>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="Activity">
|
||||
<ul>
|
||||
{data &&
|
||||
data.map((item) => (
|
||||
<li
|
||||
key={item.Id}
|
||||
className={
|
||||
data.findIndex((items) => items.Id === item.Id) <= 15
|
||||
? "new"
|
||||
: "old"
|
||||
}
|
||||
>
|
||||
<div className="ActivityDetail"> {item.Name}</div>
|
||||
<div className="ActivityTime">
|
||||
{new Date(item.Date)
|
||||
.toLocaleString("en-GB", options)
|
||||
.replace(",", "")}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div/>
|
||||
<ActivityTable data={data} itemCount={itemCount}/>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import MPMusic from "./statCards/mp_music";
|
||||
|
||||
import "../css/statCard.css";
|
||||
|
||||
function WatchStatistics() {
|
||||
function HomeStatisticCards() {
|
||||
const [days, setDays] = useState(30);
|
||||
const [input, setInput] = useState(30);
|
||||
|
||||
@@ -64,4 +64,4 @@ function WatchStatistics() {
|
||||
);
|
||||
}
|
||||
|
||||
export default WatchStatistics;
|
||||
export default HomeStatisticCards;
|
||||
139
src/pages/components/activity/activity-table.js
Normal file
139
src/pages/components/activity/activity-table.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import React ,{useState} from 'react';
|
||||
import { Link } from "react-router-dom";
|
||||
// import { useParams } from 'react-router-dom';
|
||||
|
||||
import '../../css/activity/activity-table.css';
|
||||
|
||||
|
||||
|
||||
function ActivityTable(props) {
|
||||
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [data, setData] = useState(props.data);
|
||||
|
||||
function handleSort(key) {
|
||||
const direction =
|
||||
sortConfig.key === key && sortConfig.direction === "ascending"
|
||||
? "descending"
|
||||
: "ascending";
|
||||
setSortConfig({ key, direction });
|
||||
}
|
||||
|
||||
function sortData(data, { key, direction }) {
|
||||
if (!key) return data;
|
||||
|
||||
const sortedData = [...data];
|
||||
|
||||
sortedData.sort((a, b) => {
|
||||
if (a[key] < b[key]) return direction === "ascending" ? -1 : 1;
|
||||
if (a[key] > b[key]) return direction === "ascending" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sortedData;
|
||||
}
|
||||
|
||||
const options = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
|
||||
const indexOfLastUser = currentPage * props.itemCount;
|
||||
const indexOfFirstUser = indexOfLastUser - props.itemCount;
|
||||
const currentData = sortedData.slice(indexOfFirstUser, indexOfLastUser);
|
||||
|
||||
const pageNumbers = [];
|
||||
for (let i = 1; i <= Math.ceil(sortedData.length / props.itemCount); i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
|
||||
const handleCollapse = (itemId) => {
|
||||
setData(data.map(item => {
|
||||
if ((item.NowPlayingItemId+item.EpisodeId) === itemId) {
|
||||
return { ...item, isCollapsed: !item.isCollapsed };
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<div className='activity-table'>
|
||||
<div className='table-headers'>
|
||||
<div onClick={() => handleSort("UserName")}>User</div>
|
||||
<div onClick={() => handleSort("NowPlayingItemName")}>Title </div>
|
||||
<div onClick={() => handleSort("ActivityDateInserted")}>Date</div>
|
||||
<div onClick={() => handleSort("results")}>Total Plays</div>
|
||||
</div>
|
||||
|
||||
{currentData.map((item) => (
|
||||
|
||||
<div className='table-rows' key={item.NowPlayingItemId+item.EpisodeId} onClick={() => handleCollapse(item.NowPlayingItemId+item.EpisodeId)}>
|
||||
<div className='table-rows-content'>
|
||||
<div><Link to={`/users/${item.UserId}`}>{item.UserName}</Link></div>
|
||||
<div>{!item.SeriesName ? item.NowPlayingItemName : item.SeriesName+' - '+ item.NowPlayingItemName}</div>
|
||||
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(item.ActivityDateInserted))}</div>
|
||||
<div>{item.results.length+1}</div>
|
||||
</div>
|
||||
<div className={`sub-table ${item.isCollapsed ? 'collapsed' : ''}`}>
|
||||
{item.results.map((sub_item,index) => (
|
||||
|
||||
<div className='table-rows-content bg-grey sub-row' key={sub_item.EpisodeId+index}>
|
||||
<div><Link to={`/users/${sub_item.UserId}`}>{sub_item.UserName}</Link></div>
|
||||
<div>{!sub_item.SeriesName ? sub_item.NowPlayingItemName : sub_item.SeriesName+' - '+ sub_item.NowPlayingItemName}</div>
|
||||
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(sub_item.ActivityDateInserted))}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{props.itemCount>0 ?
|
||||
|
||||
<div className="pagination">
|
||||
<button className="page-btn" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>
|
||||
First
|
||||
</button>
|
||||
|
||||
<button className="page-btn" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<div className="page-number">{`Page ${currentPage} of ${pageNumbers.length}`}</div>
|
||||
|
||||
<button className="page-btn" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === pageNumbers.length}>
|
||||
Next
|
||||
</button>
|
||||
|
||||
<button className="page-btn" onClick={() => setCurrentPage(pageNumbers.length)} disabled={currentPage === pageNumbers.length}>
|
||||
Last
|
||||
</button>
|
||||
</div>
|
||||
:<></>
|
||||
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ActivityTable;
|
||||
@@ -2,7 +2,8 @@ import { useParams } from 'react-router-dom';
|
||||
|
||||
import LibraryDetails from './library/library-details';
|
||||
import LibraryGlobalStats from './library/library-stats';
|
||||
import LastLibraryPlayed from './library/lastplayed';
|
||||
import LibraryLastPlayed from './library/lastplayed';
|
||||
import RecentlyPlayed from './library/recently-added';
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +16,8 @@ function LibraryInfo() {
|
||||
<div>
|
||||
<LibraryDetails LibraryId={LibraryId}/>
|
||||
<LibraryGlobalStats LibraryId={LibraryId}/>
|
||||
<LastLibraryPlayed LibraryId={LibraryId}/>
|
||||
<RecentlyPlayed LibraryId={LibraryId}/>
|
||||
<LibraryLastPlayed LibraryId={LibraryId}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ function formatTime(time) {
|
||||
}
|
||||
|
||||
|
||||
function LastPlayedItem(props) {
|
||||
function LastWatchedCard(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="last-card">
|
||||
@@ -42,6 +42,7 @@ function LastPlayedItem(props) {
|
||||
props.data.Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
@@ -63,4 +64,4 @@ function LastPlayedItem(props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default LastPlayedItem;
|
||||
export default LastWatchedCard;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, {useState} from "react";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
|
||||
|
||||
|
||||
function RecentlyAddedCard(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="last-card">
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : <Blurhash hash={props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]} width={'100%'} height={'100%'}/>}
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
props.data.Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="last-item-details">
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecentlyAddedCard;
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import LastPlayedItem from "./lastplayed/last-played-item";
|
||||
import ItemCardInfo from "./LastWatched/last-watched-card";
|
||||
|
||||
import Config from "../../../lib/config";
|
||||
import "../../css/users/user-details.css";
|
||||
|
||||
function LastLibraryPlayed(props) {
|
||||
function LibraryLastPlayed(props) {
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
|
||||
@@ -56,7 +56,7 @@ function LastLibraryPlayed(props) {
|
||||
<h1>Last Watched</h1>
|
||||
<div className="last-played-container">
|
||||
{data.map((item) => (
|
||||
<LastPlayedItem data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
|
||||
<ItemCardInfo data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
@@ -65,4 +65,4 @@ function LastLibraryPlayed(props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default LastLibraryPlayed;
|
||||
export default LibraryLastPlayed;
|
||||
|
||||
72
src/pages/components/library/recently-added.js
Normal file
72
src/pages/components/library/recently-added.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import RecentlyAddedCard from "./RecentlyAdded/recently-added-card";
|
||||
|
||||
import Config from "../../../lib/config";
|
||||
import "../../css/users/user-details.css";
|
||||
|
||||
function RecentlyPlayed(props) {
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let url=`${config.hostUrl}/users/5f63950a2339462196eb8cead70cae7e/Items/latest?parentId=${props.LibraryId}`;
|
||||
const itemData = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
console.log(itemData);
|
||||
setData(itemData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data,config, props.LibraryId]);
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (!data || !config) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="last-played">
|
||||
<h1>Recently Added</h1>
|
||||
<div className="last-played-container">
|
||||
{data.filter((item) => ["Series", "Movie","Audio"].includes(item.Type)).map((item) => (
|
||||
<RecentlyAddedCard data={item} base_url={config.hostUrl} key={item.Id}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecentlyPlayed;
|
||||
@@ -108,7 +108,7 @@ function sessionCard(props) {
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id) +
|
||||
"/Images/Backdrop?fillHeight=320&fillWidth=213&quality=50"
|
||||
"/Images/Backdrop?fillHeight=320&fillWidth=213&quality=80"
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
@@ -121,7 +121,7 @@ function sessionCard(props) {
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
@@ -15,6 +15,7 @@ function ItemImage(props) {
|
||||
(props.data.Id) +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ function StatComponent(props) {
|
||||
<div className="stats-list">
|
||||
{props.data &&
|
||||
props.data.map((item, index) => (
|
||||
<div className="stat-item" key={item.Id}>
|
||||
<div className="stat-item" key={item.Id || index}>
|
||||
<p className="stat-item-index">{index + 1}</p>
|
||||
{item.UserId ?
|
||||
<p className="stat-item-name"> <Link to={`/users/${item.UserId}`}> {item.Name}</Link> </p>
|
||||
|
||||
154
src/pages/components/statistics/daily-play-count.js
Normal file
154
src/pages/components/statistics/daily-play-count.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import React,{useState,useEffect} from 'react';
|
||||
import axios from 'axios';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
|
||||
import '../../css/stats.css';
|
||||
|
||||
function DailyPlayStats(props) {
|
||||
const [data, setData] = useState();
|
||||
const [days, setDays] = useState(60);
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const fetchLibraries = () => {
|
||||
|
||||
const url = `/stats/getViewsOverTime`;
|
||||
|
||||
axios
|
||||
.post(url, {days:days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (!data) {
|
||||
fetchLibraries();
|
||||
}
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
if (data.length === 0) {
|
||||
return (<div className="statistics-widget">
|
||||
|
||||
<h1>Daily Play Count Per Library - {days} Days</h1>
|
||||
|
||||
|
||||
<h1>No Stats to display</h1>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="statistics-widget" >
|
||||
<h1>Daily Play Count Per Library - {days} Days</h1>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
margin={{ top: 50, right: 100, bottom: 150, left: 50 }}
|
||||
xScale={{ type: 'point' }}
|
||||
yScale={{
|
||||
type: 'linear',
|
||||
min: 'auto',
|
||||
max: 'auto',
|
||||
stacked: false,
|
||||
reverse: false
|
||||
}}
|
||||
enableGridX={false}
|
||||
enableSlices={"x"}
|
||||
yFormat=" >-.0f"
|
||||
curve="natural"
|
||||
|
||||
axisBottom={{
|
||||
orient: 'bottom',
|
||||
tickSize: 5,
|
||||
tickPadding: 10,
|
||||
tickRotation: 0,
|
||||
legend: 'Days',
|
||||
legendOffset: 36,
|
||||
legendPosition: 'middle',
|
||||
itemTextColor: '#fff'
|
||||
}}
|
||||
axisLeft={{
|
||||
orient: 'left',
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
tickRotation: 0,
|
||||
legend: 'Views',
|
||||
legendOffset: -40,
|
||||
legendPosition: 'middle',
|
||||
itemTextColor: '#fff',
|
||||
|
||||
}}
|
||||
|
||||
colors={{ scheme: 'category10' }}
|
||||
pointSize={10}
|
||||
pointColor={{ theme: 'background' }}
|
||||
pointBorderWidth={2}
|
||||
pointBorderColor={{ from: 'serieColor' }}
|
||||
pointLabelYOffset={-12}
|
||||
useMesh={true}
|
||||
legends={[
|
||||
{
|
||||
|
||||
itemTextColor: '#fff',
|
||||
anchor: 'bottom',
|
||||
direction: 'row',
|
||||
justify: false,
|
||||
translateX: 0,
|
||||
translateY: 80,
|
||||
itemsSpacing: 0,
|
||||
itemDirection: 'left-to-right',
|
||||
itemWidth: 80,
|
||||
itemHeight: 20,
|
||||
itemOpacity: 0.75,
|
||||
symbolSize: 12,
|
||||
symbolShape: 'circle',
|
||||
symbolBorderColor: 'rgba(0, 0, 0, .5)',
|
||||
effects: [
|
||||
{
|
||||
on: 'hover',
|
||||
style: {
|
||||
itemBackground: 'rgba(0, 0, 0, .03)',
|
||||
itemOpacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default DailyPlayStats;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ function LastPlayedItem(props) {
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
onLoad={() => setLoaded(true)}
|
||||
alt=""
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
98
src/pages/css/activity/activity-table.css
Normal file
98
src/pages/css/activity/activity-table.css
Normal file
@@ -0,0 +1,98 @@
|
||||
div a
|
||||
{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-rows:hover
|
||||
{
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.table-rows-content:hover a
|
||||
{
|
||||
color: #00A4DC;
|
||||
}
|
||||
|
||||
.activity-table
|
||||
{
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
}
|
||||
|
||||
.table-rows-content{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.table-headers div {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-bottom: 1px solid transparent;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-headers div:hover {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.table-headers, .table-rows-content
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* border-bottom: 1px solid rgba(255, 255, 255, 0.05); */
|
||||
}
|
||||
|
||||
.table-headers div,
|
||||
.table-rows-content div
|
||||
{
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table-headers div:last-child,
|
||||
.table-rows-content div:last-child
|
||||
{
|
||||
border-right: none;
|
||||
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
overflow: hidden;
|
||||
max-height: 0; /* set the height to 0 to collapse the div */
|
||||
opacity:0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 100;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.sub-row{
|
||||
color: darkgray;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sub-row a{
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
.sub-row a:hover{
|
||||
color: #00A4DC;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sub-row:last-child
|
||||
{
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.bg-grey
|
||||
{
|
||||
background-color: rgb(100, 100, 100,0.2);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
min-height: 350px;
|
||||
min-height: 300px;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||
|
||||
}
|
||||
|
||||
/* .library-banner-image
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
/* width: 800px; */
|
||||
|
||||
margin-bottom: 40px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
background-color: #5a2da5;
|
||||
/* background: linear-gradient(to right, #AA5CC3,#00A4DC); */
|
||||
height: 50px;
|
||||
/* position: sticky;
|
||||
top: 0; */
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
.sessions-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(520px, 520px));
|
||||
grid-auto-rows: 235px;/* max-width+offset so 215 + 20*/
|
||||
grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/
|
||||
margin-right: 20px;
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
background-color: grey;
|
||||
/* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */
|
||||
|
||||
max-height: 215px;
|
||||
max-height: 180px;
|
||||
max-width: 500px;
|
||||
|
||||
/* margin-left: 20px; */
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
.Heading h1
|
||||
{
|
||||
padding-right: 10px;
|
||||
margin: 0;
|
||||
/* margin: 0; */
|
||||
}
|
||||
|
||||
.stat-cards-container
|
||||
@@ -141,6 +141,7 @@
|
||||
font-size: 1.2em;
|
||||
align-self: flex-end;
|
||||
justify-content: space-evenly;
|
||||
margin-bottom: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
src/pages/css/stats.css
Normal file
12
src/pages/css/stats.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.statistics-widget
|
||||
{
|
||||
|
||||
height: 700px;
|
||||
color:black !important;
|
||||
background-color:rgba(0,0,0,0.4);
|
||||
padding:20px;
|
||||
margin:20px;
|
||||
border-radius:4px;
|
||||
/* text-align: center; */
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
{
|
||||
padding: 15px 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
td a{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
@@ -37,8 +39,11 @@ td:hover a{
|
||||
color: rgb(0, 164, 219);
|
||||
}
|
||||
|
||||
|
||||
|
||||
th {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -121,6 +126,7 @@ td:first-child {
|
||||
font-size: 1.2em;
|
||||
align-self: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.9em;
|
||||
}
|
||||
|
||||
.pagination-range select
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import './css/home.css'
|
||||
|
||||
import Sessions from './components/sessions/sessions'
|
||||
import WatchStatistics from './components/WatchStatistics'
|
||||
import HomeStatisticCards from './components/HomeStatisticCards'
|
||||
import LibraryOverView from './components/libraryOverview'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Home() {
|
||||
<div>
|
||||
|
||||
<Sessions />
|
||||
<WatchStatistics/>
|
||||
<HomeStatisticCards/>
|
||||
<LibraryOverView/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@ import LibraryCard from "./components/library/library-card";
|
||||
|
||||
function Libraries() {
|
||||
const [data, setData] = useState();
|
||||
// const [items, setItems] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
8
src/pages/line-chart.js
Normal file
8
src/pages/line-chart.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { LineChart, Line } from 'recharts';
|
||||
const data = [{name: 'Page A', uv: 400, pv: 2400, amt: 2400}, ...];
|
||||
|
||||
const renderLineChart = (
|
||||
<LineChart width={400} height={400} data={data}>
|
||||
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
|
||||
</LineChart>
|
||||
);
|
||||
19
src/pages/statistics.js
Normal file
19
src/pages/statistics.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React,{useState} from 'react';
|
||||
|
||||
// import './css/library/libraries.css';
|
||||
import DailyPlayStats from './components/statistics/daily-play-count';
|
||||
|
||||
function Statistics(props) {
|
||||
|
||||
const [days, setDays] = useState(60);
|
||||
|
||||
|
||||
return (
|
||||
<DailyPlayStats days={days}/>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Statistics;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user