diff --git a/lang/de.slf b/lang/de.slf
index 41b41b50d8079b7e297ecfbbc4a46a5b4cbeef27..f0c49fdb208192816aad7fa465903acfbec8c5c2 100644
--- a/lang/de.slf
+++ b/lang/de.slf
@@ -12,6 +12,11 @@ enum.automatic_media_process_scheduler_state.enabled = "Eingeschaltet"
 enum.automatic_media_process_scheduler_state.disabled = "Ausgeschaltet"
 enum.automatic_media_process_scheduler_state.inherit = "Erben"
 
+enum.client_stat_type.device_family = "Gerätefamilie"
+enum.client_stat_type.device_brand = "Gerätemarke"
+enum.client_stat_type.operating_system = "Betriebssystem"
+enum.client_stat_type.browser = "Browser"
+
 object.announcement = "Ankündigung"
 object.announcement.type = "Typ"
 object.announcement.page_visibility = "Seitensichtbarkeit"
diff --git a/lang/en.slf b/lang/en.slf
index ba00f9c062bf2ca206aacfa1dbe4b7f47c09bfb5..d0ddea3e7e74ddf222e95b23d4a99bf8be58f533 100644
--- a/lang/en.slf
+++ b/lang/en.slf
@@ -11,6 +11,11 @@ enum.automatic_media_process_scheduler_state.enabled = "Enabled"
 enum.automatic_media_process_scheduler_state.disabled = "Disabled"
 enum.automatic_media_process_scheduler_state.inherit = "Inherit"
 
+enum.client_stat_type.device_family = "Device Family"
+enum.client_stat_type.device_brand = "Device Brand"
+enum.client_stat_type.operating_system = "Operating System"
+enum.client_stat_type.browser = "Browser"
+
 object.announcement = "Announcement"
 object.announcement.type = "Type"
 object.announcement.page_visibility = "Page Visibility"
diff --git a/package-lock.json b/package-lock.json
index 2a4c2277e35810b23b2bdc8f2aa2580c581b1d81..8738ac98d63fafb64e346ac204a03ffe12ee745a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "luxon": "^3.6.1",
         "next": "^15.3.0",
         "react": "^19.1.0",
+        "react-apexcharts": "^1.7.0",
         "react-bootstrap": "^2.10.9",
         "react-dom": "^19.1.0",
         "react-markdown": "^10.1.0",
@@ -1017,6 +1018,67 @@
         "video.js": ">=6.0.0"
       }
     },
+    "node_modules/@svgdotjs/svg.draggable.js": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
+      "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@svgdotjs/svg.js": "^3.2.4"
+      }
+    },
+    "node_modules/@svgdotjs/svg.filter.js": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
+      "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@svgdotjs/svg.js": "^3.2.4"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/@svgdotjs/svg.js": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
+      "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
+      "license": "MIT",
+      "peer": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Fuzzyma"
+      }
+    },
+    "node_modules/@svgdotjs/svg.resize.js": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
+      "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">= 14.18"
+      },
+      "peerDependencies": {
+        "@svgdotjs/svg.js": "^3.2.4",
+        "@svgdotjs/svg.select.js": "^4.0.1"
+      }
+    },
+    "node_modules/@svgdotjs/svg.select.js": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
+      "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">= 14.18"
+      },
+      "peerDependencies": {
+        "@svgdotjs/svg.js": "^3.2.4"
+      }
+    },
     "node_modules/@swc/counter": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -1449,6 +1511,13 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@yr/monotone-cubic-spline": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
+      "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/abbrev": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -1550,6 +1619,21 @@
         "node": ">= 8"
       }
     },
+    "node_modules/apexcharts": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz",
+      "integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@svgdotjs/svg.draggable.js": "^3.0.4",
+        "@svgdotjs/svg.filter.js": "^3.0.8",
+        "@svgdotjs/svg.js": "^3.2.4",
+        "@svgdotjs/svg.resize.js": "^2.0.2",
+        "@svgdotjs/svg.select.js": "^4.0.1",
+        "@yr/monotone-cubic-spline": "^1.0.3"
+      }
+    },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -6323,6 +6407,19 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/react-apexcharts": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.7.0.tgz",
+      "integrity": "sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==",
+      "license": "MIT",
+      "dependencies": {
+        "prop-types": "^15.8.1"
+      },
+      "peerDependencies": {
+        "apexcharts": ">=4.0.0",
+        "react": ">=0.13"
+      }
+    },
     "node_modules/react-bootstrap": {
       "version": "2.10.9",
       "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.9.tgz",
diff --git a/package.json b/package.json
index 2ebe6c543337dfb00787e51b58d29f902406ee85..9229dfbca9522bf1790026ed25153b8bb4455771 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,8 @@
     "remark-gfm": "^4.0.1",
     "sass": "1.77.6",
     "video.js": "^8.22.0",
-    "videojs-hotkeys": "^0.2.30"
+    "videojs-hotkeys": "^0.2.30",
+    "react-apexcharts": "^1.7.0"
   },
   "devDependencies": {
     "@next/bundle-analyzer": "15.3.0",
diff --git a/src/videoag/api/Backend.tsx b/src/videoag/api/Backend.tsx
index 30bb55976a95e074736b6f5104fb6156a4c2d43f..a07c1ccb83c8c56f1ded852c36fa2933237670e1 100644
--- a/src/videoag/api/Backend.tsx
+++ b/src/videoag/api/Backend.tsx
@@ -41,6 +41,9 @@ import type {
     UpdateMySettingsRequest,
     GetLectureResponse,
     LogWatchedPublishMediumRequest,
+    GetPublishMediumStatsResponse,
+    GetLectureStatsResponse,
+    GetCourseStatsResponse,
 } from "./types";
 /* global RequestInit */
 
@@ -523,6 +526,27 @@ export class BackendImpl {
         );
     }
 
+    // GET /stats/publish_medium/{publish_medium_id}
+    getPublishMediumStats(publishMediumId: int): Promise<GetPublishMediumStatsResponse> {
+        return this.processResponse<GetPublishMediumStatsResponse>(
+            this.fetch(`${this.baseUrl()}/stats/publish_medium/${publishMediumId}`),
+        );
+    }
+
+    // GET /stats/lecture/{lecture_id}
+    getLectureStats(lectureId: int): Promise<GetLectureStatsResponse> {
+        return this.processResponse<GetLectureStatsResponse>(
+            this.fetch(`${this.baseUrl()}/stats/lecture/${lectureId}`),
+        );
+    }
+
+    // GET /stats/course/{course_id}
+    getCourseStats(courseId: int): Promise<GetCourseStatsResponse> {
+        return this.processResponse<GetCourseStatsResponse>(
+            this.fetch(`${this.baseUrl()}/stats/course/${courseId}`),
+        );
+    }
+
     // Special method for language files
     getLanguageFile(lang: string): Promise<Record<string, string>> {
         return this.fetch(`${this.staticUrl()}/lang/${lang}.json`)
diff --git a/src/videoag/api/types.ts b/src/videoag/api/types.ts
index 8862e113464d54c9a11196a542722e22566d63df..1822adfe91f542776128355c767cc9365be7d0a5 100644
--- a/src/videoag/api/types.ts
+++ b/src/videoag/api/types.ts
@@ -1,5 +1,6 @@
 // Base types
 export type int = number;
+export type float = number;
 export type long = number;
 export type handle = string;
 export type semester_string = string;
@@ -614,3 +615,48 @@ export interface LogWatchedPublishMediumRequest {
     watch_start_time_sec: int;
 }
 // No Response Interface
+
+/// GET /stats/publish_medium/{publish_medium_id}
+// No Request Interface
+export interface GetPublishMediumStatsResponse {
+    client_stats: {
+        [key: string]: {
+            [key: string]: int;
+        };
+    };
+    segmented_stats: {
+        segment_duration_sec: int;
+        segment_total_view_counts: int[];
+        segment_unique_view_counts: int[];
+    };
+    total_watched_seconds: int;
+    view_count: int;
+}
+
+/// GET /stats/lecture/{lecture_id}
+// No Request Interface
+export interface GetLectureStatsResponse {
+    average_watch_speed: float;
+    daily_views: {
+        dates: string[];
+        view_counts: int[];
+    };
+    total_watched_seconds: int;
+    view_count: int;
+}
+
+/// GET /stats/course/{course_id}
+// No Request Interface
+export interface GetCourseStatsResponse {
+    average_watch_speed: float;
+    daily_views: {
+        dates: string[];
+        view_counts: int[];
+    };
+    lecture_views: {
+        lecture_ids: int[];
+        view_counts: int[];
+    };
+    total_watched_seconds: int;
+    view_count: int;
+}
diff --git a/src/videoag/course/CourseListing.tsx b/src/videoag/course/CourseListing.tsx
index bfffb0b4479c50ef2603382c91ca7ec896c060bc..bd97300c64ac43f7bd40bd8dc99ac77392365f63 100644
--- a/src/videoag/course/CourseListing.tsx
+++ b/src/videoag/course/CourseListing.tsx
@@ -15,6 +15,7 @@ import {
 
 import { LectureListItem } from "./Lecture";
 import DownloadAllModal from "./DownloadManager";
+import { CourseStats } from "./Stats";
 
 function ListingHeader({ course }: { course: GetCourseResponse }) {
     const authStatus = useAuthStatus();
@@ -192,11 +193,15 @@ export default function CourseListing({
     courseData: GetCourseResponse;
     disabled: boolean;
 }) {
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
+
     return (
         <>
             <Title title={courseData.full_name} />
 
             <ListingHeader course={courseData} />
+            {hasUserInfo && <CourseStats course={courseData} />}
             <ListingBody course={courseData} />
             <UpdateOverlay show={disabled} />
         </>
diff --git a/src/videoag/course/Player.tsx b/src/videoag/course/Player.tsx
index 0fb1c9279c3f441d50fd60799c4ac3e92f62056c..9e15197e2bb55745182c7392ece5acb7861f4bda 100644
--- a/src/videoag/course/Player.tsx
+++ b/src/videoag/course/Player.tsx
@@ -12,7 +12,7 @@ import VideoJsMarkers from "#src/videojs-markers";
 
 import type { course, chapter, publish_medium, lecture } from "@/api/types";
 import { useApi } from "@/api";
-import { ViewPermissionAuthorization } from "@/authentication";
+import { ViewPermissionAuthorization, useAuthStatus } from "@/authentication";
 import { useLanguage } from "@/localization";
 import { Card, Title, UpdateOverlay, showInfoToast, parseApiDatetime } from "@/miscellaneous";
 import {
@@ -32,6 +32,7 @@ import {
 } from "./Medium";
 import { Chapters, AddChapterButton } from "./Chapter";
 import { StatTracker } from "./StatTracker";
+import { LectureStats } from "./Stats";
 
 import { PlayerData } from "#src/pages/dynamic_player";
 
@@ -406,6 +407,8 @@ export default function Player({
     disabled: boolean;
 }) {
     const { language } = useLanguage();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
 
     const { course, lecture } = playerData;
 
@@ -466,6 +469,7 @@ export default function Player({
             </Head>
             <Title title={`${course.short_name} - ${lecture.title}`} />
             <UpdateOverlay show={disabled} />
+            {hasUserInfo && <LectureStats lecture={lecture} />}
             <Card
                 objectVisible={lecture.visible}
                 header={
diff --git a/src/videoag/course/Stats.tsx b/src/videoag/course/Stats.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8e1c0c4923b11f355b932ceccfd899c1f3003f99
--- /dev/null
+++ b/src/videoag/course/Stats.tsx
@@ -0,0 +1,390 @@
+import { Suspense, useEffect, useState } from "react";
+import React from "react";
+
+import type {
+    lecture,
+    course,
+    int,
+    GetCourseStatsResponse,
+    GetLectureStatsResponse,
+    GetPublishMediumStatsResponse,
+} from "@/api/types";
+import { useApi } from "@/api";
+import { useLanguage } from "@/localization";
+import {
+    Card,
+    useMutexCall,
+    useMutexProcessing,
+    Spinner,
+    useTheme,
+    timestampToString,
+    datetimeToString,
+    ReloadBoundary,
+    CollapsableCard,
+} from "@/miscellaneous";
+import { ErrorComponent } from "@/error";
+
+import { getNamedPublishMedia, getSortedPlayerPublishMedia } from "./Medium";
+
+// Lazy since the charts try to use the 'window' and this causes errors in server-side rendering
+const Chart = React.lazy(() => import("react-apexcharts"));
+
+export function GraphImp({
+    id,
+    title,
+    type,
+    dataX,
+    dataY,
+    height,
+}: {
+    id: string;
+    title: string;
+    type: "line" | "bar";
+    dataX: any[];
+    dataY: any[];
+    height?: any;
+}) {
+    const { theme } = useTheme();
+    return (
+        <Chart
+            options={{
+                chart: {
+                    id: id,
+                    background: "#00000000", // transparent
+                    zoom: {
+                        allowMouseWheelZoom: false,
+                    },
+                },
+                xaxis: {
+                    categories: dataX,
+                    title: {
+                        text: title,
+                    },
+                },
+                theme: {
+                    mode: theme,
+                },
+            }}
+            series={[
+                {
+                    name: title,
+                    data: dataY,
+                    color: "#337ab7",
+                },
+            ]}
+            type={type}
+            height={height ?? "500em"}
+        />
+    );
+}
+
+export function LectureStatsImpl({ lecture }: { lecture: lecture }) {
+    const api = useApi();
+    const { language } = useLanguage();
+    const { theme } = useTheme();
+
+    const [lectureStats, setLectureStats] = useState<GetLectureStatsResponse | undefined>(
+        undefined,
+    );
+    const [selectedMediumId, setSelectedMediumId] = useState<int | undefined>(undefined);
+    const [mediumStats, setMediumStats] = useState<GetPublishMediumStatsResponse | undefined>(
+        undefined,
+    );
+    const [error, setError] = useState<any>(undefined);
+
+    const lectureId = lecture.id;
+
+    const reloadData = useMutexCall(() =>
+        api
+            .getLectureStats(lectureId)
+            .then((res) => {
+                setLectureStats(res);
+                setError(undefined);
+            })
+            .catch((e) => {
+                console.log("Error while loading lecture stats", e);
+                setError(e);
+            }),
+    );
+
+    const loadMediumStats = useMutexProcessing<int, void>((mediumId) =>
+        api
+            .getPublishMediumStats(mediumId)
+            .then((res) => {
+                setMediumStats(res);
+                setError(undefined);
+            })
+            .catch((e) => {
+                console.log("Error while loading publish medium stats", e);
+                setError(e);
+            }),
+    );
+
+    const namedMedia = getNamedPublishMedia(
+        getSortedPlayerPublishMedia(lecture.publish_media ?? []),
+    );
+
+    useEffect(() => {
+        setMediumStats(undefined);
+        if (selectedMediumId !== undefined) {
+            loadMediumStats(selectedMediumId);
+        }
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [selectedMediumId]);
+
+    useEffect(() => {
+        setSelectedMediumId(namedMedia.length > 0 ? namedMedia[0].medium.id : undefined);
+        reloadData();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [lectureId]);
+
+    if (error !== undefined) {
+        return (
+            <ReloadBoundary reloadFunc={reloadData}>
+                <ErrorComponent error={error} objectName="Lecture Statistics" showButtons={false} />
+            </ReloadBoundary>
+        );
+    }
+
+    if (lectureStats === undefined) {
+        return <Spinner visible={true} />;
+    }
+
+    return (
+        <div>
+            <table className="table" style={{ maxWidth: "20em" }}>
+                <tbody>
+                    <tr>
+                        <td>View Count</td>
+                        <td>{lectureStats.view_count}</td>
+                    </tr>
+                    <tr>
+                        <td>Total Watch Time</td>
+                        <td>{timestampToString(lectureStats.total_watched_seconds)}</td>
+                    </tr>
+                    <tr>
+                        <td>Average Watch Speed</td>
+                        <td>{Math.round(lectureStats.average_watch_speed * 100)}%</td>
+                    </tr>
+                </tbody>
+            </table>
+            <GraphImp
+                id="lectureDailyViews"
+                title="Daily Views"
+                type="line"
+                dataX={lectureStats.daily_views.dates}
+                dataY={lectureStats.daily_views.view_counts}
+            />
+            <hr />
+            <Card
+                header={
+                    <>
+                        Medium Statistics
+                        <select
+                            className="form-select w-auto ms-2"
+                            value={selectedMediumId}
+                            onChange={(e) => setSelectedMediumId(parseInt(e.target.value))}
+                        >
+                            {namedMedia.map(({ name, medium }) => (
+                                <option key={medium.id} value={medium.id}>
+                                    {name}
+                                </option>
+                            ))}
+                        </select>
+                    </>
+                }
+            >
+                {mediumStats && (
+                    <>
+                        <Chart
+                            options={{
+                                chart: {
+                                    id: "mediumViewTimeline",
+                                    background: "#00000000", // transparent
+                                    zoom: {
+                                        allowMouseWheelZoom: false,
+                                    },
+                                },
+                                xaxis: {
+                                    type: "numeric",
+                                    tickAmount: 10,
+                                    title: {
+                                        text: "Medium Timeline",
+                                    },
+                                    labels: {
+                                        formatter: (val: int) => timestampToString(val),
+                                    },
+                                },
+                                dataLabels: {
+                                    enabled: false,
+                                },
+                                theme: {
+                                    mode: theme,
+                                },
+                                stroke: {
+                                    curve: "smooth",
+                                },
+                            }}
+                            series={[
+                                {
+                                    name: "Unique Views",
+                                    data: mediumStats.segmented_stats.segment_unique_view_counts.map(
+                                        (count, index) => [
+                                            index *
+                                                mediumStats.segmented_stats.segment_duration_sec,
+                                            count,
+                                        ],
+                                    ),
+                                    color: "#337ab7",
+                                },
+                                {
+                                    name: "Total Views",
+                                    data: mediumStats.segmented_stats.segment_total_view_counts.map(
+                                        (count, index) => [
+                                            index *
+                                                mediumStats.segmented_stats.segment_duration_sec,
+                                            count,
+                                        ],
+                                    ),
+                                    color: "#ffffff",
+                                },
+                            ]}
+                            type="area"
+                            height="500em"
+                        />
+                        <div className="row">
+                            {Object.entries(mediumStats.client_stats).map(
+                                ([typeName, clientStats]) => {
+                                    const valuesWithCounts = Object.entries(clientStats).sort(
+                                        ([value, count]) => count,
+                                    );
+                                    return (
+                                        <div
+                                            key={typeName}
+                                            className="col-12 col-sm-6 col-md-4 col-lg-3"
+                                        >
+                                            <GraphImp
+                                                id={"clientStats" + typeName}
+                                                title={language.get(
+                                                    `enum.client_stat_type.${typeName}`,
+                                                )}
+                                                type="bar"
+                                                dataX={valuesWithCounts.map(
+                                                    ([value, count]) => value,
+                                                )}
+                                                dataY={valuesWithCounts.map(
+                                                    ([value, count]) => count,
+                                                )}
+                                                height={"250em"}
+                                            />
+                                        </div>
+                                    );
+                                },
+                            )}
+                        </div>
+                    </>
+                )}
+            </Card>
+        </div>
+    );
+}
+
+export function LectureStats({ lecture }: { lecture: lecture }) {
+    return (
+        <CollapsableCard header="Statistics" defaultVisible={false}>
+            <LectureStatsImpl lecture={lecture} />
+        </CollapsableCard>
+    );
+}
+
+export function CourseStatsImpl({ course }: { course: course }) {
+    const api = useApi();
+    const [courseStats, setCourseStats] = useState<GetCourseStatsResponse | undefined>(undefined);
+    const [error, setError] = useState<any>(undefined);
+
+    const courseId = course.id;
+
+    const reloadData = useMutexCall(() =>
+        api
+            .getCourseStats(courseId)
+            .then((res) => {
+                setCourseStats(res);
+                setError(undefined);
+            })
+            .catch((e) => {
+                console.log("Error while loading course stats", e);
+                setError(e);
+            }),
+    );
+
+    useEffect(() => {
+        reloadData();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [courseId]);
+
+    let comp = null;
+    if (error !== undefined) {
+        comp = <ErrorComponent error={error} objectName="Course Statistics" showButtons={false} />;
+    } else if (courseStats === undefined) {
+        comp = <Spinner visible={true} />;
+    } else {
+        comp = (
+            <>
+                <table className="table" style={{ maxWidth: "20em" }}>
+                    <tbody>
+                        <tr>
+                            <td>View Count</td>
+                            <td>{courseStats.view_count}</td>
+                        </tr>
+                        <tr>
+                            <td>Total Watch Time</td>
+                            <td>{timestampToString(courseStats.total_watched_seconds)}</td>
+                        </tr>
+                        <tr>
+                            <td>Average Watch Speed</td>
+                            <td>{Math.round(courseStats.average_watch_speed * 100)}%</td>
+                        </tr>
+                    </tbody>
+                </table>
+                <div className="col-12 col-lg-6 d-inline-block">
+                    <GraphImp
+                        id="courseDailyViews"
+                        title="Daily Views"
+                        type="line"
+                        dataX={courseStats.daily_views.dates}
+                        dataY={courseStats.daily_views.view_counts}
+                    />
+                </div>
+                <div className="col-12 col-lg-6 d-inline-block">
+                    <GraphImp
+                        id="lectureViewCounts"
+                        title="Lecture Views"
+                        type="bar"
+                        dataX={courseStats.lecture_views.lecture_ids.map(
+                            /* Not the most efficient way... */
+                            (lecId: int) =>
+                                datetimeToString(
+                                    course.lectures!.find((lec) => lec.id == lecId)!.time,
+                                ),
+                        )}
+                        dataY={courseStats.lecture_views.view_counts}
+                    />
+                </div>
+            </>
+        );
+    }
+
+    return (
+        <ReloadBoundary reloadFunc={reloadData}>
+            <Suspense fallback={<Spinner visible={true} />}>{comp}</Suspense>
+        </ReloadBoundary>
+    );
+}
+
+export function CourseStats({ course }: { course: course }) {
+    return (
+        <CollapsableCard header="Statistics" defaultVisible={false}>
+            <CourseStatsImpl course={course} />
+        </CollapsableCard>
+    );
+}