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
143 changes: 55 additions & 88 deletions components/user/brain/UserPageBrainActivityHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import {
LOADING_HEATMAP_WEEK_COUNT,
HEATMAP_VIEWPORT_CLASS_NAME,
LOADING_MONTH_HEADER_SEGMENTS,
MONTH_LABEL_MIN_SPACING_PX,
MONTH_LABEL_ROW_HEIGHT_PX,
TOOLTIP_DATE_FORMATTER,
TOOLTIP_STYLE,
type HeatmapTooltipData,
getHeatmapColumnLeftPx,
getHeatmapGridWidthPx,
getCellFrameClassName,
getCellNodeClassName,
getCellTooltipAnchorProps,
Expand All @@ -33,7 +34,7 @@ import {
} from "./userPageBrainActivityHeatmap.helpers";
import { useHeatmapViewport } from "./userPageBrainActivityHeatmap.viewport";

const LOADING_COLUMN_STRIDE_PX = CELL_SIZE_PX + CELL_GAP_PX;
const LOADING_GRID_WIDTH_PX = getHeatmapGridWidthPx(LOADING_HEATMAP_WEEK_COUNT);
const LOADING_HEATMAP_CELLS = Array.from(
{ length: LOADING_HEATMAP_WEEK_COUNT * 7 },
(_, index) => {
Expand All @@ -51,34 +52,29 @@ const LOADING_HEATMAP_CELLS = Array.from(
);

export function UserPageBrainActivityHeatmapLoading() {
const { viewportRef, viewportMetrics } = useHeatmapViewport("loading");
const { viewportRef } = useHeatmapViewport("loading");

return (
<div
className="tw-flex tw-gap-3 tw-px-4"
aria-label="Loading activity heatmap"
>
<HeatmapDayLabels />
<div className="tw-relative tw-min-w-0 tw-flex-1">
<HeatmapLoadingMonthLabels
scrollLeft={viewportMetrics.scrollLeft}
clientWidth={viewportMetrics.clientWidth}
/>
<div ref={viewportRef} className={HEATMAP_VIEWPORT_CLASS_NAME}>
<div className={HEATMAP_CONTENT_CLASS_NAME}>
<div
className="tw-grid tw-animate-pulse tw-grid-flow-col tw-grid-rows-7"
aria-hidden="true"
style={HEATMAP_GRID_STYLE}
>
{LOADING_HEATMAP_CELLS.map((cell) => (
<HeatmapLoadingCell
key={cell.key}
isPadding={cell.isPadding}
isAccent={cell.isAccent}
/>
))}
</div>
<div ref={viewportRef} className={HEATMAP_VIEWPORT_CLASS_NAME}>
<div className={HEATMAP_CONTENT_CLASS_NAME}>
<HeatmapLoadingMonthLabels />
<div
className="tw-grid tw-animate-pulse tw-grid-flow-col tw-grid-rows-7"
aria-hidden="true"
style={HEATMAP_GRID_STYLE}
>
{LOADING_HEATMAP_CELLS.map((cell) => (
<HeatmapLoadingCell
key={cell.key}
isPadding={cell.isPadding}
isAccent={cell.isAccent}
/>
))}
</div>
</div>
</div>
Expand All @@ -92,45 +88,37 @@ export function UserPageBrainActivityHeatmap({
activity: UserPageBrainActivityViewModel;
}>) {
const tooltipId = useId();
const { viewportRef, viewportMetrics } = useHeatmapViewport(
activity.resetKey
);
const { viewportRef } = useHeatmapViewport(activity.resetKey);

return (
<div className="tw-flex tw-gap-3 tw-px-4">
<HeatmapDayLabels />
<div className="tw-relative tw-min-w-0 tw-flex-1">
<HeatmapMonthLabels
activity={activity}
scrollLeft={viewportMetrics.scrollLeft}
clientWidth={viewportMetrics.clientWidth}
/>
<div ref={viewportRef} className={HEATMAP_VIEWPORT_CLASS_NAME}>
<div className={HEATMAP_CONTENT_CLASS_NAME}>
<div
className="tw-grid tw-grid-flow-col tw-grid-rows-7"
role="img"
aria-label={`Public activity heatmap for ${activity.periodLabel}`}
style={HEATMAP_GRID_STYLE}
>
{activity.cells.map((cell) => (
<HeatmapCell key={cell.key} cell={cell} tooltipId={tooltipId} />
))}
</div>
<Tooltip
id={tooltipId}
place="top"
offset={8}
opacity={1}
style={TOOLTIP_STYLE}
render={({ activeAnchor }) => {
const tooltipData = getHeatmapTooltipData(activeAnchor);
return tooltipData ? (
<HeatmapTooltipContent {...tooltipData} />
) : null;
}}
/>
<div ref={viewportRef} className={HEATMAP_VIEWPORT_CLASS_NAME}>
<div className={HEATMAP_CONTENT_CLASS_NAME}>
<HeatmapMonthLabels activity={activity} />
<div
className="tw-grid tw-grid-flow-col tw-grid-rows-7"
role="img"
aria-label={`Public activity heatmap for ${activity.periodLabel}`}
style={HEATMAP_GRID_STYLE}
>
{activity.cells.map((cell) => (
<HeatmapCell key={cell.key} cell={cell} tooltipId={tooltipId} />
))}
</div>
<Tooltip
id={tooltipId}
place="top"
offset={8}
opacity={1}
style={TOOLTIP_STYLE}
render={({ activeAnchor }) => {
const tooltipData = getHeatmapTooltipData(activeAnchor);
return tooltipData ? (
<HeatmapTooltipContent {...tooltipData} />
) : null;
}}
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -237,39 +225,22 @@ function HeatmapTooltipContent({
);
}

function HeatmapLoadingMonthLabels({
scrollLeft,
clientWidth,
}: Readonly<{
scrollLeft: number;
clientWidth: number;
}>) {
const visibleSegments = LOADING_MONTH_HEADER_SEGMENTS.filter((segment) => {
const leftPx = segment.labelColumn * LOADING_COLUMN_STRIDE_PX - scrollLeft;
const rightPx = leftPx + segment.widthPx;

if (clientWidth <= 0) {
return true;
}

return rightPx >= 0 && leftPx <= clientWidth;
});

function HeatmapLoadingMonthLabels() {
return (
<div
className="tw-relative tw-mb-1 tw-overflow-hidden"
aria-hidden="true"
style={{
height: `${MONTH_LABEL_ROW_HEIGHT_PX}px`,
minWidth: `${MONTH_LABEL_MIN_SPACING_PX}px`,
width: `${LOADING_GRID_WIDTH_PX}px`,
}}
>
{visibleSegments.map((segment) => (
{LOADING_MONTH_HEADER_SEGMENTS.map((segment) => (
<span
key={segment.key}
className="tw-absolute tw-bottom-0 tw-h-[5px] tw-animate-pulse tw-rounded-full tw-bg-white/[0.08]"
style={{
left: `${segment.labelColumn * LOADING_COLUMN_STRIDE_PX - scrollLeft}px`,
left: `${getHeatmapColumnLeftPx(segment.labelColumn)}px`,
width: `${segment.widthPx}px`,
}}
/>
Expand All @@ -280,24 +251,20 @@ function HeatmapLoadingMonthLabels({

function HeatmapMonthLabels({
activity,
scrollLeft,
clientWidth,
}: Readonly<{
activity: UserPageBrainActivityViewModel;
scrollLeft: number;
clientWidth: number;
}>) {
const placedMonthLabels = getPlacedMonthLabels(
activity.monthLabels,
scrollLeft,
clientWidth
);
const placedMonthLabels = getPlacedMonthLabels(activity.monthLabels);
const gridWidthPx = getHeatmapGridWidthPx(activity.weekCount);

return (
<div
className="tw-relative tw-mb-1 tw-overflow-hidden tw-text-[9px] tw-font-semibold tw-uppercase tw-tracking-widest tw-text-iron-600"
aria-hidden="true"
style={{ height: `${MONTH_LABEL_ROW_HEIGHT_PX}px` }}
style={{
height: `${MONTH_LABEL_ROW_HEIGHT_PX}px`,
width: `${gridWidthPx}px`,
}}
>
{placedMonthLabels.map((label) => (
<span
Expand Down
4 changes: 2 additions & 2 deletions components/user/brain/UserPageBrainSidebarMobileStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ function UserPageBrainSidebarMobileWavePill({
prefetch={false}
className="tw-group tw-inline-flex tw-h-9 tw-max-w-[14rem] tw-items-center tw-gap-2 tw-rounded-lg tw-border tw-border-solid tw-border-white/[0.08] tw-bg-iron-950 tw-p-1 tw-pr-3 tw-no-underline tw-shadow-sm tw-transition-all tw-duration-200 desktop-hover:hover:tw-border-white/[0.15] desktop-hover:hover:tw-bg-white/[0.05]"
>
<div className="tw-relative tw-flex tw-h-7 tw-w-7 tw-shrink-0 tw-items-center tw-justify-center tw-overflow-hidden tw-rounded-full tw-border tw-border-solid tw-border-white/10 tw-bg-iron-900">
<div className="tw-relative tw-flex tw-h-7 tw-w-7 tw-shrink-0 tw-items-center tw-justify-center tw-overflow-hidden tw-rounded-full tw-border tw-border-solid tw-border-white/[0.04] tw-bg-iron-900 tw-shadow-sm tw-transition-colors desktop-hover:group-hover:tw-border-white/[0.1]">
{imageSrc ? (
<Image
src={imageSrc}
alt={wave.name ? `Wave ${wave.name}` : "Wave picture"}
fill
sizes="28px"
className="tw-object-cover tw-transition-transform tw-duration-300 desktop-hover:group-hover:tw-scale-105"
className="tw-object-cover"
/>
) : (
<FallbackIcon className="tw-h-3.5 tw-w-3.5 tw-flex-shrink-0 tw-text-iron-300" />
Expand Down
7 changes: 3 additions & 4 deletions components/user/brain/UserPageBrainSidebarWaveItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,18 @@ export default function UserPageBrainSidebarWaveItem({
prefetch={false}
className="tw-group tw-flex tw-cursor-pointer tw-items-center tw-gap-3 tw-rounded-xl tw-border tw-border-solid tw-border-white/10 tw-bg-iron-950/80 tw-p-3 tw-no-underline tw-shadow-2xl tw-transition-all desktop-hover:hover:tw-border-white/15"
>
<div className="tw-relative tw-h-10 tw-w-10 tw-shrink-0 tw-overflow-hidden tw-rounded-full tw-border tw-border-solid tw-border-white/10">
<div className="tw-absolute tw-inset-0 tw-z-10 tw-transition-colors desktop-hover:group-hover:tw-bg-transparent" />
<div className="tw-relative tw-h-10 tw-w-10 tw-shrink-0 tw-overflow-hidden tw-rounded-full tw-border tw-border-solid tw-border-white/[0.04] tw-bg-iron-900 tw-shadow-sm tw-transition-colors desktop-hover:group-hover:tw-border-white/[0.1]">
{imageSrc ? (
<Image
src={imageSrc}
alt={wave.name ? `Wave ${wave.name}` : "Wave picture"}
fill
sizes="40px"
className="tw-object-cover tw-transition-transform tw-duration-500 tw-will-change-transform desktop-hover:group-hover:tw-scale-110"
className="tw-object-cover"
/>
) : (
<div className="tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-bg-iron-900">
<FallbackIcon className="tw-h-4 tw-w-4 tw-flex-shrink-0 tw-text-iron-300 tw-transition-transform tw-duration-500 tw-will-change-transform desktop-hover:group-hover:tw-scale-110" />
<FallbackIcon className="tw-h-4 tw-w-4 tw-flex-shrink-0 tw-text-iron-300" />
</div>
)}
</div>
Expand Down
42 changes: 19 additions & 23 deletions components/user/brain/userPageBrainActivityHeatmap.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export const LOADING_MONTH_HEADER_SEGMENTS = [
{ key: "feb", labelColumn: 44, widthPx: 18 },
{ key: "mar", labelColumn: 48, widthPx: 20 },
] as const;
export const MONTH_LABEL_MIN_SPACING_PX = 34;
const MONTH_LABEL_OVERFLOW_TOLERANCE_PX = 18;
const MONTH_LABEL_MIN_SPACING_PX = 34;
export const TOOLTIP_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
Expand All @@ -62,8 +61,8 @@ export const TOOLTIP_STYLE = {
pointerEvents: "none",
} as const;
export const HEATMAP_VIEWPORT_CLASS_NAME =
"tw-[scrollbar-gutter:stable] tw-flex-1 tw-overflow-x-auto tw-overflow-y-hidden tw-pb-3 tw-scrollbar-thin tw-scrollbar-track-transparent tw-scrollbar-thumb-iron-700/60 desktop-hover:hover:tw-scrollbar-thumb-iron-600/80";
export const HEATMAP_CONTENT_CLASS_NAME = "tw-inline-flex tw-pr-2";
"horizontal-menu-scrollable-x tw-[scrollbar-gutter:stable] tw-flex-1 tw-overflow-x-auto tw-overflow-y-hidden tw-overscroll-x-contain tw-pb-3 tw-scrollbar-thin tw-scrollbar-track-transparent tw-scrollbar-thumb-iron-700/60 [touch-action:pan-x] desktop-hover:hover:tw-scrollbar-thumb-iron-600/80";
export const HEATMAP_CONTENT_CLASS_NAME = "tw-inline-flex tw-flex-col tw-pr-2";
export const HEATMAP_GRID_STYLE = {
gridTemplateRows: `repeat(7, ${CELL_SIZE_PX}px)`,
gridAutoColumns: `${CELL_SIZE_PX}px`,
Expand All @@ -74,6 +73,18 @@ export const CELL_FRAME_STYLE = {
height: `${CELL_SIZE_PX}px`,
} as const;

export function getHeatmapColumnLeftPx(column: number): number {
return column * COLUMN_STRIDE_PX;
}

export function getHeatmapGridWidthPx(weekCount: number): number {
if (weekCount <= 0) {
return 0;
}

return weekCount * COLUMN_STRIDE_PX - CELL_GAP_PX;
}

// Cell presentation
const CELL_NODE_CLASS_NAMES: Readonly<
Record<
Expand Down Expand Up @@ -183,7 +194,7 @@ export function getCellFrameClassName(cell: UserPageBrainActivityCell): string {
}

return clsx(
"tw-flex tw-items-center tw-justify-center tw-rounded tw-transition-colors tw-duration-200",
"tw-flex tw-items-center tw-justify-center tw-rounded desktop-hover:tw-transition-colors desktop-hover:tw-duration-200",
cell.ariaLabel &&
"tw-cursor-pointer desktop-hover:hover:tw-z-20 desktop-hover:hover:tw-bg-[#171b21]"
);
Expand All @@ -195,36 +206,21 @@ export function getCellNodeClassName(
): string {
return clsx(
variant === "grid" &&
"tw-transform-gpu tw-transition-all tw-duration-200 tw-ease-out",
"desktop-hover:tw-transform-gpu desktop-hover:tw-transition-all desktop-hover:tw-duration-200 desktop-hover:tw-ease-out",
CELL_NODE_CLASS_NAMES[variant][cell.intensity]
);
}

// Month label placement
export function getPlacedMonthLabels(
monthLabels: readonly HeatmapMonthLabel[],
scrollLeft: number,
clientWidth: number
monthLabels: readonly HeatmapMonthLabel[]
): readonly PlacedMonthLabel[] {
const placedMonthLabels: PlacedMonthLabel[] = [];
let nextMinimumLeftPx = 0;

for (const label of monthLabels) {
const naturalLeftPx = label.labelColumn * COLUMN_STRIDE_PX - scrollLeft;
const leftPx = getHeatmapColumnLeftPx(label.firstVisibleColumn);

if (
clientWidth > 0 &&
(naturalLeftPx < -MONTH_LABEL_OVERFLOW_TOLERANCE_PX ||
naturalLeftPx > clientWidth)
) {
continue;
}

const leftPx = Math.max(-MONTH_LABEL_OVERFLOW_TOLERANCE_PX, naturalLeftPx);

if (clientWidth > 0 && leftPx >= clientWidth) {
continue;
}
if (leftPx < nextMinimumLeftPx) {
continue;
}
Expand Down
Loading
Loading