Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 100 additions & 55 deletions components/analytics/AnalyticsModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,71 +211,116 @@
font-size: 0.95rem;
}

/* ── Phone ───────────────────────────────────────────────────
Side-by-side sidebar/content squeezes the content column too
narrow on a phone, cropping the stat cards. Fill the screen and
stack vertically: the sidebar collapses to a horizontal tab strip
above the (now full-width, scrollable) content. */
@media (max-width: 767px) {
.overlay {
padding: 0;
}
/* ── Phone ────────────────────────────────────────────────────
A dedicated layout (rendered via a JS fork on phone viewports — see
AnalyticsModal) that matches the other navbar tool sheets: a full-width
panel pinned below the navbar with a compact header, a section dropdown,
and a scrollable body. These classes are only mounted on phone, so no
media query is needed to gate them. */

/* Transparent full-screen catcher for outside taps. Its z-index sits below
the portaled dropdown menu (.dropdown_menu, z-index 50) so the section
picker's menu renders above the sheet rather than behind it. */
.mobileOverlay {
position: fixed;
inset: 0;
z-index: 40;
}

.modal {
width: 100vw;
height: 100dvh;
max-width: none;
max-height: none;
border-radius: 0;
flex-direction: column;
}
.mobilePanel {
position: fixed;
top: calc(var(--navbar-height) + 4px);
left: calc(12px + var(--safe-left));
right: calc(12px + var(--safe-right));
max-height: calc(100dvh - var(--navbar-height) - 16px - var(--safe-bottom));
display: flex;
flex-direction: column;
background-color: var(--secondary);
border: 1px solid var(--separator);
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
overflow: hidden;
}

.sidebar {
flex: 0 0 auto;
padding: calc(12px + var(--safe-top)) 12px 12px;
border-right: none;
border-bottom: 1px solid var(--separator);
}
/* Header — mirrors the shared panel header (Production/Read-aloud/Saves). */
.mobileHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--separator);
flex-shrink: 0;
}

.sidebarTitle {
padding: 0 4px;
margin-bottom: 12px;
}
.mobileTitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
font-weight: 600;
color: var(--primary-text);
}

/* Group sections and their items lay out inline as a single
horizontally-scrollable row of tabs. Group labels are dropped. */
.navMenu {
display: flex;
gap: 8px;
padding: 0;
overflow-x: auto;
}
.mobileTitle svg {
color: var(--secondary-text);
}

.navMenu > div {
display: flex;
gap: 8px;
}
.mobileIconBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border: none;
background: none;
color: var(--secondary-text);
cursor: pointer;
border-radius: 4px;
}

.groupLabel {
display: none;
}
.mobileIconBtn:hover {
background-color: var(--tertiary);
color: var(--primary-text);
}

.navItem {
width: auto;
white-space: nowrap;
}
/* Section picker row. */
.mobileToolbar {
padding: 12px 16px;
border-bottom: 1px solid var(--separator);
flex-shrink: 0;
}

.content {
flex: 1;
min-height: 0;
padding: 20px 16px calc(20px + var(--safe-bottom));
}
/* Constrains the dropdown so it doesn't stretch the full panel width. */
.mobileTabField {
width: 200px;
max-width: 100%;
}

.scrollArea {
padding-right: 0;
}
.tabSelectTrigger {
padding: 8px 12px;
background: var(--primary);
border: 1px solid var(--separator);
border-radius: 8px;
color: var(--primary-text);
font-size: 0.9rem;
}

/* Single column: two-up grids can't fit a phone width. */
.tabOption {
display: flex;
align-items: center;
gap: 12px;
}

/* Scrollable body — padded on the right so content clears the scrollbar. */
.mobileScroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 16px 20px 16px 16px;
}

/* Single column: two-up grids can't fit a phone-width panel. */
@media (max-width: 767px) {
.statsGridWide {
grid-template-columns: 1fr;
}
Expand Down
88 changes: 79 additions & 9 deletions components/analytics/AnalyticsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"use client";

import { useEffect, useState, ReactNode } from "react";
import { createPortal } from "react-dom";
import { BarChart2, Clapperboard, Users, MapPin, FileBarChart, X } from "lucide-react";

import { useIsPhone } from "@src/lib/utils/hooks";
import Dropdown, { DropdownOption } from "@components/utils/Dropdown";

import ScenesStats from "./stats/ScenesStats";
import CharactersStats from "./stats/CharactersStats";
import LocationsStats from "./stats/LocationsStats";
Expand Down Expand Up @@ -52,6 +56,19 @@ const TAB_TITLES: Record<AnalyticsTab, string> = {
reports: "Reports",
};

// Flattened tabs for the mobile section dropdown (kept in sync with MENU).
const TAB_OPTIONS: DropdownOption[] = MENU.flatMap((section) =>
section.items.map((item) => ({
value: item.id,
label: (
<span className={styles.tabOption}>
<span className={styles.iconWrapper}>{item.icon}</span>
{item.label}
</span>
),
})),
);

// ── Reports placeholder ───────────────────────────────────────────────────────

function ReportsPlaceholder() {
Expand All @@ -74,6 +91,10 @@ interface AnalyticsModalProps {

export default function AnalyticsModal({ isOpen, onClose }: AnalyticsModalProps) {
const [activeTab, setActiveTab] = useState<AnalyticsTab>("scenes");
const [mounted, setMounted] = useState(false);
const isPhone = useIsPhone();

useEffect(() => setMounted(true), []);

// ESC key closes the modal
useEffect(() => {
Expand All @@ -85,9 +106,62 @@ export default function AnalyticsModal({ isOpen, onClose }: AnalyticsModalProps)
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, onClose]);

if (!isOpen) return null;
if (!isOpen || !mounted) return null;

return (
const activeStats = (
<>
{activeTab === "scenes" && <ScenesStats />}
{activeTab === "characters" && <CharactersStats />}
{activeTab === "locations" && <LocationsStats />}
{activeTab === "reports" && <ReportsPlaceholder />}
</>
);

// ── Mobile ──────────────────────────────────────────────────────────────
// A distinct layout that matches the other navbar tool sheets (Production,
// Read-aloud, Saves): a full-width panel pinned below the navbar with a
// compact header, a section dropdown, and a scrollable body. Kept separate
// from the desktop two-pane modal because the structures don't overlap.
// The overlay carries a low z-index (below the portaled dropdown menu) so
// the section picker's menu renders above the sheet, not behind it.
if (isPhone) {
return createPortal(
<div className={styles.mobileOverlay} onClick={onClose}>
<div className={styles.mobilePanel} onClick={(e) => e.stopPropagation()}>
<div className={styles.mobileHeader}>
<span className={styles.mobileTitle}>
<BarChart2 size={15} />
Analytics
</span>
<button className={styles.mobileIconBtn} onClick={onClose} aria-label="Close">
<X size={16} />
</button>
</div>

<div className={styles.mobileToolbar}>
<div className={styles.mobileTabField}>
<Dropdown
value={activeTab}
onChange={(value) => setActiveTab(value as AnalyticsTab)}
options={TAB_OPTIONS}
className={styles.tabSelectTrigger}
fitContent
portal
/>
</div>
</div>

<div className={styles.mobileScroll}>{activeStats}</div>
</div>
</div>,
document.body,
);
}

// ── Desktop ─────────────────────────────────────────────────────────────
// Portal to <body> so the overlay's fixed positioning escapes the navbar's
// transform/stacking context (which otherwise becomes its containing block).
return createPortal(
<div className={styles.overlay} onClick={onClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>

Expand Down Expand Up @@ -124,15 +198,11 @@ export default function AnalyticsModal({ isOpen, onClose }: AnalyticsModalProps)
<X className={styles.close_btn} onClick={onClose} />
</header>

<div className={styles.scrollArea}>
{activeTab === "scenes" && <ScenesStats />}
{activeTab === "characters" && <CharactersStats />}
{activeTab === "locations" && <LocationsStats />}
{activeTab === "reports" && <ReportsPlaceholder />}
</div>
<div className={styles.scrollArea}>{activeStats}</div>
</div>

</div>
</div>
</div>,
document.body,
);
}
Loading
Loading