Refactor tooltip component and integrate context menu triggers for enhanced mobile interactions; update Reader and HoverCards components for improved responsiveness and styling.

This commit is contained in:
pious2847 2025-04-30 02:02:28 +00:00
parent e7c2303da4
commit 4f711a54be
6 changed files with 309 additions and 68 deletions

View File

@ -216,12 +216,9 @@ export default function Reader() {
<div className="w-full h-[80px] fixed top-0 z-30 flex items-center justify-between px-6 bg-transparent">
{/* Logo */}
<Link href="/creator" >
<div className="flex items-center text-white">
<Link href="/creator" className="mr-4 ">
<Button size="icon" className="bg-white">
<ArrowLeft2 size="24" color="#555555" />
</Button>
</Link>
<Image
src="/images/logo2.png"
alt="Wodey"
@ -231,6 +228,7 @@ export default function Reader() {
/>
<span className="text-xl font-semibold">Wodey</span>
</div>
</Link>
{/* Brutal Logo - Center */}
<div className="absolute left-1/2 transform -translate-x-1/2">
@ -255,8 +253,7 @@ export default function Reader() {
{pages.map((page, index) => (
<section
key={page.id}
className={`absolute w-full h-full transition-opacity duration-1000 ${
currentPageIndex === index ? "opacity-100 z-10" : "opacity-0 z-0"
className={`absolute w-full h-full transition-opacity duration-1000 ${currentPageIndex === index ? "opacity-100 z-10" : "opacity-0 z-0"
}`}
>
{/* Background Video */}

View File

@ -1,13 +1,16 @@
'use client'
import React from 'react'
import React, { useState, useEffect, useRef } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useContextMenuTriggers } from "@/hooks/useContextMenuTriggers"
export interface HoverCardsProps {
triggerText: string
@ -26,17 +29,108 @@ export default function HoverCards({
linkText = 'Purchase & Read More',
isImage
}: HoverCardsProps) {
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const tooltipRef = useRef<HTMLDivElement>(null);
// Check if device is mobile
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
// Set initial value
checkMobile();
// Add event listener for window resize
window.addEventListener('resize', checkMobile);
// Cleanup
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Add click outside listener when tooltip is open
useEffect(() => {
if (isTooltipOpen && isMobile) {
const handleOutsideClick = (e: MouseEvent) => {
if (tooltipRef.current && !tooltipRef.current.contains(e.target as Node)) {
setIsTooltipOpen(false);
}
};
// Add a slight delay before adding the event listener to prevent immediate closing
const timeoutId = setTimeout(() => {
document.addEventListener('click', handleOutsideClick);
}, 100);
return () => {
clearTimeout(timeoutId);
document.removeEventListener('click', handleOutsideClick);
};
}
}, [isTooltipOpen, isMobile]);
// Custom handlers for mobile interactions
const { handlers } = useContextMenuTriggers({
longPressDelay: 500,
doubleClickDelay: 300,
onLongPress: () => {
if (isMobile) {
console.log("Long press triggered tooltip");
setIsTooltipOpen(true);
}
},
onDoubleTap: () => {
if (isMobile) {
console.log("Double tap toggling tooltip");
setIsTooltipOpen(!isTooltipOpen);
}
}
});
// Close tooltip function
const handleCloseTooltip = (e: React.MouseEvent) => {
e.stopPropagation();
setIsTooltipOpen(false);
};
return (
<HoverCard>
<HoverCardTrigger asChild>
<span className="text-blue-400 cursor-pointer underline inline">{triggerText}</span>
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="max-w-xs bg-white rounded-lg overflow-hidden shadow-lg">
<TooltipProvider>
<Tooltip
open={isMobile ? isTooltipOpen : undefined}
onOpenChange={!isMobile ? setIsTooltipOpen : undefined}
>
<TooltipTrigger asChild>
<span
className={`text-blue-400 cursor-pointer underline inline relative ${
isMobile ? 'after:content-[""] after:absolute after:bottom-[-3px] after:left-0 after:w-full after:h-[2px] after:bg-blue-400 after:opacity-70' : ''
}`}
{...(isMobile ? handlers : {})}
title={isMobile ? "Double tap or long press to view details" : "Hover to view details"}
>
{triggerText}
</span>
</TooltipTrigger>
<TooltipContent className="w-80 tooltip-content p-0" ref={tooltipRef}>
<div className="max-w-xs bg-white rounded-lg overflow-hidden shadow-lg relative">
{isMobile && (
<button
className="absolute top-2 right-2 z-10 bg-gray-800 text-white rounded-full w-6 h-6 flex items-center justify-center"
onClick={handleCloseTooltip}
aria-label="Close"
>
</button>
)}
<div className="relative h-48 w-full">
{isImage ?
<Image src={videourl} alt={videourl} width={100} height={100} className="absolute top-0 left-0 w-full h-full object-cover" />
:
{isImage ? (
<Image
src={videourl}
alt="Content preview"
layout="fill"
objectFit="cover"
/>
) : (
<video
className="absolute top-0 left-0 w-full h-full object-cover"
muted
@ -44,10 +138,8 @@ export default function HoverCards({
playsInline
autoPlay
src={videourl}
></video>
}
/>
)}
</div>
<div className="p-4">
@ -62,7 +154,8 @@ export default function HoverCards({
</Link>
</div>
</div>
</HoverCardContent>
</HoverCard>
)
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

61
components/ui/tooltip.tsx Normal file
View File

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -5,19 +5,26 @@ import { useRef, useCallback, useState } from 'react';
interface UseContextMenuTriggersProps {
longPressDelay?: number;
doubleClickDelay?: number;
onLongPress?: () => void;
onDoubleTap?: () => void;
}
export const useContextMenuTriggers = ({
longPressDelay = 500, // Default long press delay in ms
doubleClickDelay = 300, // Default double click delay in ms
onLongPress,
onDoubleTap
}: UseContextMenuTriggersProps = {}) => {
const [isLongPressing, setIsLongPressing] = useState(false);
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
const lastClickTimeRef = useRef<number>(0);
const targetRef = useRef<HTMLElement | null>(null);
const preventClickRef = useRef<boolean>(false);
// Function to simulate a right-click event
const simulateRightClick = useCallback((element: HTMLElement) => {
console.log("Simulating right click");
// Create and dispatch a custom contextmenu event
const contextMenuEvent = new MouseEvent('contextmenu', {
bubbles: true,
@ -31,6 +38,7 @@ export const useContextMenuTriggers = ({
// Handle touch start (for long press)
const handleTouchStart = useCallback((e: React.TouchEvent) => {
console.log("Touch start detected");
if (e.touches.length === 1) {
const element = e.currentTarget as HTMLElement;
targetRef.current = element;
@ -41,18 +49,27 @@ export const useContextMenuTriggers = ({
}
setIsLongPressing(true);
preventClickRef.current = false;
// Start a timer for long press
longPressTimerRef.current = setTimeout(() => {
if (isLongPressing && targetRef.current) {
if (targetRef.current) {
console.log("Long press detected");
preventClickRef.current = true;
if (onLongPress) {
onLongPress();
} else {
simulateRightClick(targetRef.current);
}
}
}, longPressDelay);
}
}, [longPressDelay, isLongPressing, simulateRightClick]);
}, [longPressDelay, simulateRightClick, onLongPress]);
// Handle touch end (cancel long press)
const handleTouchEnd = useCallback(() => {
console.log("Touch end detected");
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current);
}
@ -61,27 +78,41 @@ export const useContextMenuTriggers = ({
// Handle touch move (cancel long press if moving)
const handleTouchMove = useCallback(() => {
console.log("Touch move detected");
if (longPressTimerRef.current) {
clearTimeout(longPressTimerRef.current);
}
setIsLongPressing(false);
}, []);
// Handle double click
// Handle click and double click/tap
const handleClick = useCallback((e: React.MouseEvent) => {
if (preventClickRef.current) {
preventClickRef.current = false;
return;
}
const currentTime = new Date().getTime();
const element = e.currentTarget as HTMLElement;
// Check if this is a double click
if (currentTime - lastClickTimeRef.current < doubleClickDelay) {
console.log("Double tap detected");
// Double click detected
if (onDoubleTap) {
onDoubleTap();
} else {
simulateRightClick(element);
}
lastClickTimeRef.current = 0; // Reset to prevent triple-click issues
} else {
// First click
console.log("Single tap detected");
lastClickTimeRef.current = currentTime;
}
}, [doubleClickDelay, simulateRightClick]);
}, [doubleClickDelay, simulateRightClick, onDoubleTap]);
return {
handlers: {

View File

@ -10,11 +10,12 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.7",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-context-menu": "^2.2.12",
"@radix-ui/react-hover-card": "^1.1.11",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"iconsax-react": "^0.0.8",

View File

@ -26,6 +26,9 @@ importers:
'@radix-ui/react-tabs':
specifier: ^1.1.9
version: 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-tooltip':
specifier: ^1.2.4
version: 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -608,6 +611,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.2.4':
resolution: {integrity: sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@ -689,6 +705,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-visually-hidden@1.2.0':
resolution: {integrity: sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
@ -2671,6 +2700,26 @@ snapshots:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-tooltip@1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0)
'@radix-ui/react-visually-hidden': 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.1.0)':
dependencies:
react: 19.1.0
@ -2732,6 +2781,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.2
'@radix-ui/react-visually-hidden@1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.2
'@types/react-dom': 19.1.2(@types/react@19.1.2)
'@radix-ui/rect@1.1.1': {}
'@rtsao/scc@1.1.0': {}