Untitled

 avatar
unknown
plain_text
12 days ago
6.5 kB
3
Indexable
'use client'

import { useEffect, useState, useMemo } from 'react'
import { useTheme } from 'next-themes'
import { Label, PolarGrid, PolarRadiusAxis, RadialBar, RadialBarChart } from "recharts"
import { Card, CardContent } from "@/components/ui/card"
import { ChartContainer, ChartConfig } from "@/components/ui/chart"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { useSettings } from "@/hooks/useSettings"
import { useTranslation, type SupportedLanguages } from "@/utils/translations"
import { Timer, AlertTriangle, CheckCircle2 } from "lucide-react"
import { format, formatDistanceToNow } from 'date-fns'

interface CircularProgressProps {
  binId: string
  fillLevel: number
  color: string
  darkColor: string
  name: string
  prediction?: {
    predicted_full_time: string
    current_level: number
    fill_rate: number
    updated_at: string
  }
}

const DEFAULT_BINS = ['Biomüll', 'Gelber Sack', 'Papier', 'Restmüll']

export function CircularProgress({ binId, fillLevel, color, darkColor, name, prediction }: CircularProgressProps) {
  const { theme } = useTheme()
  const [progress, setProgress] = useState(0)
  const { settings } = useSettings()
  const { t } = useTranslation(settings?.language as SupportedLanguages || 'EN')

  useEffect(() => {
    const timer = setTimeout(() => setProgress(fillLevel), 100)
    return () => clearTimeout(timer)
  }, [fillLevel])

  const isDarkMode = theme === 'dark'
  const bgColor = isDarkMode ? darkColor : color
  const progressColor = getProgressColor(fillLevel, isDarkMode)

  const displayName = useMemo(() => 
    DEFAULT_BINS.includes(name) ? t(`bins.${name}`) : name,
  [name, t])

  const chartConfig: ChartConfig = useMemo(() => ({
    [binId]: {
      label: displayName,
      color: progressColor,
    },
  }), [binId, displayName, progressColor])

  const chartData = useMemo(() => ([{
    name: `${binId}-progress`,
    value: progress,
    fill: progressColor
  }]), [binId, progress, progressColor])

  const getPredictionStatus = (predictionDate: string) => {
    const timeUntilFull = new Date(predictionDate).getTime() - new Date().getTime()
    const hoursUntilFull = timeUntilFull / (1000 * 60 * 60)

    if (hoursUntilFull <= 1) return { color: 'text-red-500', icon: AlertTriangle }
    if (hoursUntilFull <= 24) return { color: 'text-amber-500', icon: Timer }
    return { color: 'text-green-500', icon: CheckCircle2 }
  }

  const tooltipContent = prediction ? (
    <div className="flex flex-col gap-2 p-2">
      <div className="flex items-center gap-2">
        {(() => {
          const { color, icon: Icon } = getPredictionStatus(prediction.predicted_full_time)
          return <Icon className={cn("h-4 w-4", color)} />
        })()}
        <span className="font-medium">
          Full by {format(new Date(prediction.predicted_full_time), 'MMM d, HH:mm')}
        </span>
      </div>
      <div className="text-sm text-muted-foreground">
        ({formatDistanceToNow(new Date(prediction.predicted_full_time), { addSuffix: true })})
      </div>
      <div className="text-xs text-muted-foreground">
        Fill rate: {prediction.fill_rate.toFixed(1)}%/hour
      </div>
    </div>
  ) : null

  return (
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <Card className="border-0 bg-transparent shadow-none w-[460px] min-w-[460px]">
            <CardContent className="p-0">
              <ChartContainer config={chartConfig} className="relative w-full aspect-square">
                <div className="absolute inset-0 flex items-center justify-center">
                  <div
                    className={cn(
                      "rounded-full",
                      isDarkMode ? "opacity-[0.15]" : "opacity-[0.55]"
                    )}
                    style={{
                      backgroundColor: bgColor,
                      width: "57%",
                      height: "57%",
                      position: "absolute",
                      left: "50%",
                      top: "50%",
                      transform: "translate(-50%, -50%)"
                    }}
                  />
                </div>
                <RadialBarChart
                  width="100%"
                  height="100%"
                  data={chartData}
                  startAngle={90}
                  endAngle={90 - (progress * 3.6)}
                  innerRadius="65%"
                  outerRadius="90%"
                  barSize={12}
                >
                  <PolarRadiusAxis tick={false} axisLine={false} tickLine={false} stroke="none">
                    <Label
                      content={({ viewBox }) => {
                        if (viewBox && "cx" in viewBox && "cy" in viewBox) {
                          return (
                            <text
                              x={viewBox.cx}
                              y={viewBox.cy}
                              textAnchor="middle"
                              dominantBaseline="middle"
                              className="fill-foreground text-2xl font-medium"
                            >
                              {displayName}
                            </text>
                          );
                        }
                      }}
                    />
                  </PolarRadiusAxis>
                  <RadialBar
                    background={false}
                    dataKey="value"
                    cornerRadius={15}
                    className="stroke-none"
                  />
                </RadialBarChart>
              </ChartContainer>
            </CardContent>
          </Card>
        </TooltipTrigger>
        {tooltipContent && (
          <TooltipContent>
            {tooltipContent}
          </TooltipContent>
        )}
      </Tooltip>
    </TooltipProvider>
  )
}

function getProgressColor(level: number, isDark: boolean) {
  if (isDark) {
    if (level >= 80) return '#ff3b30'
    if (level >= 60) return '#ff9f0a'
    if (level >= 40) return '#ffd60a'
    return '#30d158'
  } else {
    if (level >= 80) return '#ff453a'
    if (level >= 60) return '#ff9f0a'
    if (level >= 40) return '#ffd60a'
    return '#34c759'
  }
}
Leave a Comment