Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
import type React from "react";
import { useEffect, useRef, useState, useCallback } from "react";
import { createPortal } from "react-dom";
interface DropdownProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
/** Reference element for positioning (button that triggers dropdown) */
anchorRef?: React.RefObject<HTMLElement>;
/** Placement: 'left' | 'right' | 'bottom-left' | 'bottom-right' */
placement?: 'left' | 'right' | 'bottom-left' | 'bottom-right';
}
export const Dropdown: React.FC<DropdownProps> = ({
isOpen,
onClose,
children,
className = "",
anchorRef,
placement = 'right',
}) => {
const dropdownRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
// Calculate position based on anchor element
const updatePosition = useCallback(() => {
if (!isOpen || !anchorRef?.current || !dropdownRef.current) return;
const anchorRect = anchorRef.current.getBoundingClientRect();
const dropdownRect = dropdownRef.current.getBoundingClientRect();
// For fixed positioning, coordinates are relative to viewport (no scroll offset needed)
let top = anchorRect.bottom + 8; // 8px = mt-2 equivalent
let left = anchorRect.left;
// Handle placement - get width from actual render or fallback to className parsing
let dropdownWidth = dropdownRect.width;
if (!dropdownWidth || dropdownWidth === 0) {
// Try to extract width from className (e.g., "w-64" = 256px, "w-48" = 192px)
const widthMatch = className.match(/w-(\d+)/);
if (widthMatch) {
const widthValue = parseInt(widthMatch[1], 10);
dropdownWidth = widthValue * 4; // Tailwind: w-64 = 16rem = 256px, so w-N = N*4px
} else {
dropdownWidth = 192; // Default fallback
}
}
if (placement === 'right') {
// Align dropdown right edge to button right edge
left = anchorRect.right - dropdownWidth;
} else if (placement === 'bottom-right') {
// Position below and align to right edge of button
left = anchorRect.right - dropdownWidth;
} else if (placement === 'bottom-left') {
// Position below and align to left edge of button
left = anchorRect.left;
} else {
// 'left' - align dropdown left edge to button left edge
left = anchorRect.left;
}
// Edge detection - flip if would overflow viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const dropdownHeight = dropdownRect.height || 200;
// Check right edge
if (left + dropdownWidth > viewportWidth) {
left = anchorRect.left - dropdownWidth;
}
// Check bottom edge
if (anchorRect.bottom + dropdownHeight > viewportHeight) {
// Position above instead
top = anchorRect.top - dropdownHeight - 8;
}
// Ensure doesn't go off left edge
if (left < 0) {
left = 8;
}
setPosition({ top, left });
}, [isOpen, anchorRef, placement]);
useEffect(() => {
if (isOpen && anchorRef?.current) {
// Update on scroll/resize
const handleUpdate = () => updatePosition();
window.addEventListener('scroll', handleUpdate, true);
window.addEventListener('resize', handleUpdate);
// Use multiple requestAnimationFrame calls to ensure dropdown is rendered and measured
let rafId2: number | undefined;
const rafId1 = requestAnimationFrame(() => {
updatePosition();
// Second frame to ensure dimensions are calculated
rafId2 = requestAnimationFrame(() => {
updatePosition();
});
});
return () => {
window.removeEventListener('scroll', handleUpdate, true);
window.removeEventListener('resize', handleUpdate);
cancelAnimationFrame(rafId1);
if (rafId2 !== undefined) cancelAnimationFrame(rafId2);
};
}
}, [isOpen, anchorRef, placement, updatePosition]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
!(event.target as HTMLElement).closest(".dropdown-toggle")
) {
onClose();
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
const dropdownContent = (
<div
ref={dropdownRef}
className={`fixed z-[999999] rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark ${className}`}
style={{
top: `${position.top}px`,
left: `${position.left}px`,
}}
>
{children}
</div>
);
// Portal to body to escape overflow constraints
return createPortal(dropdownContent, document.body);
};

View File

@@ -0,0 +1,46 @@
import type React from "react";
import { Link } from "react-router";
interface DropdownItemProps {
tag?: "a" | "button";
to?: string;
onClick?: () => void;
onItemClick?: () => void;
baseClassName?: string;
className?: string;
children: React.ReactNode;
}
export const DropdownItem: React.FC<DropdownItemProps> = ({
tag = "button",
to,
onClick,
onItemClick,
baseClassName = "block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900",
className = "",
children,
}) => {
const combinedClasses = `${baseClassName} ${className}`.trim();
const handleClick = (event: React.MouseEvent) => {
if (tag === "button") {
event.preventDefault();
}
if (onClick) onClick();
if (onItemClick) onItemClick();
};
if (tag === "a" && to) {
return (
<Link to={to} className={combinedClasses} onClick={handleClick}>
{children}
</Link>
);
}
return (
<button onClick={handleClick} className={combinedClasses}>
{children}
</button>
);
};