wip: bars bars bars

This commit is contained in:
Danny White committed 2025-08-22 12:45:02 +10:00
1 parent 4d5fefe11d
commit ab4d75691e
6 files changed
+169 -61

No files matched your search

@@ -307,13 +307,11 @@ export function GenericChartWithQuery({
const displayData = isLoading ? skeletonData : chartData || []
return (
<div ref={chartRef} className="w-full bg-surface-100 border-y">
<div ref={chartRef} className="w-full bg-200 border-y">
<header className="px-6 py-5">
<h3 className="text-foreground-light text-sm">{title}</h3>
</header>
<div
className={`${view === 'chart' ? 'bg-surface-100' : 'bg-surface-75'} border-b last:border-none`}
>
<div className={`${view === 'chart' ? 'bg-200' : 'bg-surface-75'} border-b last:border-none`}>
{/* Filters and toggle */}
<div className="flex flex-row flex-wrap gap-4 p-4 justify-between">
{filters && activeFilters && setFilterValue && (
@@ -397,7 +395,7 @@ export function GenericChartWithQuery({
width: `calc(max(0.5%, (var(--bar-value) / var(--reference)) * 100%))`,
transform: shouldAnimateBars ? 'scaleX(1)' : 'scaleX(0)',
transformOrigin: 'left',
transition: `transform 0.4s steps(${Math.max(2, Math.floor((item.value / maxValue) * 12))}, end) ${index * 0.05}s`,
transition: `transform 0.5s steps(${Math.max(2, Math.floor((item.value / maxValue) * 12))}, end) ${index * 0.05}s`,
}}
>
{/* Foreground pattern for the filled portion */}
@@ -25,7 +25,7 @@ export function SurveyChapter({
return (
<section
id={`chapter-${number}`}
className="flex flex-col bg-200 border-t last-of-type:border-b pt-40"
className="flex flex-col bg-alternative border-t last-of-type:border-b pt-40"
>
<div className="flex flex-col container max-w-[60rem] mx-auto px-6 lg:px-0">
<header className="mx-auto flex flex-col gap-4 text-center text-balance">
@@ -4,14 +4,38 @@ export function SurveyRankedAnswersPair({
rankedAnswersPair: Array<{ label: string; answers: string[] }>
}) {
return (
<aside className="flex flex-row flex-wrap divide-x divide-muted divide-opacity-50">
{/* <div className="flex flex-row bg-surface-100 border border-default rounded-md divide-x"> */}
<aside className="flex flex-col sm:flex-row flex-wrap divide-x divide-muted divide-opacity-50">
{rankedAnswersPair.map((item, index) => (
<div key={index} className="flex flex-col gap-3 flex-1 px-6 py-8">
<div
key={index}
className={`flex gap-3 flex-1 px-6 py-8 ${index % 2 === 0 ? 'flex-col' : 'flex-col sm:flex-col-reverse'}`}
>
{/* Progress bar */}
{/* Filled portion of the bar */}
<div className={`flex flex-row ${index % 2 === 0 ? 'items-start' : 'items-end'}`}>
{item.answers.map((answer, answerIndex) => (
<div
key={answerIndex}
className={`h-${(answerIndex + 1) * 4} w-full relative bg-surface-100`}
>
<div
className={`absolute inset-0 pointer-events-none ${answerIndex === 0 ? 'bg-brand' : answerIndex === 1 ? 'bg-brand-500' : 'bg-brand-300'}`}
style={{
maskImage: 'url("/survey/pattern-front.svg")',
maskSize: '14.5px 15px',
maskRepeat: 'repeat',
maskPosition: 'top left',
}}
/>
</div>
))}
</div>
<ol className="flex flex-col gap-3">
{item.answers.map((answer, answerIndex) => (
<li key={answerIndex} className="font-mono text-brand">
#{answerIndex + 1}. {answer}
<li key={answerIndex} className="flex flex-col gap-2">
<span className="text-sm font-mono text-brand">#{answerIndex + 1}</span>
<span className="text-lg text-foreground">{answer}</span>
</li>
))}
</ol>
@@ -8,7 +8,7 @@ export function SurveyStatCard({
}: {
unit?: string
label: string
progressValue?: number
progressValue: number
maxValue?: number
}) {
const [displayValue, setDisplayValue] = useState(0)
@@ -68,34 +68,53 @@ export function SurveyStatCard({
return (
<div ref={cardRef} className="flex-1 px-6 py-8 flex flex-col gap-4">
{/* Progress bar */}
{progressValue !== undefined && (
{/* Entire bar (including background) */}
<div
className="h-2 relative overflow-hidden"
style={
{
'--bar-value': progressValue,
'--reference': maxValue,
} as React.CSSProperties
}
>
{/* Background pattern for the entire bar */}
<div
className="h-[6px] flex items-center md:mr-12"
style={
{
background: `repeating-linear-gradient(
45deg,
transparent,
transparent 3px,
color-mix(in srgb, hsl(var(--foreground-muted)) 50%, transparent) 3px,
color-mix(in srgb, hsl(var(--foreground-muted)) 50%, transparent) 4px
)`,
} as React.CSSProperties
}
className="absolute inset-0 pointer-events-none bg-foreground-muted"
style={{
maskImage: 'url("/survey/pattern-back.svg")',
maskSize: '15px 15px',
maskRepeat: 'repeat',
maskPosition: 'center',
}}
/>
{/* Filled portion of the bar */}
<div
className={`h-full relative bg-surface-100`}
style={{
width: `calc(max(0.5%, (var(--bar-value) / var(--reference)) * 100%))`,
transform: shouldAnimateBar ? 'scaleX(1)' : 'scaleX(0)',
transformOrigin: 'left',
transition: `transform 0.5s steps(${Math.max(2, Math.floor((progressValue / maxValue) * 12))}, end)`,
}}
>
{/* Foreground pattern for the filled portion */}
<div
className="h-full bg-brand origin-left"
className={`absolute inset-0 pointer-events-none bg-brand`}
style={{
width: `calc(max(0.5%, (${progressValue} / ${maxValue}) * 100%))`,
transform: shouldAnimateBar ? 'scaleX(1)' : 'scaleX(0)',
transition: 'transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
maskImage: 'url("/survey/pattern-front.svg")',
maskSize: '14.5px 15px',
maskRepeat: 'repeat',
maskPosition: 'top left',
}}
/>
</div>
)}
</div>
{/* Text */}
<div className="flex flex-col gap-2">
<p
className={`md:-ml-1 md:mt-8 text-2xl md:text-6xl font-mono tracking-tight inline-block flex flex-row items-baseline ${hasAnimated ? 'text-foreground' : 'text-foreground-muted'} transition-colors duration-1000`}
className={`md:-ml-1 md:mt-8 text-2xl md:text-6xl font-mono tracking-tight inline-block flex flex-row items-baseline ${hasAnimated ? 'text-brand' : 'text-foreground-muted'} transition-colors duration-1000`}
>
{displayValue}
<span className="md:text-4xl">{unit}</span>
@@ -1,35 +1,80 @@
import { useState, useEffect } from 'react'
import { useEffect, useState } from 'react'
export function SurveySummarizedAnswer({ label, answers }: { label: string; answers: string[] }) {
const [currentIndex, setCurrentIndex] = useState(0)
const [isAnimating, setIsAnimating] = useState(false)
useEffect(() => {
if (answers.length <= 1) return
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % answers.length)
}, 3000)
}, 4000)
return () => clearInterval(interval)
}, [answers.length])
if (answers.length <= 1) {
return (
<div className="flex flex-col gap-3 px-6 py-8">
<p className="font-mono text-brand text-2xl">{answers[0]}</p>
<p className="text-foreground-light text-sm">{label}</p>
</div>
)
}
useEffect(() => {
// First, reset the bar to 0%
setIsAnimating(false)
// Then start the animation after a brief delay to ensure the reset is applied
const startTimer = setTimeout(() => {
setIsAnimating(true)
}, 50)
// Stop the animation after 3 seconds
const stopTimer = setTimeout(() => {
setIsAnimating(false)
}, 4050)
return () => {
clearTimeout(startTimer)
clearTimeout(stopTimer)
}
}, [currentIndex])
return (
<div className="flex flex-col gap-3 px-6 py-8">
<div className="min-h-[1.5rem] flex items-center">
<p className="font-mono text-brand text-2xl transition-opacity duration-500">
{answers[currentIndex]}
</p>
<div className="flex flex-col gap-3 px-3 py-8 sm:items-center sm:text-center">
<p className="text-foreground text-xl transition-opacity duration-500">
{answers[currentIndex]}
</p>
<div className="w-24 h-1 relative overflow-hidden">
{/* Background pattern for the entire bar */}
<div
className="absolute inset-0 pointer-events-none bg-foreground-muted"
style={{
maskImage: 'url("/survey/pattern-back.svg")',
maskSize: '15px 15px',
maskRepeat: 'repeat',
maskPosition: 'center',
}}
/>
{/* Filled portion of the bar - timer-based */}
<div
key={currentIndex}
className={`h-full relative bg-surface-100`}
style={{
width: '100%',
transform: isAnimating ? 'scaleX(1)' : 'scaleX(0)',
transformOrigin: 'left',
transition: isAnimating ? 'transform 3s steps(3, end)' : 'transform 0s',
}}
>
{/* Foreground pattern for the filled portion */}
<div
className={`absolute inset-0 pointer-events-none bg-brand`}
style={{
maskImage: 'url("/survey/pattern-front.svg")',
maskSize: '14.5px 15px',
maskRepeat: 'repeat',
maskPosition: 'top left',
}}
/>
</div>
</div>
<p className="text-foreground-light text-sm">{label}</p>
<p className="w-full text-foreground-lighter text-sm font-mono uppercase">{label}</p>
</div>
)
}
@@ -128,43 +128,65 @@ export function SurveyWordCloud({
}, [isRotating, answers, scramblingTexts, currentItems])
return (
<aside className="flex flex-col gap-8 px-3 md:px-6">
<p className="text-foreground-lighter text-sm">{label}</p>
<aside className="flex flex-col gap-12 px-3 md:px-6">
<ol className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-x-16 gap-y-10">
{currentItems.map(({ text, count }, index) => (
<li key={`${text}-${index}`} className={`flex flex-col gap-2 items-start`}>
{/* Progress bar */}
{/* Entire bar (including background) */}
{count && (
<div
className="h-[2px] flex items-center w-full"
className="w-full h-1 relative overflow-hidden"
style={
{
background: `repeating-linear-gradient(
45deg,
transparent,
transparent 3px,
color-mix(in srgb, hsl(var(--foreground-muted)) 50%, transparent) 3px,
color-mix(in srgb, hsl(var(--foreground-muted)) 50%, transparent) 4px
)`,
'--bar-value': count,
'--reference': maxCount,
} as React.CSSProperties
}
>
{/* Background pattern for the entire bar */}
<div
className="h-full bg-brand origin-left"
className="absolute inset-0 pointer-events-none bg-foreground-muted"
style={{
maskImage: 'url("/survey/pattern-back.svg")',
maskSize: '15px 15px',
maskRepeat: 'repeat',
maskPosition: 'center',
}}
/>
{/* Filled portion of the bar */}
<div
className={`h-full relative bg-surface-100`}
style={{
width: `calc(max(0.5%, (${count} / ${maxCount}) * 100%))`,
transform: isRotating ? 'scaleX(1)' : 'scaleX(0)',
transition: 'transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
transformOrigin: 'left',
transition: `transform 0.5s steps(${Math.max(2, Math.floor((count / maxCount) * 12))}, end)`,
}}
/>
>
{/* Foreground pattern for the filled portion */}
<div
className={`absolute inset-0 pointer-events-none bg-brand`}
style={{
maskImage: 'url("/survey/pattern-front.svg")',
maskSize: '14.5px 15px',
maskRepeat: 'repeat',
maskPosition: 'top left',
}}
/>
</div>
</div>
)}
<span className="font-mono md:text-lg text-center text-foreground">
<span className="font-mono md:text-lg text-foreground">
{scramblingTexts[index].toUpperCase() || text.toUpperCase()}
</span>
</li>
))}
</ol>
<p className="w-full text-foreground-lighter text-sm font-mono uppercase sm:text-center">
{label}
</p>
</aside>
)
}