Comics — Creator Dashboard (views · subs · revenue)
A webcomic creator dashboard for the fictional studio Inkwell, built on a comic-ink design system in Bangers and Inter. Four KPI stat cards animate a count-up for total views, subscribers, monthly revenue, and average rating, each with a trend delta. A hand-drawn SVG line chart of views over time flips between 7-day, 30-day, and 90-day ranges, a sortable top-episodes table ranks by views, likes, or revenue, and a recent-activity feed and toast helper round out a fully interactive vanilla-JS admin view.
MCP
Codice
:root {
--ink: #0e0e12;
--ink-2: #23232b;
--paper: #fdfcf7;
--panel: #ffffff;
--accent: #ff2e4d;
--accent-2: #ffd23f;
--accent-blue: #2e6bff;
--muted: #6b6b78;
--line: rgba(14, 14, 18, 0.14);
--line-2: rgba(14, 14, 18, 0.28);
--halftone: radial-gradient(circle, rgba(14, 14, 18, 0.18) 1px, transparent 1.6px);
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow: 4px 4px 0 var(--ink);
--shadow-sm: 3px 3px 0 var(--ink);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background-color: var(--paper);
background-image: var(--halftone);
background-size: 6px 6px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
z-index: 50;
background: var(--accent-2);
color: var(--ink);
border: 2px solid var(--ink);
padding: 8px 14px;
font-weight: 700;
border-radius: var(--r-sm);
}
.skip-link:focus {
left: 8px;
}
/* Layout shell */
.shell {
display: grid;
grid-template-columns: 248px 1fr;
min-height: 100vh;
max-width: 1320px;
margin: 0 auto;
}
/* Sidebar */
.sidebar {
background: var(--ink);
color: var(--paper);
border-right: 3px solid var(--ink);
padding: 22px 18px;
display: flex;
flex-direction: column;
gap: 26px;
position: sticky;
top: 0;
align-self: start;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-mark {
display: grid;
place-items: center;
width: 38px;
height: 38px;
background: var(--accent-2);
color: var(--ink);
border: 2px solid var(--paper);
border-radius: var(--r-sm);
font-size: 22px;
transform: rotate(-6deg);
}
.brand-name {
font-family: "Bangers", system-ui, sans-serif;
font-size: 30px;
letter-spacing: 1.5px;
color: var(--paper);
}
.nav {
display: flex;
flex-direction: column;
gap: 6px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
color: rgba(253, 252, 247, 0.78);
text-decoration: none;
font-weight: 600;
font-size: 14px;
border: 2px solid transparent;
border-radius: var(--r-sm);
transition: background 0.15s, color 0.15s, transform 0.12s;
}
.nav-ico {
display: inline-grid;
place-items: center;
width: 22px;
font-size: 15px;
opacity: 0.9;
}
.nav-item:hover {
background: rgba(253, 252, 247, 0.08);
color: var(--paper);
transform: translateX(3px);
}
.nav-item.is-active {
background: var(--accent);
color: #fff;
border-color: var(--paper);
}
.series-chip {
margin-top: auto;
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--ink-2);
border: 2px solid rgba(253, 252, 247, 0.2);
border-radius: var(--r-md);
}
.series-cover {
display: grid;
place-items: center;
width: 42px;
height: 42px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent));
border: 2px solid var(--paper);
border-radius: var(--r-sm);
font-family: "Bangers", system-ui, sans-serif;
font-size: 18px;
color: #fff;
letter-spacing: 1px;
}
.series-meta {
display: flex;
flex-direction: column;
font-size: 13px;
}
.series-meta strong {
font-size: 14px;
}
.series-meta span {
color: rgba(253, 252, 247, 0.6);
font-size: 12px;
}
/* Main */
.main {
padding: 26px clamp(18px, 3vw, 38px) 38px;
min-width: 0;
}
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.eyebrow {
margin: 0 0 2px;
text-transform: uppercase;
letter-spacing: 2px;
font-size: 11px;
font-weight: 800;
color: var(--accent);
}
.page-title {
margin: 0;
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: clamp(30px, 4vw, 44px);
letter-spacing: 1px;
line-height: 1.05;
}
.page-title span {
color: var(--accent-blue);
-webkit-text-stroke: 1px var(--ink);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.live-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: #21b573;
border: 1.5px solid var(--ink);
animation: pulse 1.8s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(33, 181, 115, 0.5);
}
50% {
box-shadow: 0 0 0 5px rgba(33, 181, 115, 0);
}
}
.live-label {
font-size: 12px;
color: var(--muted);
font-weight: 600;
}
.btn {
font-family: "Inter", sans-serif;
font-weight: 700;
font-size: 14px;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
padding: 9px 16px;
cursor: pointer;
background: var(--panel);
color: var(--ink);
box-shadow: var(--shadow-sm);
transition: transform 0.1s, box-shadow 0.1s;
}
.btn:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 var(--ink);
}
.btn:active {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0 var(--ink);
}
.btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
.btn-accent {
background: var(--accent);
color: #fff;
}
/* KPI cards */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 18px;
}
.kpi {
position: relative;
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-md);
padding: 16px 16px 14px;
box-shadow: var(--shadow);
overflow: hidden;
}
.kpi::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--halftone);
background-size: 6px 6px;
opacity: 0.35;
pointer-events: none;
-webkit-mask-image: linear-gradient(to top left, #000, transparent 55%);
mask-image: linear-gradient(to top left, #000, transparent 55%);
}
.kpi > * {
position: relative;
}
.kpi-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.kpi-label {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 11px;
font-weight: 800;
color: var(--muted);
}
.kpi-ico {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
font-size: 15px;
color: #fff;
}
.kpi[data-accent="red"] .kpi-ico {
background: var(--accent);
}
.kpi[data-accent="blue"] .kpi-ico {
background: var(--accent-blue);
}
.kpi[data-accent="gold"] .kpi-ico {
background: var(--accent-2);
color: var(--ink);
}
.kpi[data-accent="ink"] .kpi-ico {
background: var(--ink);
}
.kpi-value {
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: 38px;
line-height: 1;
letter-spacing: 1px;
margin-bottom: 8px;
}
.kpi-trend {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
}
.kpi-trend span {
color: var(--muted);
font-weight: 500;
}
.kpi-trend.is-up {
color: #1a9c5f;
}
.kpi-trend.is-down {
color: var(--accent);
}
/* Grid: chart + feed */
.grid {
display: grid;
grid-template-columns: 1.7fr 1fr;
gap: 16px;
margin-bottom: 18px;
}
.card {
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 18px;
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.card-title {
margin: 0;
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: 24px;
letter-spacing: 0.6px;
}
.card-sub {
margin: 2px 0 0;
font-size: 12px;
color: var(--muted);
font-weight: 500;
}
/* Range toggle */
.range-toggle {
display: inline-flex;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
overflow: hidden;
flex-shrink: 0;
}
.range-btn {
font-family: "Inter", sans-serif;
font-weight: 700;
font-size: 12px;
border: none;
border-right: 2px solid var(--ink);
background: var(--panel);
color: var(--ink);
padding: 6px 12px;
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.range-btn:last-child {
border-right: none;
}
.range-btn:hover {
background: var(--accent-2);
}
.range-btn.is-active {
background: var(--ink);
color: var(--paper);
}
.range-btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: -3px;
}
/* Chart */
.chart-wrap {
position: relative;
border: 2px solid var(--line-2);
border-radius: var(--r-md);
padding: 14px 14px 14px 48px;
background: linear-gradient(0deg, rgba(46, 107, 255, 0.04), transparent);
}
.chart-axis {
position: absolute;
left: 0;
top: 14px;
bottom: 14px;
width: 44px;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 10px;
font-weight: 700;
color: var(--muted);
text-align: right;
padding-right: 6px;
}
.chart {
display: block;
width: 100%;
height: 220px;
}
.chart .grid-line {
stroke: var(--line);
stroke-width: 1;
}
.chart .area {
fill: url(#areaGrad);
opacity: 0;
transition: opacity 0.5s ease;
}
.chart.is-ready .area {
opacity: 1;
}
.chart .line {
fill: none;
stroke: var(--accent);
stroke-width: 3.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: var(--len);
stroke-dashoffset: var(--len);
}
.chart.is-ready .line {
animation: draw 0.9s ease forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
.chart .dot {
fill: var(--paper);
stroke: var(--ink);
stroke-width: 2;
}
.chart-labels {
display: flex;
justify-content: space-between;
margin-top: 8px;
padding-left: 48px;
font-size: 11px;
font-weight: 700;
color: var(--muted);
}
/* Feed */
.feed {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.feed li {
display: flex;
gap: 12px;
align-items: flex-start;
padding-bottom: 12px;
border-bottom: 1.5px dashed var(--line-2);
}
.feed li:last-child {
border-bottom: none;
padding-bottom: 0;
}
.feed-ico {
flex-shrink: 0;
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 2px solid var(--ink);
border-radius: var(--r-sm);
font-size: 14px;
background: var(--accent-2);
}
.feed-ico.sub {
background: var(--accent-blue);
color: #fff;
}
.feed-ico.tip {
background: #21b573;
color: #fff;
}
.feed-ico.comment {
background: var(--accent);
color: #fff;
}
.feed-body {
font-size: 13px;
min-width: 0;
}
.feed-body strong {
font-weight: 700;
}
.feed-time {
display: block;
font-size: 11px;
color: var(--muted);
font-weight: 600;
margin-top: 1px;
}
/* Table */
.table-card {
padding-bottom: 8px;
}
.table-scroll {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
min-width: 460px;
}
.table thead th {
text-align: left;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 800;
color: var(--muted);
padding: 8px 12px;
border-bottom: 3px solid var(--ink);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.table th.th-num {
text-align: right;
}
.table thead th:hover {
color: var(--ink);
background: var(--accent-2);
}
.table thead th.is-sorted {
color: var(--ink);
}
.table thead th::after {
content: " ↕";
opacity: 0.4;
font-size: 10px;
}
.table thead th[aria-sort="ascending"]::after {
content: " ▲";
opacity: 1;
color: var(--accent);
}
.table thead th[aria-sort="descending"]::after {
content: " ▼";
opacity: 1;
color: var(--accent);
}
.table tbody td {
padding: 11px 12px;
border-bottom: 1.5px solid var(--line);
vertical-align: middle;
}
.table tbody td.num {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.table tbody tr {
transition: background 0.12s;
}
.table tbody tr:hover {
background: rgba(255, 210, 63, 0.18);
}
.ep-title {
display: flex;
align-items: center;
gap: 10px;
}
.ep-num {
display: grid;
place-items: center;
width: 26px;
height: 26px;
flex-shrink: 0;
background: var(--ink);
color: var(--paper);
border-radius: var(--r-sm);
font-family: "Bangers", system-ui, sans-serif;
font-size: 14px;
}
.ep-name {
font-weight: 700;
}
.rev {
color: #1a9c5f;
font-weight: 700;
}
.foot {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-top: 22px;
font-size: 12px;
color: var(--muted);
font-weight: 600;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--paper);
border: 2px solid var(--accent-2);
border-radius: var(--r-sm);
padding: 11px 18px;
font-weight: 700;
font-size: 14px;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 60;
max-width: calc(100% - 32px);
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* Responsive */
@media (max-width: 980px) {
.kpis {
grid-template-columns: repeat(2, 1fr);
}
.grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
height: auto;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 14px;
border-right: none;
border-bottom: 3px solid var(--ink);
}
.nav {
flex-direction: row;
flex-wrap: wrap;
flex: 1;
justify-content: flex-end;
}
.nav-item {
padding: 8px 10px;
}
.series-chip {
margin-top: 0;
width: 100%;
}
}
@media (max-width: 520px) {
.main {
padding: 18px 14px 30px;
}
.kpis {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.kpi {
padding: 13px;
}
.kpi-value {
font-size: 30px;
}
.topbar {
align-items: flex-start;
}
.topbar-actions {
width: 100%;
flex-wrap: wrap;
}
.btn-accent {
margin-left: auto;
}
.nav-item span:not(.nav-ico) {
display: none;
}
.nav-ico {
width: auto;
font-size: 18px;
}
.card {
padding: 14px;
}
.chart-wrap {
padding-left: 40px;
}
.chart-axis {
width: 36px;
font-size: 9px;
}
}
@media (prefers-reduced-motion: reduce) {
.chart.is-ready .line {
animation: none;
stroke-dashoffset: 0;
}
.live-dot {
animation: none;
}
*,
*::before,
*::after {
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
var prefersReduced =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ----------------------------------------------------------- *
* Toast helper
* ----------------------------------------------------------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ----------------------------------------------------------- *
* Number formatting
* ----------------------------------------------------------- */
function fmtCompact(n) {
if (n >= 1e6) return (n / 1e6).toFixed(2).replace(/\.?0+$/, "") + "M";
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K";
return String(Math.round(n));
}
function fmtNumber(n) {
return Math.round(n).toLocaleString("en-US");
}
function fmtMoney(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function fmtRating(n) {
return n.toFixed(1);
}
var formatters = {
compact: fmtCompact,
number: fmtNumber,
money: fmtMoney,
rating: fmtRating,
};
/* ----------------------------------------------------------- *
* Animated count-up on KPI cards
* ----------------------------------------------------------- */
function countUp(el) {
var target = parseFloat(el.getAttribute("data-count")) || 0;
var fmt = formatters[el.getAttribute("data-format")] || fmtNumber;
if (prefersReduced) {
el.textContent = fmt(target);
return;
}
var dur = 1100;
var start = null;
function easeOut(t) {
return 1 - Math.pow(1 - t, 3);
}
function step(ts) {
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
el.textContent = fmt(target * easeOut(p));
if (p < 1) requestAnimationFrame(step);
else el.textContent = fmt(target);
}
requestAnimationFrame(step);
}
var kpiValues = document.querySelectorAll(".kpi-value");
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(
function (entries, obs) {
entries.forEach(function (e) {
if (e.isIntersecting) {
countUp(e.target);
obs.unobserve(e.target);
}
});
},
{ threshold: 0.4 }
);
kpiValues.forEach(function (el) {
io.observe(el);
});
} else {
kpiValues.forEach(countUp);
}
/* ----------------------------------------------------------- *
* Chart data (synthetic but stable per range)
* ----------------------------------------------------------- */
function seededSeries(count, base, variance, seed) {
var arr = [];
var s = seed;
for (var i = 0; i < count; i++) {
// deterministic pseudo-random
s = (s * 9301 + 49297) % 233280;
var rnd = s / 233280;
var trend = base * (1 + (i / count) * 0.5);
var wave = Math.sin(i / 3) * variance * 0.4;
arr.push(Math.max(0, Math.round(trend + (rnd - 0.4) * variance + wave)));
}
return arr;
}
var SERIES = {
"7d": seededSeries(7, 9200, 4200, 41),
"30d": seededSeries(30, 7600, 5200, 77),
"90d": seededSeries(90, 5400, 6400, 113),
};
var SVG_NS = "http://www.w3.org/2000/svg";
var chart = document.getElementById("chart");
var axis = document.getElementById("chartAxis");
var labelsEl = document.getElementById("chartLabels");
var summaryEl = document.getElementById("chartSummary");
var VW = 720;
var VH = 280;
var PAD = 14;
function el(name, attrs) {
var e = document.createElementNS(SVG_NS, name);
for (var k in attrs) e.setAttribute(k, attrs[k]);
return e;
}
function renderChart(range) {
var data = SERIES[range];
var max = Math.max.apply(null, data) * 1.12;
var min = 0;
var n = data.length;
var innerW = VW - PAD * 2;
var innerH = VH - PAD * 2;
function x(i) {
return PAD + (n === 1 ? innerW / 2 : (i / (n - 1)) * innerW);
}
function y(v) {
return PAD + innerH - ((v - min) / (max - min)) * innerH;
}
chart.classList.remove("is-ready");
while (chart.firstChild) chart.removeChild(chart.firstChild);
// defs / gradient
var defs = el("defs", {});
var grad = el("linearGradient", {
id: "areaGrad",
x1: "0",
y1: "0",
x2: "0",
y2: "1",
});
grad.appendChild(el("stop", { offset: "0", "stop-color": "#ff2e4d", "stop-opacity": "0.28" }));
grad.appendChild(el("stop", { offset: "1", "stop-color": "#ff2e4d", "stop-opacity": "0" }));
defs.appendChild(grad);
chart.appendChild(defs);
// horizontal grid lines
for (var g = 0; g <= 4; g++) {
var gy = PAD + (innerH / 4) * g;
chart.appendChild(
el("line", {
class: "grid-line",
x1: PAD,
x2: VW - PAD,
y1: gy,
y2: gy,
})
);
}
// build path
var linePts = "";
for (var i = 0; i < n; i++) {
linePts += (i === 0 ? "M" : "L") + x(i).toFixed(1) + " " + y(data[i]).toFixed(1) + " ";
}
var areaPts =
linePts +
"L" + x(n - 1).toFixed(1) + " " + (VH - PAD) + " " +
"L" + x(0).toFixed(1) + " " + (VH - PAD) + " Z";
var area = el("path", { class: "area", d: areaPts });
chart.appendChild(area);
var line = el("path", { class: "line", d: linePts });
chart.appendChild(line);
// dots — fewer for dense ranges
var dotStep = n > 30 ? Math.ceil(n / 12) : n > 10 ? 3 : 1;
for (var d = 0; d < n; d += dotStep) {
chart.appendChild(el("circle", { class: "dot", cx: x(d), cy: y(data[d]), r: 4 }));
}
// always mark the last point
chart.appendChild(el("circle", { class: "dot", cx: x(n - 1), cy: y(data[n - 1]), r: 4 }));
// animate stroke
var len = line.getTotalLength ? line.getTotalLength() : 1000;
line.style.setProperty("--len", len);
// force reflow then enable animation
void chart.getBoundingClientRect();
chart.classList.add("is-ready");
// Y axis labels
axis.innerHTML = "";
for (var a = 4; a >= 0; a--) {
var span = document.createElement("span");
span.textContent = fmtCompact((max / 4) * a);
axis.appendChild(span);
}
// X labels
renderXLabels(range, n);
// summary
var total = data.reduce(function (s2, v) {
return s2 + v;
}, 0);
summaryEl.textContent =
fmtNumber(total) + " views · avg " + fmtCompact(total / n) + "/day · last " + range;
}
function renderXLabels(range, n) {
labelsEl.innerHTML = "";
var labels;
if (range === "7d") {
labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
} else if (range === "30d") {
labels = ["Wk 1", "Wk 2", "Wk 3", "Wk 4"];
} else {
labels = ["Mar", "Apr", "May", "Jun"];
}
labels.forEach(function (t) {
var s = document.createElement("span");
s.textContent = t;
labelsEl.appendChild(s);
});
}
var rangeBtns = document.querySelectorAll(".range-btn");
rangeBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var range = btn.getAttribute("data-range");
rangeBtns.forEach(function (b) {
b.classList.remove("is-active");
b.removeAttribute("aria-pressed");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
renderChart(range);
toast("Showing last " + range);
});
});
renderChart("30d");
/* ----------------------------------------------------------- *
* Top episodes table — sortable
* ----------------------------------------------------------- */
var episodes = [
{ num: 12, title: "Static Bloom", views: 412800, likes: 38420, revenue: 1860 },
{ num: 9, title: "Rooftop Requiem", views: 388110, likes: 35210, revenue: 1740 },
{ num: 11, title: "Ash & Neon", views: 351900, likes: 31980, revenue: 1520 },
{ num: 7, title: "The Quiet Blade", views: 298440, likes: 27110, revenue: 1280 },
{ num: 10, title: "Signal Lost", views: 264700, likes: 24050, revenue: 1095 },
{ num: 8, title: "Midnight Vendor", views: 211360, likes: 19840, revenue: 870 },
];
var body = document.getElementById("epBody");
var headers = document.querySelectorAll("#epTable th[data-sort]");
var sortState = { key: "views", dir: "desc" };
function renderRows() {
var sorted = episodes.slice().sort(function (a, b) {
var k = sortState.key;
var av = a[k];
var bv = b[k];
var cmp;
if (typeof av === "string") cmp = av.localeCompare(bv);
else cmp = av - bv;
return sortState.dir === "asc" ? cmp : -cmp;
});
body.innerHTML = "";
sorted.forEach(function (e) {
var tr = document.createElement("tr");
var tdTitle = document.createElement("td");
tdTitle.innerHTML =
'<div class="ep-title"><span class="ep-num">' +
e.num +
'</span><span class="ep-name">' +
e.title +
"</span></div>";
var tdViews = document.createElement("td");
tdViews.className = "num";
tdViews.textContent = fmtNumber(e.views);
var tdLikes = document.createElement("td");
tdLikes.className = "num";
tdLikes.textContent = fmtNumber(e.likes);
var tdRev = document.createElement("td");
tdRev.className = "num";
tdRev.innerHTML = '<span class="rev">' + fmtMoney(e.revenue) + "</span>";
tr.appendChild(tdTitle);
tr.appendChild(tdViews);
tr.appendChild(tdLikes);
tr.appendChild(tdRev);
body.appendChild(tr);
});
}
function updateHeaderState() {
headers.forEach(function (h) {
h.classList.remove("is-sorted");
h.removeAttribute("aria-sort");
if (h.getAttribute("data-sort") === sortState.key) {
h.classList.add("is-sorted");
h.setAttribute("aria-sort", sortState.dir === "asc" ? "ascending" : "descending");
}
});
}
headers.forEach(function (h) {
h.addEventListener("click", function () {
var key = h.getAttribute("data-sort");
if (sortState.key === key) {
sortState.dir = sortState.dir === "asc" ? "desc" : "asc";
} else {
sortState.key = key;
sortState.dir = key === "title" ? "asc" : "desc";
}
updateHeaderState();
renderRows();
});
});
updateHeaderState();
renderRows();
/* ----------------------------------------------------------- *
* Recent activity feed
* ----------------------------------------------------------- */
var activity = [
{ type: "sub", ico: "♥", html: "<strong>+182</strong> new subscribers today", time: "2 min ago" },
{ type: "tip", ico: "$", html: "<strong>R. Okafor</strong> tipped <strong>$15</strong> on Ep 12", time: "18 min ago" },
{ type: "comment", ico: "✦", html: "<strong>Static Bloom</strong> hit <strong>400K</strong> views", time: "1 hr ago" },
{ type: "comment", ico: "✎", html: "<strong>312</strong> new comments on Ep 11", time: "3 hrs ago" },
{ type: "sub", ico: "♥", html: "Featured in <strong>Editor's Picks</strong>", time: "Yesterday" },
];
var feed = document.getElementById("feed");
activity.forEach(function (a) {
var li = document.createElement("li");
li.innerHTML =
'<span class="feed-ico ' +
a.type +
'" aria-hidden="true">' +
a.ico +
'</span><span class="feed-body">' +
a.html +
'<span class="feed-time">' +
a.time +
"</span></span>";
feed.appendChild(li);
});
/* ----------------------------------------------------------- *
* New episode button
* ----------------------------------------------------------- */
var newBtn = document.getElementById("newEpisode");
if (newBtn) {
newBtn.addEventListener("click", function () {
toast("Draft created — Episode 13 ✎");
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Inkwell Studio — Creator Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Bangers&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to dashboard</a>
<div class="shell">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Studio navigation">
<div class="brand">
<span class="brand-mark" aria-hidden="true">★</span>
<span class="brand-name">INKWELL</span>
</div>
<nav class="nav">
<a href="#main" class="nav-item is-active" aria-current="page">
<span class="nav-ico" aria-hidden="true">▣</span> Dashboard
</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">▤</span> Episodes</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◎</span> Audience</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">$</span> Revenue</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">✎</span> Studio</a>
</nav>
<div class="series-chip">
<div class="series-cover" aria-hidden="true">NR</div>
<div class="series-meta">
<strong>Neon Ronin</strong>
<span>Ongoing · Ep 12</span>
</div>
</div>
</aside>
<!-- Main -->
<main class="main" id="main">
<header class="topbar">
<div>
<p class="eyebrow">Creator Dashboard</p>
<h1 class="page-title">Hey, <span>Mika Tanaka</span></h1>
</div>
<div class="topbar-actions">
<span class="live-dot" aria-hidden="true"></span>
<span class="live-label">Updated just now</span>
<button class="btn btn-accent" type="button" id="newEpisode">+ New Episode</button>
</div>
</header>
<!-- KPI cards -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi" data-accent="red">
<div class="kpi-top">
<span class="kpi-label">Total Views</span>
<span class="kpi-ico" aria-hidden="true">◉</span>
</div>
<div class="kpi-value" data-count="2480000" data-format="compact">0</div>
<div class="kpi-trend is-up">▲ 12.4% <span>vs last month</span></div>
</article>
<article class="kpi" data-accent="blue">
<div class="kpi-top">
<span class="kpi-label">Subscribers</span>
<span class="kpi-ico" aria-hidden="true">♥</span>
</div>
<div class="kpi-value" data-count="84920" data-format="number">0</div>
<div class="kpi-trend is-up">▲ 5.1% <span>+4,120 new</span></div>
</article>
<article class="kpi" data-accent="gold">
<div class="kpi-top">
<span class="kpi-label">Revenue (Jun)</span>
<span class="kpi-ico" aria-hidden="true">$</span>
</div>
<div class="kpi-value" data-count="7340" data-format="money">0</div>
<div class="kpi-trend is-up">▲ 8.9% <span>vs May</span></div>
</article>
<article class="kpi" data-accent="ink">
<div class="kpi-top">
<span class="kpi-label">Avg Rating</span>
<span class="kpi-ico" aria-hidden="true">★</span>
</div>
<div class="kpi-value" data-count="4.8" data-format="rating">0</div>
<div class="kpi-trend is-down">▼ 0.1 <span>last 30 days</span></div>
</article>
</section>
<div class="grid">
<!-- Chart -->
<section class="card chart-card" aria-label="Views over time">
<div class="card-head">
<div>
<h2 class="card-title">Views over time</h2>
<p class="card-sub" id="chartSummary">Loading…</p>
</div>
<div class="range-toggle" role="group" aria-label="Chart range">
<button class="range-btn" type="button" data-range="7d">7d</button>
<button class="range-btn is-active" type="button" data-range="30d" aria-pressed="true">30d</button>
<button class="range-btn" type="button" data-range="90d">90d</button>
</div>
</div>
<div class="chart-wrap">
<div class="chart-axis" id="chartAxis" aria-hidden="true"></div>
<svg
class="chart"
id="chart"
viewBox="0 0 720 280"
preserveAspectRatio="none"
role="img"
aria-label="Line chart of daily views"
></svg>
</div>
<div class="chart-labels" id="chartLabels" aria-hidden="true"></div>
</section>
<!-- Activity feed -->
<section class="card feed-card" aria-label="Recent activity">
<div class="card-head">
<h2 class="card-title">Recent activity</h2>
</div>
<ul class="feed" id="feed"></ul>
</section>
</div>
<!-- Top episodes table -->
<section class="card table-card" aria-label="Top episodes">
<div class="card-head">
<h2 class="card-title">Top episodes</h2>
<p class="card-sub">Click a column to sort</p>
</div>
<div class="table-scroll">
<table class="table" id="epTable">
<thead>
<tr>
<th scope="col" data-sort="title" class="th-text">Episode</th>
<th scope="col" data-sort="views" class="th-num is-sorted" aria-sort="descending">Views</th>
<th scope="col" data-sort="likes" class="th-num">Likes</th>
<th scope="col" data-sort="revenue" class="th-num">Revenue</th>
</tr>
</thead>
<tbody id="epBody"></tbody>
</table>
</div>
</section>
<footer class="foot">
<span>Illustrative UI — fictional series & data.</span>
<span>Inkwell Studio · v0.6</span>
</footer>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Creator Dashboard (views · subs · revenue)
An admin home for Inkwell Studio, the publishing desk behind the fictional series Neon Ronin. An inked sidebar carries the studio brand, navigation, and a current-series chip, while the main column opens on a greeting bar and a row of four KPI stat cards — total views, subscribers, this-month revenue, and average rating — each set in Bangers, dusted with a Ben-Day halftone fade, and tagged with an up or down trend delta. The cards count up from zero the moment they scroll into view.
Below the KPIs, a hand-built SVG line chart traces daily views inside a thick-bordered plot with a gradient area fill and an animated draw-on. A 7d / 30d / 90d range toggle re-renders the series, axis labels, and a running summary on the fly. Beside it, a recent-activity feed stacks new-subscriber, tip, milestone, and comment events with inked icon chips. A sortable top-episodes table ranks chapters by views, likes, or revenue — click any column header to flip the sort and the aria-sort arrow follows.
Everything is dependency-free vanilla JavaScript: an IntersectionObserver drives the count-up, a deterministic generator keeps each range’s chart stable across redraws, the table re-sorts in place, and a small toast() helper surfaces feedback for range changes and the “New Episode” action. Controls are real keyboard-focusable buttons with visible focus rings, body text meets WCAG AA contrast, the grid collapses cleanly down to ~360px, and a prefers-reduced-motion block stills the count-up, chart draw, and live pulse.
Illustrative UI only — fictional series, characters, and data.