From 3e7b2beca1136a42700a7e1aebfe4c0fb2861a00 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sat, 15 Nov 2025 18:00:09 +0000
Subject: Initial commit

---
 frontend/src/components/HeartLogo.tsx | 270 ++++++++++++++++++++++++++++++++++
 1 file changed, 270 insertions(+)
 create mode 100644 frontend/src/components/HeartLogo.tsx

(limited to 'frontend/src/components/HeartLogo.tsx')

diff --git a/frontend/src/components/HeartLogo.tsx b/frontend/src/components/HeartLogo.tsx
new file mode 100644
index 0000000..2d407f7
--- /dev/null
+++ b/frontend/src/components/HeartLogo.tsx
@@ -0,0 +1,270 @@
+import React, { useState, useEffect } from 'react'
+
+interface HeartLogoProps {
+  size?: 'small' | 'medium' | 'large' | 'header' | 'header-no-rays'
+  className?: string
+  onClick?: () => void
+}
+
+export function HeartLogo({ size = 'small', className = '', onClick }: HeartLogoProps) {
+  const [animationOffset, setAnimationOffset] = useState(0)
+  const [isAnimating, setIsAnimating] = useState(false)
+
+  // Click handler to toggle animation
+  const handleClick = () => {
+    setIsAnimating(!isAnimating)
+    if (onClick) {
+      onClick()
+    }
+  }
+
+  // Animation for header beams
+  useEffect(() => {
+    if (size !== 'header-no-rays' || !isAnimating) return
+
+    let animationId: number
+    let startTime = Date.now()
+
+    const animate = () => {
+      const elapsed = Date.now() - startTime
+      const rotationSpeed = 0.02 // Slow rotation speed
+      const offset = (elapsed * rotationSpeed) % 360
+      setAnimationOffset(offset)
+      animationId = requestAnimationFrame(animate)
+    }
+
+    animationId = requestAnimationFrame(animate)
+
+    return () => {
+      if (animationId) {
+        cancelAnimationFrame(animationId)
+      }
+    }
+  }, [size, isAnimating])
+  if (size === 'header') {
+    // Header version with rays filling the entire header rectangle
+    return (
+      <div className={`heart-logo ${size} ${className}`}>
+        <div className="heart-logo-rectangle">
+          <div className="heart-logo-content">
+            <div className="heart-logo-rays">
+              <svg viewBox="0 0 100 100" className="heart-logo-rays-svg" preserveAspectRatio="none">
+                {/* Header rays matching loading screen exactly */}
+                <g transform="translate(50, 50)">
+                  {[...Array(16)].map((_, i) => {
+                    const angle = (i * 22.5);
+                    const rayWidth = 2; // Much thinner rays
+
+                    // Create triangular ray path - thin rays extending to header edges
+                    const startAngle = angle - rayWidth;
+                    const endAngle = angle + rayWidth;
+
+                    const innerRadius = 0; // Start from center (heart center)
+                    const outerRadius = 200; // Extend far beyond to reach all header edges
+
+                    const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+                    const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+                    const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+                    const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+                    const x3 = Math.cos(startAngle * Math.PI / 180) * outerRadius;
+                    const y3 = Math.sin(startAngle * Math.PI / 180) * outerRadius;
+                    const x4 = Math.cos(endAngle * Math.PI / 180) * outerRadius;
+                    const y4 = Math.sin(endAngle * Math.PI / 180) * outerRadius;
+
+                    return (
+                      <polygon
+                        key={i}
+                        points={`${x1},${y1} ${x2},${y2} ${x4},${y4} ${x3},${y3}`}
+                        fill={i % 2 === 0 ? "#8B0000" : "#000000"}
+                        opacity="0.9"
+                      />
+                    );
+                  })}
+                </g>
+              </svg>
+            </div>
+            <div className="heart-logo-outline">
+              <svg viewBox="0 0 100 100" className="heart-logo-svg">
+                <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+                      fill="#8B0000"
+                      stroke="#8B0000"
+                      strokeWidth="2"/>
+              </svg>
+            </div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  if (size === 'header-no-rays') {
+    // Header version with small red beams - compact sunlight effect
+    return (
+      <div className={`heart-logo ${size} ${className}`} onClick={handleClick} style={{ cursor: 'pointer' }}>
+        <div className="heart-logo-content">
+          <div className="heart-logo-rays">
+            <svg viewBox="-200 -67 400 134" className="heart-logo-rays-svg" preserveAspectRatio="xMidYMid slice">
+              {/* Red beams extending across entire header rectangle - optimized for wide screens */}
+              <g>
+                {[...Array(32)].map((_, i) => {
+                  const angle = (i * 11.25) + animationOffset; // 32 rays, 11.25 degrees apart, animated
+                  const rayWidth = 2; // Much narrower rays to show background between them
+
+                  // Create triangular ray path
+                  const startAngle = angle - rayWidth;
+                  const endAngle = angle + rayWidth;
+
+                  const innerRadius = 0; // Start from heart center
+
+                  // Calculate intersection with rectangle bounds for wide header
+                  const getIntersectionWithRect = (angleInDegrees: number) => {
+                    const rad = angleInDegrees * Math.PI / 180;
+                    const dx = Math.cos(rad);
+                    const dy = Math.sin(rad);
+
+                    // Header rectangle bounds (wide format ~3:1 ratio)
+                    const rectBoundsX = 200; // Full width
+                    const rectBoundsY = 67;  // Height to match header proportions
+
+                    // Calculate intersection with each edge
+                    let t = Infinity;
+
+                    // Right edge (x = rectBoundsX)
+                    if (dx > 0) {
+                      t = Math.min(t, rectBoundsX / dx);
+                    }
+                    // Left edge (x = -rectBoundsX)
+                    if (dx < 0) {
+                      t = Math.min(t, -rectBoundsX / dx);
+                    }
+                    // Top edge (y = -rectBoundsY)
+                    if (dy < 0) {
+                      t = Math.min(t, -rectBoundsY / dy);
+                    }
+                    // Bottom edge (y = rectBoundsY)
+                    if (dy > 0) {
+                      t = Math.min(t, rectBoundsY / dy);
+                    }
+
+                    return { x: dx * t, y: dy * t };
+                  };
+
+                  const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+                  const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+                  const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+                  const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+
+                  const edge1 = getIntersectionWithRect(startAngle);
+                  const edge2 = getIntersectionWithRect(endAngle);
+
+                  return (
+                    <polygon
+                      key={i}
+                      points={`${x1},${y1} ${x2},${y2} ${edge2.x},${edge2.y} ${edge1.x},${edge1.y}`}
+                      fill={i % 2 === 0 ? "#DC2626" : "#B91C1C"}
+                      opacity="0.6"
+                    />
+                  );
+                })}
+              </g>
+            </svg>
+          </div>
+          <div className="heart-logo-outline">
+            <svg viewBox="0 0 100 100" className="heart-logo-svg">
+              <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+                    fill="#8B0000"
+                    stroke="#8B0000"
+                    strokeWidth="2"/>
+            </svg>
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  // Original square version for other sizes
+  return (
+    <div className={`heart-logo ${size} ${className}`}>
+      <div className="heart-logo-rectangle">
+        <div className="heart-logo-content">
+          <div className="heart-logo-rays">
+            <svg viewBox="-100 -67 200 134" className="heart-logo-rays-svg">
+              {/* Rising Sun flag style rays - triangular rays extending to rectangle edges */}
+              <g>
+                {[...Array(16)].map((_, i) => {
+                  const angle = (i * 22.5);
+                  const rayWidth = 11.25; // Half the angle between rays for triangular shape
+
+                  // Create triangular ray path
+                  const startAngle = angle - rayWidth;
+                  const endAngle = angle + rayWidth;
+
+                  const innerRadius = 0; // Start from center (heart center)
+
+                  // Calculate intersection with rectangle bounds
+                  // Rectangle aspect ratio 120:80 = 3:2, so use different bounds for x and y
+                  const getIntersectionWithRect = (angleInDegrees: number) => {
+                    const rad = angleInDegrees * Math.PI / 180;
+                    const dx = Math.cos(rad);
+                    const dy = Math.sin(rad);
+
+                    // Rectangle bounds matching the 120x80 aspect ratio (3:2)
+                    const rectBoundsX = 100; // Full width
+                    const rectBoundsY = 67;  // 2/3 of width to maintain 3:2 ratio
+
+                    // Calculate intersection with each edge
+                    let t = Infinity;
+
+                    // Right edge (x = rectBoundsX)
+                    if (dx > 0) {
+                      t = Math.min(t, rectBoundsX / dx);
+                    }
+                    // Left edge (x = -rectBoundsX)
+                    if (dx < 0) {
+                      t = Math.min(t, -rectBoundsX / dx);
+                    }
+                    // Top edge (y = -rectBoundsY)
+                    if (dy < 0) {
+                      t = Math.min(t, -rectBoundsY / dy);
+                    }
+                    // Bottom edge (y = rectBoundsY)
+                    if (dy > 0) {
+                      t = Math.min(t, rectBoundsY / dy);
+                    }
+
+                    return { x: dx * t, y: dy * t };
+                  };
+
+                  const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+                  const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+                  const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+                  const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+
+                  const edge1 = getIntersectionWithRect(startAngle);
+                  const edge2 = getIntersectionWithRect(endAngle);
+
+                  return (
+                    <polygon
+                      key={i}
+                      points={`${x1},${y1} ${x2},${y2} ${edge2.x},${edge2.y} ${edge1.x},${edge1.y}`}
+                      fill={i % 2 === 0 ? "#8B0000" : "#000000"}
+                      opacity="0.9"
+                    />
+                  );
+                })}
+              </g>
+            </svg>
+          </div>
+          <div className="heart-logo-outline">
+            <svg viewBox="0 0 100 100" className="heart-logo-svg">
+              <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+                    fill="#8B0000"
+                    stroke="#8B0000"
+                    strokeWidth="2"/>
+            </svg>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

