Build the platform metrics dashboard
The dashboard leads with the numbers that are supposed to be zero, because a dashboard of healthy counts is one nobody opens: - Clubs with no season covering today. Seasons scope memberships, rosters and events, so such a club cannot take a signup or schedule a match -- and it fails silently, nothing errors, it is just inert. - Dormant clubs: nothing on the calendar for 30 days. Churn signal. - Admins pending MFA. RequireMFAMiddleware redirects them to enrolment, so they are locked out of their own club until they act: a support queue, not a stat. - Outstanding money across every club. Then the shape of the business: an onboarding funnel (clubs → with members → with a team → with events, which separates working clubs from shells), feature-flag adoption per club, and two charts -- signups and revenue per month. Charts use chart.js, self-hosted rather than pulled from a CDN, for the same reason as the fonts: no third-party in the render path. Two things the browser taught me: the canvas needs a height-bounded wrapper (with maintainAspectRatio off it sizes to its parent, and a parent with no height grew it to 3489px), and chart.js cannot read daisyUI's CSS variables, so the charts re-render on a data-theme change or keep the light palette in dark mode. The month series is zero-filled: a chart that skips empty months draws a smooth line straight over a month in which nothing happened. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,75 @@
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.tooltip {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
--tt-bg: var(--color-neutral);
|
||||
--tt-off: calc(100% + 0.5rem);
|
||||
--tt-tail: calc(100% + 1px + 0.25rem);
|
||||
--tt-tail-off: 0.5rem;
|
||||
& > .tooltip-content, &[data-tip]:before {
|
||||
position: absolute;
|
||||
max-width: 20rem;
|
||||
border-radius: var(--radius-field);
|
||||
padding-inline: calc(0.25rem * 2);
|
||||
padding-block: 0.25rem;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
color: var(--color-neutral-content);
|
||||
opacity: 0%;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25;
|
||||
background-color: var(--tt-bg);
|
||||
width: max-content;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
--tw-content: attr(data-tip);
|
||||
content: var(--tw-content);
|
||||
}
|
||||
&:after {
|
||||
opacity: 0%;
|
||||
background-color: var(--tt-bg);
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
width: 0.625rem;
|
||||
height: 0.25rem;
|
||||
display: block;
|
||||
position: absolute;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: -1px 0;
|
||||
--mask-tooltip: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A");
|
||||
mask-image: var(--mask-tooltip);
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
& > .tooltip-content, &[data-tip]:before, &:after {
|
||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms;
|
||||
}
|
||||
}
|
||||
&:is([data-tip]:not([data-tip=""]), :has(.tooltip-content:not(:empty))) {
|
||||
&.tooltip-open, &:hover, &:has(:focus-visible) {
|
||||
& > .tooltip-content, &[data-tip]:before, &:after {
|
||||
opacity: 100%;
|
||||
--tt-pos: 0rem;
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer daisyui.l1.l2 {
|
||||
> .tooltip-content, &[data-tip]:before {
|
||||
transform: translateX(var(--tt-trans, -50%)) translateY(var(--tt-pos, 0.25rem));
|
||||
inset: auto auto var(--tt-off) 50%;
|
||||
}
|
||||
&:after {
|
||||
transform: translateX(var(--tt-trans, -50%)) translateY(var(--tt-pos, 0.25rem));
|
||||
inset: auto auto var(--tt-tail) 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
&:is(.tabs > .tab) {
|
||||
@@ -1590,6 +1659,116 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.steps {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
counter-reset: step;
|
||||
grid-auto-columns: 1fr;
|
||||
.step {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: 40px 1fr;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
min-width: 4rem;
|
||||
--step-bg: var(--color-base-300);
|
||||
--step-fg: var(--color-base-content);
|
||||
&:before {
|
||||
top: 0;
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
height: calc(0.25rem * 2);
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
color: var(--step-bg);
|
||||
background-color: var(--step-bg);
|
||||
content: "";
|
||||
margin-inline-start: -100%;
|
||||
}
|
||||
> .step-icon, &:not(:has(.step-icon)):after {
|
||||
--tw-content: counter(step);
|
||||
content: var(--tw-content);
|
||||
counter-increment: step;
|
||||
z-index: 1;
|
||||
color: var(--step-fg);
|
||||
background-color: var(--step-bg);
|
||||
border: 1px solid var(--step-bg);
|
||||
position: relative;
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 1;
|
||||
display: grid;
|
||||
height: calc(0.25rem * 8);
|
||||
width: calc(0.25rem * 8);
|
||||
place-items: center;
|
||||
place-self: center;
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
&:first-child:before {
|
||||
--tw-content: none;
|
||||
content: var(--tw-content);
|
||||
}
|
||||
&[data-content]:after {
|
||||
--tw-content: attr(data-content);
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer daisyui.l1.l2 {
|
||||
.step-neutral {
|
||||
+ .step-neutral:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-neutral);
|
||||
--step-fg: var(--color-neutral-content);
|
||||
}
|
||||
}
|
||||
.step-primary {
|
||||
+ .step-primary:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-primary);
|
||||
--step-fg: var(--color-primary-content);
|
||||
}
|
||||
}
|
||||
.step-secondary {
|
||||
+ .step-secondary:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-secondary);
|
||||
--step-fg: var(--color-secondary-content);
|
||||
}
|
||||
}
|
||||
.step-accent {
|
||||
+ .step-accent:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-accent);
|
||||
--step-fg: var(--color-accent-content);
|
||||
}
|
||||
}
|
||||
.step-info {
|
||||
+ .step-info:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-info);
|
||||
--step-fg: var(--color-info-content);
|
||||
}
|
||||
}
|
||||
.step-success {
|
||||
+ .step-success:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-success);
|
||||
--step-fg: var(--color-success-content);
|
||||
}
|
||||
}
|
||||
.step-warning {
|
||||
+ .step-warning:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-warning);
|
||||
--step-fg: var(--color-warning-content);
|
||||
}
|
||||
}
|
||||
.step-error {
|
||||
+ .step-error:before, &:after, > .step-icon {
|
||||
--step-bg: var(--color-error);
|
||||
--step-fg: var(--color-error-content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.select {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
position: relative;
|
||||
@@ -2462,6 +2641,87 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.stack\! {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: inline-grid !important;
|
||||
grid-template-columns: 3px 4px 1fr 4px 3px !important;
|
||||
grid-template-rows: 3px 4px 1fr 4px 3px !important;
|
||||
& > * {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
&:nth-child(n + 2) {
|
||||
width: 100% !important;
|
||||
opacity: 70% !important;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
z-index: 2 !important;
|
||||
opacity: 90% !important;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
z-index: 3 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@layer daisyui.l1.l2 {
|
||||
&, &.stack-bottom {
|
||||
> * {
|
||||
grid-column: 3 / 4 !important;
|
||||
grid-row: 3 / 6 !important;
|
||||
&:nth-child(2) {
|
||||
grid-column: 2 / 5 !important;
|
||||
grid-row: 2 / 5 !important;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
grid-column: 1 / 6 !important;
|
||||
grid-row: 1 / 4 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.stack-top {
|
||||
> * {
|
||||
grid-column: 3 / 4 !important;
|
||||
grid-row: 1 / 4 !important;
|
||||
&:nth-child(2) {
|
||||
grid-column: 2 / 5 !important;
|
||||
grid-row: 2 / 5 !important;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
grid-column: 1 / 6 !important;
|
||||
grid-row: 3 / 6 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.stack-start {
|
||||
> * {
|
||||
grid-column: 1 / 4 !important;
|
||||
grid-row: 3 / 4 !important;
|
||||
&:nth-child(2) {
|
||||
grid-column: 2 / 5 !important;
|
||||
grid-row: 2 / 5 !important;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
grid-column: 3 / 6 !important;
|
||||
grid-row: 1 / 6 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.stack-end {
|
||||
> * {
|
||||
grid-column: 3 / 6 !important;
|
||||
grid-row: 3 / 4 !important;
|
||||
&:nth-child(2) {
|
||||
grid-column: 2 / 5 !important;
|
||||
grid-row: 2 / 5 !important;
|
||||
}
|
||||
&:nth-child(1) {
|
||||
grid-column: 1 / 4 !important;
|
||||
grid-row: 1 / 6 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -2506,6 +2766,17 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
.stat-desc {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
grid-column-start: 1;
|
||||
white-space: nowrap;
|
||||
color: var(--color-base-content);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
|
||||
}
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
.stat-title {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
grid-column-start: 1;
|
||||
@@ -2640,6 +2911,9 @@
|
||||
.mt-10 {
|
||||
margin-top: calc(var(--spacing) * 10);
|
||||
}
|
||||
.mb-1 {
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -2713,6 +2987,31 @@
|
||||
flex-direction: var(--tabs-direction);
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-auto-flow: row;
|
||||
place-items: start;
|
||||
column-gap: calc(0.25rem * 4);
|
||||
row-gap: calc(0.25rem * 10);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
& > *:not(script, style, template) {
|
||||
display: grid;
|
||||
place-items: start;
|
||||
gap: calc(0.25rem * 2);
|
||||
}
|
||||
&.footer-center {
|
||||
grid-auto-flow: column dense;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
& > *:not(script, style, template) {
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.stat {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
display: inline-grid;
|
||||
@@ -2877,12 +3176,18 @@
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
.inline-grid {
|
||||
display: inline-grid;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
.h-16 {
|
||||
height: calc(var(--spacing) * 16);
|
||||
}
|
||||
.h-56 {
|
||||
height: calc(var(--spacing) * 56);
|
||||
}
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -3023,6 +3328,13 @@
|
||||
margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-3 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.divide-y {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
@@ -3066,6 +3378,10 @@
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.border-l-4 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 4px;
|
||||
}
|
||||
.badge-ghost {
|
||||
@layer daisyui.l1.l2 {
|
||||
border-color: var(--color-base-200);
|
||||
@@ -3092,6 +3408,12 @@
|
||||
border-color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
|
||||
}
|
||||
}
|
||||
.border-error {
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
.border-warning {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
@@ -3158,6 +3480,9 @@
|
||||
--btn-shadow: 0 0 0 0 oklch(0% 0 0/0);
|
||||
}
|
||||
}
|
||||
.mask-repeat {
|
||||
mask-repeat: repeat;
|
||||
}
|
||||
.object-contain {
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -3170,6 +3495,17 @@
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
.table-sm {
|
||||
@layer daisyui.l1.l2 {
|
||||
:not(thead, tfoot) tr {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
:where(th, td) {
|
||||
padding-inline: calc(0.25rem * 3);
|
||||
padding-block: calc(0.25rem * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
.px-4 {
|
||||
padding-inline: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -3290,6 +3626,11 @@
|
||||
--alert-color: var(--color-warning);
|
||||
}
|
||||
}
|
||||
.progress-primary {
|
||||
@layer daisyui.l1.l2 {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
.text-base-content {
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
@@ -3482,11 +3823,26 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.sm\:grid-cols-2 {
|
||||
@media (width >= 40rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.md\:grid-cols-2 {
|
||||
@media (width >= 48rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.lg\:grid-cols-2 {
|
||||
@media (width >= 64rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.lg\:grid-cols-4 {
|
||||
@media (width >= 64rem) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Ubuntu";
|
||||
|
||||
Reference in New Issue
Block a user