mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 14:49:10 +03:00
wip: bars bars bars
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user