diff --git a/src/components/DraggableButton.tsx b/src/components/DraggableButton.tsx new file mode 100644 index 0000000..da1e765 --- /dev/null +++ b/src/components/DraggableButton.tsx @@ -0,0 +1,185 @@ +import { css, cx } from "@emotion/css"; +import React, { useEffect, useRef, useState } from "react"; + +export interface DraggableButtonProps + extends React.ButtonHTMLAttributes {} + +interface Pos { + x: number; + y: number; +} + +const relativePos = (pos: Pos, container: DOMRect) => { + return { + x: pos.x - container.left, + y: pos.y - container.top, + }; +}; + +export const DraggableButton = React.forwardRef< + HTMLButtonElement, + DraggableButtonProps +>(({ children, ...props }, ref) => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const myRef = useRef(null); + const containerRef = useRef(null); + const [isInitialized, setIsInitialized] = useState(false); + + // Convert initial position and set up element on mount + useEffect(() => { + const el = myRef.current; + if (!el || isInitialized) return; + + const rect = el.getBoundingClientRect(); + const parentRect = el.parentElement?.getBoundingClientRect() ?? rect; + + // Convert current position to top/left + const top = rect.top - parentRect.top; + const left = rect.left - parentRect.left; + + // Set position and clear any bottom/right values + el.style.position = "absolute"; + el.style.top = `${top}px`; + el.style.left = `${left}px`; + el.style.bottom = "unset"; + el.style.right = "unset"; + + setPosition({ x: left, y: top }); + setIsInitialized(true); + }, []); + + useEffect(() => { + const el = myRef.current; + if (!el || !isInitialized) return; + + containerRef.current = el.parentElement?.getBoundingClientRect() ?? null; + + el.style.transition = "none"; + el.style.left = `${position.x}px`; + el.style.top = `${position.y}px`; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setIsDragging(false); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [position, isInitialized]); + + const getEventPos = ( + e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, + ): Pos => { + if ("touches" in e) { + return { + x: e.touches[0].clientX, + y: e.touches[0].clientY, + }; + } + return { + x: e.clientX, + y: e.clientY, + }; + }; + + const handleStart = (e: React.MouseEvent | React.TouchEvent) => { + if (!myRef.current || !containerRef.current) return; + + // Prevent scrolling on touch devices + if ("touches" in e) { + e.preventDefault(); + } + + setIsDragging(true); + + const eventPos = getEventPos(e); + const relative = relativePos(eventPos, containerRef.current); + + setDragOffset({ + x: relative.x - position.x, + y: relative.y - position.y, + }); + }; + + const handleMove = (e: MouseEvent | TouchEvent) => { + if (!isDragging || !containerRef.current) return; + + // Prevent scrolling on touch devices + if ("touches" in e) { + e.preventDefault(); + } + + const eventPos = getEventPos(e); + const relative = relativePos(eventPos, containerRef.current); + + setPosition({ + x: relative.x - dragOffset.x, + y: relative.y - dragOffset.y, + }); + }; + + const handleEnd = () => { + setIsDragging(false); + }; + + useEffect(() => { + if (isDragging) { + // Mouse events + window.addEventListener("mousemove", handleMove); + window.addEventListener("mouseup", handleEnd); + + // Touch events + window.addEventListener("touchmove", handleMove, { passive: false }); + window.addEventListener("touchend", handleEnd); + window.addEventListener("touchcancel", handleEnd); + } + + return () => { + // Mouse events + window.removeEventListener("mousemove", handleMove); + window.removeEventListener("mouseup", handleEnd); + + // Touch events + window.removeEventListener("touchmove", handleMove); + window.removeEventListener("touchend", handleEnd); + window.removeEventListener("touchcancel", handleEnd); + }; + }, [isDragging, dragOffset]); + + return ( + + ); +}); diff --git a/src/index.css b/src/index.css index abbec0d..92a3076 100644 --- a/src/index.css +++ b/src/index.css @@ -94,4 +94,6 @@ a:hover { button { cursor: pointer; + border: none; + text-align: left; } diff --git a/src/pages/main/Contact.tsx b/src/pages/main/Contact.tsx index ef4ebc8..33ff632 100644 --- a/src/pages/main/Contact.tsx +++ b/src/pages/main/Contact.tsx @@ -1,7 +1,10 @@ import React from "react"; -import { css } from "@emotion/css"; +import { css, cx } from "@emotion/css"; import { useEffect, useState } from "react"; import Container from "../../components/Container"; +import { setupCursorTracking } from "../../util"; +import { ReactComponent as Logo } from "../../assets/logo.svg"; +import { DraggableButton } from "../../components/DraggableButton"; const A = css` text-decoration: none; @@ -83,14 +86,47 @@ const Home: React.FC = () => { min-height: 50vh; display: flex; flex-direction: column; + position: relative; `}>

MKRhere

-
+ + + { li { list-style: none; - min-width: 5rem; + min-width: 4rem; max-width: 100%; } @@ -118,7 +154,9 @@ const Home: React.FC = () => { margin-block-start: 1rem; } } - `}> + `} + ref={setupCursorTracking}> +
    { className={A} href={value.link} target="_blank" - rel="noreferrer"> + rel="noreferrer" + style={{ width: "fit-content" }}> {value.value} ) : ( @@ -150,7 +189,7 @@ const Home: React.FC = () => { ); })}
-
+
); };