repos
/ analytics-django master

analytics-django

mirror archived upstream

Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.

analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics

18.1 KB · 894 lines · SCSS Raw History
  1@use "variables" as *;
  2
  3// ── CSS variable overrides ──
  4// Bootstrap 5.3 writes defaults into `:root,[data-bs-theme=light]`. Use
  5// `html:root` so we outrank the default block and enforce the dark palette.
  6html:root {
  7  --bs-body-bg: #{$body-bg};
  8  --bs-body-color: #{$body-color};
  9  --bs-body-bg-rgb: 14, 13, 10;
 10  --bs-body-color-rgb: 221, 215, 205;
 11  --bs-emphasis-color: #{$white};
 12  --bs-secondary-bg: #13120e;
 13  --bs-tertiary-bg: #18160f;
 14  --bs-border-color: rgba(107, 158, 120, 0.12);
 15  --bs-link-color-rgb: 125, 184, 140;
 16  --bs-link-hover-color-rgb: 149, 204, 162;
 17  --bs-heading-color: #{$white};
 18}
 19
 20// ── Global ──
 21
 22body {
 23  min-height: 100vh;
 24  display: flex;
 25  flex-direction: column;
 26  -webkit-font-smoothing: antialiased;
 27  -moz-osx-font-smoothing: grayscale;
 28  letter-spacing: 0.01em;
 29  background-color: $body-bg !important;
 30  color: $body-color;
 31}
 32
 33main { flex: 1; }
 34
 35.vh-100 { height: 100vh; }
 36
 37// ── Logo (rising-bars mark) ──
 38// An ascending bar-chart silhouette — evokes growth / aggregate counts, which
 39// is what Analytics is actually for. Rendered inline as SVG so it scales
 40// cleanly and matches the favicon exactly.
 41
 42.logo {
 43  display: inline-flex;
 44  align-items: center;
 45  justify-content: center;
 46  width: 28px;
 47  height: 28px;
 48  text-decoration: none !important;
 49  transition: transform 250ms ease;
 50
 51  svg {
 52    width: 100%;
 53    height: 100%;
 54    display: block;
 55  }
 56
 57  &:hover {
 58    transform: translateY(-1px);
 59  }
 60
 61  &.logo-sm {
 62    width: 18px;
 63    height: 18px;
 64  }
 65}
 66
 67// ── Navbar ──
 68
 69.navbar {
 70  height: 52px;
 71  padding: 0;
 72  background: #090806 !important;
 73  border-bottom: 1px solid rgba(107, 158, 120, 0.06);
 74
 75  .navbar-brand {
 76    display: flex;
 77    align-items: center;
 78    gap: 0.6rem;
 79    color: $white;
 80    font-weight: 600;
 81    letter-spacing: 0.1em;
 82    text-transform: uppercase;
 83    font-size: 0.78rem;
 84    text-decoration: none;
 85
 86    .brand-text { color: $white; }
 87    .brand-dot {
 88      display: inline-block;
 89      width: 6px; height: 6px; border-radius: 50%;
 90      background: $green;
 91    }
 92  }
 93
 94  .nav-link {
 95    font-size: 0.8rem;
 96    font-weight: 500;
 97    letter-spacing: 0.05em;
 98    text-transform: lowercase;
 99    padding-left: 1rem !important;
100    padding-right: 1rem !important;
101    color: rgba(221, 215, 205, 0.6);
102    position: relative;
103    transition: color 200ms ease;
104
105    &::after {
106      content: '';
107      position: absolute;
108      bottom: 2px;
109      left: 1rem;
110      right: 1rem;
111      height: 1px;
112      background: $green;
113      transform: scaleX(0);
114      transition: transform 250ms ease;
115    }
116
117    &:hover,
118    &.active { color: $white; }
119
120    &:hover::after,
121    &.active::after { transform: scaleX(1); }
122  }
123}
124
125// ── Breadcrumb bar ──
126
127.breadcrumb-bar {
128  background: rgba(107, 158, 120, 0.02);
129  border-bottom: 1px solid rgba(107, 158, 120, 0.04);
130
131  .breadcrumb { font-family: $font-family-monospace; }
132}
133
134// ── Section labels — amber caps, no prompt glyph ──
135
136.section-label {
137  font-size: 0.7rem;
138  font-weight: 600;
139  letter-spacing: 0.12em;
140  text-transform: uppercase;
141  color: rgba(201, 168, 76, 0.75);
142  font-family: $font-family-monospace;
143}
144
145// ── Cards (base) ──
146
147.card {
148  background: $card-bg;
149  border: 1px solid $card-border-color;
150  transition: border-color 250ms ease;
151
152  &:hover { border-color: rgba(107, 158, 120, 0.2); }
153}
154
155// ── Status dot ──
156
157.status-dot {
158  display: inline-block;
159  width: 8px; height: 8px;
160  border-radius: 50%;
161  margin-right: 0.55rem;
162  vertical-align: middle;
163  flex-shrink: 0;
164
165  &.status-dot-lg {
166    width: 14px; height: 14px;
167    margin-right: 0;
168  }
169
170  &.is-up { background: $green; box-shadow: 0 0 0 3px rgba(107, 158, 120, 0.12); }
171  &.is-down { background: $terracotta; box-shadow: 0 0 0 3px rgba(196, 112, 85, 0.12); }
172  &.is-warn { background: $amber; box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.12); }
173  &.is-idle { background: #4a443c; }
174}
175
176// ── Visibility toggle (segmented pill) ──
177
178.toggle-label {
179  font-family: $font-family-monospace;
180  font-size: 0.68rem;
181  font-weight: 600;
182  letter-spacing: 0.12em;
183  text-transform: uppercase;
184  color: #847c72;
185  cursor: help;
186}
187
188.toggle-pill {
189  display: inline-flex;
190  align-items: stretch;
191  border: 1px solid rgba(107, 158, 120, 0.22);
192  border-radius: 999px;
193  overflow: hidden;
194  cursor: pointer;
195  background: #0e0d0a;
196  margin: 0;
197  user-select: none;
198  line-height: 1;
199
200  input {
201    position: absolute;
202    opacity: 0;
203    pointer-events: none;
204    width: 0; height: 0;
205  }
206
207  .toggle-pill-seg {
208    padding: 0.45rem 0.9rem;
209    font-family: $font-family-monospace;
210    font-size: 0.7rem;
211    font-weight: 600;
212    letter-spacing: 0.08em;
213    text-transform: uppercase;
214    color: #665f56;
215    transition: background 180ms ease, color 180ms ease;
216  }
217
218  input ~ .toggle-pill-private {
219    background: rgba(201, 168, 76, 0.12);
220    color: #ddc06a;
221  }
222
223  input:checked ~ .toggle-pill-private {
224    background: transparent;
225    color: #665f56;
226  }
227  input:checked ~ .toggle-pill-public {
228    background: $green-dim;
229    color: $green-bright;
230  }
231
232  &:hover {
233    border-color: rgba(107, 158, 120, 0.35);
234  }
235}
236
237// ── Metric tile ──
238
239.metric-tile {
240  background: $card-bg;
241  border: 1px solid $card-border-color;
242  border-radius: $border-radius;
243  padding: 1rem 1.15rem;
244  height: 100%;
245  display: flex;
246  flex-direction: column;
247  position: relative;
248
249  .metric-label {
250    font-size: 0.68rem;
251    font-weight: 600;
252    letter-spacing: 0.12em;
253    text-transform: uppercase;
254    color: #847c72;
255  }
256
257  .metric-value {
258    font-size: 1.75rem;
259    font-weight: 700;
260    line-height: 1.1;
261    color: $white;
262    margin-top: 0.35rem;
263    letter-spacing: -0.01em;
264    word-break: break-word;
265  }
266
267  .metric-sub {
268    font-size: 0.72rem;
269    color: #847c72;
270    margin-top: auto;
271    padding-top: 0.35rem;
272    letter-spacing: 0.02em;
273  }
274
275  // Small delta badge pinned in the top-right
276  .metric-delta {
277    position: absolute;
278    top: 0.55rem;
279    right: 0.7rem;
280    font-family: $font-family-monospace;
281    font-size: 0.75rem;
282    font-weight: 600;
283    letter-spacing: 0.04em;
284    padding: 0.22rem 0.5rem;
285    border-radius: 2px;
286    border: 1px solid transparent;
287    line-height: 1;
288
289    &.is-up {
290      color: $green-bright;
291      background: $green-dim;
292      border-color: $green-border;
293    }
294    &.is-down {
295      color: #e38871;
296      background: $terracotta-dim;
297      border-color: $terracotta-border;
298    }
299    &.is-flat {
300      color: #847c72;
301      background: rgba(221, 215, 205, 0.04);
302      border-color: rgba(221, 215, 205, 0.08);
303    }
304  }
305
306  &.metric-accent-green {
307    border-color: $green-border;
308    .metric-value { color: $green-bright; }
309  }
310  &.metric-accent-amber {
311    border-color: $amber-border;
312    .metric-value { color: #ddc06a; }
313  }
314  &.metric-accent-danger {
315    border-color: $terracotta-border;
316    .metric-value { color: #d88870; }
317  }
318}
319
320// ── Chips (inline status/score) ──
321
322.chip {
323  display: inline-flex;
324  align-items: center;
325  gap: 0.4rem;
326  font-family: $font-family-monospace;
327  font-size: 0.7rem;
328  font-weight: 600;
329  letter-spacing: 0.05em;
330  text-transform: uppercase;
331  padding: 0.3rem 0.6rem;
332  border-radius: 2px;
333  border: 1px solid transparent;
334  line-height: 1;
335
336  &.chip-ok {
337    background: $green-dim;
338    color: $green-bright;
339    border-color: $green-border;
340  }
341  &.chip-down {
342    background: $terracotta-dim;
343    color: #e38871;
344    border-color: $terracotta-border;
345  }
346  &.chip-warn {
347    background: $amber-dim;
348    color: #ddc06a;
349    border-color: $amber-border;
350  }
351  &.chip-info {
352    background: rgba(126, 170, 184, 0.08);
353    color: #9ec5d2;
354    border-color: rgba(126, 170, 184, 0.22);
355  }
356  &.chip-muted {
357    background: rgba(221, 215, 205, 0.04);
358    color: #847c72;
359    border-color: rgba(221, 215, 205, 0.08);
360  }
361}
362
363// ── Footer ──
364
365footer {
366  padding-top: 4rem;
367  padding-bottom: 3rem;
368  position: relative;
369  background: #090806;
370  border-top: 1px solid rgba(107, 158, 120, 0.06);
371
372  .h5 {
373    font-size: 0.7rem;
374    letter-spacing: 0.12em;
375    text-transform: uppercase;
376    color: rgba(201, 168, 76, 0.55);
377    font-weight: 600;
378  }
379
380  p {
381    color: rgba(221, 215, 205, 0.5);
382    font-size: 0.85rem;
383    line-height: 1.75;
384
385    a {
386      color: rgba(221, 215, 205, 0.75);
387      text-decoration: underline;
388      text-underline-offset: 2px;
389
390      &:hover { color: $white; }
391    }
392  }
393
394  .links {
395    padding-top: 2rem;
396    padding-bottom: 1rem;
397  }
398}
399
400.footer-bar {
401  background: #050403;
402  border-top: 1px solid rgba(107, 158, 120, 0.04);
403  padding: 0.8rem 0;
404  color: rgba(221, 215, 205, 0.3);
405  font-size: 0.78rem;
406
407  small { color: rgba(221, 215, 205, 0.3); }
408
409  &-link {
410    color: rgba(221, 215, 205, 0.3);
411    transition: color 200ms ease;
412
413    &:hover { color: $white; }
414  }
415}
416
417// ── Terminal-style block ──
418
419.terminal-block {
420  background: rgba(9, 8, 6, 0.7);
421  border: 1px solid rgba(107, 158, 120, 0.15);
422  border-radius: $border-radius;
423  font-family: $font-family-monospace;
424  font-size: 0.8rem;
425  color: $green-bright;
426  padding: 1rem 1.2rem;
427  line-height: 1.8;
428
429  .t-line { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
430  .t-prompt { color: $amber; margin-right: 0.4rem; }
431  .t-out { color: $gray-200; }
432  .t-comment { color: #665f56; }
433  .t-key { color: $amber; }
434  .t-val { color: $green-bright; }
435  .t-cursor {
436    display: inline-block; width: 7px; height: 0.9em; vertical-align: -2px;
437    background: $green-bright;
438    animation: t-blink 1s steps(1) infinite;
439  }
440}
441
442@keyframes t-blink {
443  0%, 50% { opacity: 1; }
444  51%, 100% { opacity: 0; }
445}
446
447// ── Property row (dashboard listing) ──
448
449.property-row {
450  display: grid;
451  grid-template-columns: minmax(0, 1fr) auto;
452  gap: 1rem 1.5rem;
453  background: $card-bg;
454  border: 1px solid $card-border-color;
455  border-radius: $border-radius;
456  padding: 1rem 1.15rem;
457  margin-bottom: 0.75rem;
458  transition: border-color 200ms ease;
459
460  &:hover { border-color: rgba(107, 158, 120, 0.22); }
461
462  &.is-quiet { border-color: rgba(132, 124, 114, 0.25); }
463
464  .pr-main {
465    min-width: 0;
466    display: flex;
467    flex-direction: column;
468    gap: 0.35rem;
469  }
470
471  .pr-title {
472    font-weight: 600;
473    color: $white;
474    display: flex;
475    align-items: center;
476    min-width: 0;
477
478    a {
479      color: inherit;
480      text-decoration: none;
481      min-width: 0;
482      overflow: hidden;
483      text-overflow: ellipsis;
484      white-space: nowrap;
485
486      &:hover { color: $green-bright; }
487    }
488  }
489
490  .pr-id {
491    font-size: 0.72rem;
492    color: #665f56;
493    font-family: $font-family-monospace;
494    overflow: hidden;
495    text-overflow: ellipsis;
496    white-space: nowrap;
497    display: flex;
498    align-items: baseline;
499    gap: 0.5rem;
500
501    .pr-id-k {
502      text-transform: uppercase;
503      font-size: 0.6rem;
504      letter-spacing: 0.1em;
505      color: #847c72;
506      flex-shrink: 0;
507    }
508  }
509
510  .pr-side {
511    display: flex;
512    flex-direction: column;
513    justify-content: space-between;
514    align-items: flex-end;
515    gap: 0.75rem;
516    min-height: 68px;
517  }
518
519  .pr-meta {
520    display: flex;
521    gap: 0.85rem;
522    align-items: flex-end;
523    font-family: $font-family-monospace;
524
525    .pr-meta-cell {
526      display: flex;
527      flex-direction: column;
528      align-items: flex-start;
529      gap: 0.2rem;
530      min-width: 0;
531    }
532
533    .pr-meta-k {
534      color: #665f56;
535      text-transform: uppercase;
536      font-size: 0.6rem;
537      letter-spacing: 0.1em;
538      line-height: 1;
539    }
540
541    .pr-meta-v {
542      color: $gray-200;
543      font-size: 0.88rem;
544      font-weight: 600;
545      line-height: 1;
546    }
547  }
548
549  .pr-actions {
550    display: flex;
551    gap: 0.4rem;
552  }
553}
554
555@media (max-width: 767px) {
556  .property-row {
557    grid-template-columns: 1fr;
558
559    .pr-side {
560      min-height: 0;
561      align-items: stretch;
562    }
563    .pr-meta {
564      flex-wrap: wrap;
565      justify-content: flex-start;
566    }
567    .pr-actions {
568      justify-content: flex-end;
569    }
570  }
571}
572
573// ── Search form ──
574
575.search-form {
576  position: relative;
577
578  .search-icon {
579    position: absolute;
580    left: 0.9rem;
581    top: 50%;
582    transform: translateY(-50%);
583    color: #665f56;
584    pointer-events: none;
585    z-index: 2;
586  }
587
588  .search-input {
589    padding-left: 2.5rem;
590    font-size: 0.85rem;
591    border: 1px solid rgba(107, 158, 120, 0.15);
592    background: rgba(19, 18, 14, 0.7);
593    color: $body-color;
594    transition: border-color 200ms ease;
595
596    &::placeholder {
597      color: #665f56;
598      font-style: normal;
599    }
600
601    &:focus {
602      border-color: rgba(107, 158, 120, 0.4);
603      box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.1);
604      background: #13120e;
605    }
606  }
607}
608
609// ── Dashboard toolbar ──
610
611.dashboard-toolbar {
612  display: flex;
613  gap: 0.5rem;
614  align-items: stretch;
615
616  .form-control {
617    height: 38px;
618    font-size: 0.85rem;
619  }
620
621  .btn {
622    height: 38px;
623    padding: 0 1.1rem;
624    display: inline-flex;
625    align-items: center;
626    white-space: nowrap;
627  }
628}
629
630// ── Chart panel ──
631
632.chart-panel {
633  background: $card-bg;
634  border: 1px solid $card-border-color;
635  border-radius: $border-radius;
636  padding: 1rem 1.15rem;
637
638  .chart-panel-header {
639    display: flex;
640    align-items: center;
641    justify-content: space-between;
642    margin-bottom: 0.75rem;
643    gap: 0.75rem;
644  }
645
646  .chart-panel-title {
647    font-size: 0.68rem;
648    font-weight: 600;
649    letter-spacing: 0.12em;
650    text-transform: uppercase;
651    color: rgba(201, 168, 76, 0.75);
652    font-family: $font-family-monospace;
653  }
654
655  .chart-panel-body {
656    position: relative;
657  }
658}
659
660// ── Insight / top-list table ──
661// Used for "top pages", "referrers", "UTM source" etc. — compact grid rows
662// that read like a ranked terminal listing rather than a Bootstrap card.
663
664.rank-list {
665  background: $card-bg;
666  border: 1px solid $card-border-color;
667  border-radius: $border-radius;
668  overflow: hidden;
669  height: 100%;
670  display: flex;
671  flex-direction: column;
672
673  .rank-list-header {
674    display: flex;
675    justify-content: space-between;
676    align-items: center;
677    background: rgba(107, 158, 120, 0.04);
678    padding: 0.6rem 1rem;
679    border-bottom: 1px solid rgba(107, 158, 120, 0.08);
680
681    .rank-list-title {
682      font-size: 0.68rem;
683      font-weight: 600;
684      letter-spacing: 0.12em;
685      text-transform: uppercase;
686      color: rgba(201, 168, 76, 0.75);
687      font-family: $font-family-monospace;
688    }
689
690    .rank-list-col {
691      font-size: 0.62rem;
692      font-weight: 600;
693      letter-spacing: 0.1em;
694      text-transform: uppercase;
695      color: #665f56;
696      font-family: $font-family-monospace;
697    }
698  }
699
700  .rank-list-body {
701    display: flex;
702    flex-direction: column;
703    flex: 1 1 auto;
704  }
705
706  .rank-list-row {
707    display: grid;
708    grid-template-columns: minmax(0, 1fr) auto;
709    gap: 0.75rem;
710    align-items: center;
711    padding: 0.5rem 1rem;
712    border-bottom: 1px solid rgba(107, 158, 120, 0.04);
713    font-size: 0.85rem;
714
715    &:last-child { border-bottom: none; }
716
717    .rank-label {
718      min-width: 0;
719      overflow: hidden;
720      text-overflow: ellipsis;
721      white-space: nowrap;
722      color: $gray-200;
723
724      a {
725        color: inherit;
726        text-decoration: none;
727
728        &:hover {
729          color: $green-bright;
730          text-decoration: underline;
731        }
732      }
733    }
734
735    .rank-count {
736      font-family: $font-family-monospace;
737      font-size: 0.78rem;
738      color: $amber;
739      font-weight: 600;
740    }
741  }
742
743  .rank-list-empty {
744    padding: 1.25rem 1rem;
745    color: #665f56;
746    font-size: 0.82rem;
747    text-align: center;
748  }
749}
750
751// ── Filter chip (active query filter indicator) ──
752
753.filter-chip {
754  display: inline-flex;
755  align-items: center;
756  gap: 0.5rem;
757  background: $amber-dim;
758  border: 1px solid $amber-border;
759  color: #ddc06a;
760  font-family: $font-family-monospace;
761  font-size: 0.75rem;
762  padding: 0.3rem 0.6rem;
763  border-radius: 2px;
764  max-width: 100%;
765
766  .filter-chip-label {
767    overflow: hidden;
768    text-overflow: ellipsis;
769    white-space: nowrap;
770  }
771
772  .filter-chip-clear {
773    background: transparent;
774    border: none;
775    color: #ddc06a;
776    cursor: pointer;
777    padding: 0;
778    line-height: 1;
779    opacity: 0.7;
780    transition: opacity 150ms ease;
781
782    &:hover { opacity: 1; }
783  }
784}
785
786// ── Date range form (property dashboard) ──
787
788.date-range-form {
789  display: grid;
790  grid-template-columns: 1fr 1fr 1fr;
791  gap: 0.5rem;
792
793  @media (max-width: 575px) {
794    grid-template-columns: 1fr;
795  }
796
797  .form-floating label {
798    font-size: 0.78rem;
799  }
800
801  .form-control,
802  .form-select {
803    font-size: 0.85rem;
804  }
805}
806
807// ── Auth split view ──
808
809.auth-shell {
810  min-height: calc(100vh - 52px);
811  display: grid;
812  grid-template-columns: 1fr 1fr;
813
814  @media (max-width: 991px) {
815    grid-template-columns: 1fr;
816  }
817
818  .auth-form-col {
819    padding: 3rem 2rem;
820    display: flex;
821    align-items: center;
822    justify-content: center;
823  }
824
825  .auth-visual-col {
826    position: relative;
827    overflow: hidden;
828    background: linear-gradient(145deg, #090806 0%, #0e0d0a 60%, #13120e 100%);
829    border-left: 1px solid rgba(107, 158, 120, 0.08);
830
831    @media (max-width: 991px) { display: none; }
832  }
833
834  .auth-form-wrap {
835    width: 100%;
836    max-width: 380px;
837  }
838}
839
840// ── Site-tag codebox (for the collector snippet modal) ──
841
842.codebox {
843  background: #090806;
844  border: 1px solid rgba(107, 158, 120, 0.15);
845  border-radius: $border-radius;
846  color: $green-bright;
847  font-family: $font-family-monospace;
848  font-size: 0.78rem;
849  line-height: 1.7;
850  padding: 1rem 1.1rem;
851  white-space: pre-wrap;
852  word-break: break-word;
853  resize: vertical;
854  min-height: 160px;
855  width: 100%;
856
857  &:focus {
858    outline: none;
859    border-color: $green-border;
860    box-shadow: 0 0 0 2px rgba(107, 158, 120, 0.1);
861  }
862}
863
864// ── World map ──
865// d3-geo renders an SVG inside #map. Tooltip styling lives inline in
866// property_map.js since it needs runtime positioning anyway.
867
868#map {
869  position: relative;
870
871  svg path {
872    transition: fill 80ms ease, stroke 80ms ease;
873  }
874}
875
876.chart-panel-action {
877  background: transparent;
878  border: 1px solid rgba(107, 158, 120, 0.25);
879  color: rgba(221, 215, 205, 0.7);
880  font-family: $font-family-monospace;
881  font-size: 0.62rem;
882  letter-spacing: 0.08em;
883  text-transform: uppercase;
884  padding: 0.25rem 0.55rem;
885  border-radius: $border-radius;
886  cursor: pointer;
887  transition: color 80ms, border-color 80ms;
888
889  &:hover {
890    color: #c9a84c;
891    border-color: rgba(201, 168, 76, 0.5);
892  }
893}