基礎元件介面都能處理。細節需要逐步展開處理。

https://v0.build/4Mt2YNQ

prompt: use react, tailwindcss, css3, canvas, programming. iterate on the design to make it match the image as closely as possible.
traning tips: In this updated version, I've made the following changes to match the UI more closely.
import React, { useEffect, useRef, useState } from 'react';

export default function SleepTracker() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [sleepTime, setSleepTime] = useState<Date>(new Date(2023, 0, 1, 0, 0));
  const [wakeTime, setWakeTime] = useState<Date>(new Date(2023, 0, 1, 7, 0));
  const [isDragging, setIsDragging] = useState<'sleep' | 'wake' | null>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const drawSleepTracker = () => {
      // Clear canvas
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // Set background
      ctx.fillStyle = '#000000';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // Draw sleep time
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 44px Arial';
      ctx.textAlign = 'left';
      ctx.fillText(formatTime(sleepTime), 20, 70);

      // Draw wake time
      ctx.textAlign = 'right';
      ctx.fillText(formatTime(wakeTime), canvas.width - 20, 70);

      // Draw sleep icon (moon)
      drawMoonIcon(ctx, 30, 110);

      // Draw wake icon (bell)
      drawBellIcon(ctx, canvas.width - 30, 110);

      // Draw "就寝時刻" text
      ctx.fillStyle = '#ffffff';
      ctx.font = '16px Arial';
      ctx.textAlign = 'left';
      ctx.fillText('就寝時刻', 60, 115);

      // Draw "起床時刻" text
      ctx.textAlign = 'right';
      ctx.fillText('起床時刻', canvas.width - 60, 115);

      // Draw clock
      const centerX = canvas.width / 2;
      const centerY = canvas.height / 2 + 60;
      const radius = 140;

      // Draw clock circle
      ctx.strokeStyle = '#333333';
      ctx.lineWidth = 4;
      ctx.beginPath();
      ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
      ctx.stroke();

      // Draw sleep arc
      const startAngle = (sleepTime.getHours() * 60 + sleepTime.getMinutes()) * Math.PI / 720 - Math.PI / 2;
      const endAngle = (wakeTime.getHours() * 60 + wakeTime.getMinutes()) * Math.PI / 720 - Math.PI / 2;
      const gradient = ctx.createLinearGradient(centerX - radius, centerY, centerX + radius, centerY);
      gradient.addColorStop(0, '#ff8c00');
      gradient.addColorStop(0.6, '#ffd700');
      gradient.addColorStop(1, '#ffff00');
      ctx.strokeStyle = gradient;
      ctx.lineWidth = 14;
      ctx.beginPath();
      ctx.arc(centerX, centerY, radius, startAngle, endAngle);
      ctx.stroke();

      // Draw clock numbers and tick marks
      ctx.fillStyle = '#ffffff';
      ctx.font = '20px Arial';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      for (let i = 0; i < 60; i++) {
        const angle = i * Math.PI / 30;
        const innerRadius = i % 5 === 0 ? radius - 15 : radius - 5;
        const x1 = centerX + Math.cos(angle) * radius;
        const y1 = centerY + Math.sin(angle) * radius;
        const x2 = centerX + Math.cos(angle) * innerRadius;
        const y2 = centerY + Math.sin(angle) * innerRadius;
        
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.strokeStyle = '#666666';
        ctx.lineWidth = 1;
        ctx.stroke();

        if (i % 15 === 0) {
          const x = centerX + Math.cos(angle) * (radius - 30);
          const y = centerY + Math.sin(angle) * (radius - 30);
          ctx.fillText((i / 5 || 12).toString(), x, y);
        }
      }

      // Draw moon icon at sleep time
      const sleepIconX = centerX + Math.cos(startAngle) * (radius + 15);
      const sleepIconY = centerY + Math.sin(startAngle) * (radius + 15);
      drawMoonIcon(ctx, sleepIconX, sleepIconY, 12);

      // Draw bell icon at wake time
      const wakeIconX = centerX + Math.cos(endAngle) * (radius + 15);
      const wakeIconY = centerY + Math.sin(endAngle) * (radius + 15);
      drawBellIcon(ctx, wakeIconX, wakeIconY, 12);

      // Draw sleep duration
      const duration = calculateDuration(sleepTime, wakeTime);
      ctx.fillStyle = '#ffffff';
      ctx.font = 'bold 40px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(`${duration.toFixed(1)}時間`, centerX, centerY);

      // Draw draggable handles
      drawHandle(ctx, centerX, centerY, radius, sleepTime, '#ffa500');
      drawHandle(ctx, centerX, centerY, radius, wakeTime, '#ffd700');
    };

    drawSleepTracker();
  }, [sleepTime, wakeTime]);

  const formatTime = (date: Date) => {
    return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
  };

  const calculateDuration = (start: Date, end: Date) => {
    let duration = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
    if (duration < 0) duration += 24;
    return duration;
  };

  const drawMoonIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number = 18) => {
    ctx.fillStyle = '#ffa500';
    ctx.beginPath();
    ctx.arc(x, y, size, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#000000';
    ctx.beginPath();
    ctx.arc(x + size / 3, y, size * 0.8, 0, Math.PI * 2);
    ctx.fill();
  };

  const drawBellIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number = 18) => {
    const scale = size / 18;
    ctx.fillStyle = '#ffd700';
    ctx.beginPath();
    ctx.moveTo(x - 12 * scale, y + 10 * scale);
    ctx.lineTo(x + 12 * scale, y + 10 * scale);
    ctx.lineTo(x + 15 * scale, y - 3 * scale);
    ctx.lineTo(x + 10 * scale, y - 15 * scale);
    ctx.lineTo(x - 10 * scale, y - 15 * scale);
    ctx.lineTo(x - 15 * scale, y - 3 * scale);
    ctx.closePath();
    ctx.fill();
    ctx.fillStyle = '#000000';
    ctx.beginPath();
    ctx.arc(x, y + 12 * scale, 4 * scale, 0, Math.PI * 2);
    ctx.fill();
  };

  const drawHandle = (ctx: CanvasRenderingContext2D, centerX: number, centerY: number, radius: number, time: Date, color: string) => {
    const angle = (time.getHours() * 60 + time.getMinutes()) * Math.PI / 720 - Math.PI / 2;
    const x = centerX + Math.cos(angle) * radius;
    const y = centerY + Math.sin(angle) * radius;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, 12, 0, Math.PI * 2);
    ctx.fill();
  };

  const handleMouseDown = (event: React.MouseEvent<HTMLCanvasElement>) => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const rect = canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2 + 60;
    const radius = 140;

    const sleepAngle = (sleepTime.getHours() * 60 + sleepTime.getMinutes()) * Math.PI / 720 - Math.PI / 2;
    const wakeAngle = (wakeTime.getHours() * 60 + wakeTime.getMinutes()) * Math.PI / 720 - Math.PI / 2;

    const sleepX = centerX + Math.cos(sleepAngle) * radius;
    const sleepY = centerY + Math.sin(sleepAngle) * radius;
    const wakeX = centerX + Math.cos(wakeAngle) * radius;
    const wakeY = centerY + Math.sin(wakeAngle) * radius;

    const sleepDist = Math.sqrt((x - sleepX) ** 2 + (y - sleepY) ** 2);
    const wakeDist = Math.sqrt((x - wakeX) ** 2 + (y - wakeY) ** 2);

    if (sleepDist < 15) {
      setIsDragging('sleep');
    } else if (wakeDist < 15) {
      setIsDragging('wake');
    }
  };

  const handleMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
    if (!isDragging) return;

    const canvas = canvasRef.current;
    if (!canvas) return;

    const rect = canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2 + 60;

    const angle = Math.atan2(y - centerY, x - centerX);
    const hours = Math.floor(((angle + Math.PI / 2 + Math.PI * 2) % (Math.PI * 2)) * 12 / Math.PI);
    const minutes = Math.floor((((angle + Math.PI / 2 + Math.PI * 2) % (Math.PI * 2)) * 12 / Math.PI - hours) * 60);

    const newTime = new Date(2023, 0, 1, hours, minutes);

    if (isDragging === 'sleep') {
      setSleepTime(newTime);
    } else {
      setWakeTime(newTime);
    }
  };

  const handleMouseUp = () => {
    setIsDragging(null);
  };

  return (
    <div className="flex justify-center items-center h-screen bg-black">
      <canvas 
        ref={canvasRef} 
        width={350} 
        height={520} 
        className="border border-gray-700 rounded-lg cursor-pointer"
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        onMouseLeave={handleMouseUp}
      />
    </div>
  );
}
點讚的用戶