mirror of
https://github.com/arkorty/Osborne.git
synced 2026-03-18 00:57:14 +00:00
ui fixes yooooooo
This commit is contained in:
@@ -104,16 +104,17 @@ const Home = () => {
|
|||||||
const currentTheme = VSCODE_THEMES[currentThemeIndex];
|
const currentTheme = VSCODE_THEMES[currentThemeIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen flex items-center justify-center bg-background dark:bg-background ui-font">
|
<div className="relative min-h-dvh flex flex-col items-center justify-between bg-background dark:bg-background ui-font">
|
||||||
|
<main className="flex-grow flex items-center justify-center">
|
||||||
<Card className="relative z-10 px-6 md:px-12 py-12 md:py-24 backdrop-blur-sm shadow-lg bg-card/0 bg-opacity-0 dark:bg-card/70 border border-border dark:border-border flex flex-col items-center">
|
<Card className="relative z-10 px-6 md:px-12 py-12 md:py-24 backdrop-blur-sm shadow-lg bg-card/0 bg-opacity-0 dark:bg-card/70 border border-border dark:border-border flex flex-col items-center">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<h1 className="text-6xl md:text-8xl translate-x-1.5 font-bold text-foreground mb-4">
|
<h1 className="text-6xl md:text-8xl translate-x-1.5 font-bold text-foreground mb-6 md:mb-8">
|
||||||
Osborne
|
Osborne
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Theme Switcher - Pill Button */}
|
{/* Theme Switcher - Pill Button */}
|
||||||
<div className="mb-12">
|
<div className="mb-6 md:mb-8">
|
||||||
<button
|
<button
|
||||||
onClick={nextTheme}
|
onClick={nextTheme}
|
||||||
className="px-4 min-w-36 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm font-medium text-foreground transition-colors border border-border/50 hover:border-border"
|
className="px-4 min-w-36 py-2 bg-muted hover:bg-muted/80 rounded-full text-sm font-medium text-foreground transition-colors border border-border/50 hover:border-border"
|
||||||
@@ -165,6 +166,7 @@ const Home = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</main>
|
||||||
<LegalFooter
|
<LegalFooter
|
||||||
onDisclaimerOpen={() => setIsDisclaimerOpen(true)}
|
onDisclaimerOpen={() => setIsDisclaimerOpen(true)}
|
||||||
onDMCAOpen={() => setIsDMCAOpen(true)}
|
onDMCAOpen={() => setIsDMCAOpen(true)}
|
||||||
|
|||||||
@@ -11,13 +11,16 @@ import {
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
Card,
|
||||||
HoverCardContent,
|
CardContent,
|
||||||
HoverCardTrigger,
|
CardHeader,
|
||||||
} from "@/components/ui/hover-card";
|
CardTitle,
|
||||||
|
CardFooter,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
WifiOff,
|
WifiOff,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
TriangleAlert,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { CommentsPanel } from "@/components/RightPanel";
|
import { CommentsPanel } from "@/components/RightPanel";
|
||||||
@@ -35,6 +38,7 @@ import dotenv from "dotenv";
|
|||||||
import { JetBrains_Mono } from "next/font/google";
|
import { JetBrains_Mono } from "next/font/google";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import { ContentWarningModal } from "@/components/ContentWarningModal";
|
import { ContentWarningModal } from "@/components/ContentWarningModal";
|
||||||
|
import { BetterHoverCard, HoverCardProvider } from "@/components/ui/BetterHoverCard";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -210,7 +214,7 @@ const Room = () => {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isPurgeModalOpen, setIsPurgeModalOpen] = useState(false);
|
const [isPurgeModalOpen, setIsPurgeModalOpen] = useState(false);
|
||||||
const [showDisconnectToast, setShowDisconnectToast] = useState(false);
|
const [showReconnectOverlay, setShowReconnectOverlay] = useState(false);
|
||||||
const [currentThemeId, setCurrentThemeId] = useState("one-dark");
|
const [currentThemeId, setCurrentThemeId] = useState("one-dark");
|
||||||
const [selectedLineStart, setSelectedLineStart] = useState<number>();
|
const [selectedLineStart, setSelectedLineStart] = useState<number>();
|
||||||
const [selectedLineEnd, setSelectedLineEnd] = useState<number>();
|
const [selectedLineEnd, setSelectedLineEnd] = useState<number>();
|
||||||
@@ -223,6 +227,8 @@ const Room = () => {
|
|||||||
const [rightPanelForced, setRightPanelForced] = useState(false);
|
const [rightPanelForced, setRightPanelForced] = useState(false);
|
||||||
const [popupMessage, setPopupMessage] = useState<{text: string; type?: 'default' | 'warning'} | null>(null);
|
const [popupMessage, setPopupMessage] = useState<{text: string; type?: 'default' | 'warning'} | null>(null);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [fileSizeError, setFileSizeError] = useState<string | null>(null);
|
||||||
|
const [purgeError, setPurgeError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Detect mobile screen size
|
// Detect mobile screen size
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -380,25 +386,19 @@ const Room = () => {
|
|||||||
}
|
}
|
||||||
}, [currentThemeId]);
|
}, [currentThemeId]);
|
||||||
|
|
||||||
// Show disconnect toast only if still disconnected after a delay
|
// Show reconnect overlay only if still disconnected after a delay
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let showTimer: NodeJS.Timeout | null = null;
|
let showTimer: NodeJS.Timeout | null = null;
|
||||||
let hideTimer: NodeJS.Timeout | null = null;
|
|
||||||
if (status === "Disconnected") {
|
if (status === "Disconnected") {
|
||||||
// Wait 800ms before showing toast
|
// Wait 800ms before showing overlay
|
||||||
showTimer = setTimeout(() => {
|
showTimer = setTimeout(() => {
|
||||||
setShowDisconnectToast(true);
|
setShowReconnectOverlay(true);
|
||||||
// Auto-hide after 10 seconds
|
|
||||||
hideTimer = setTimeout(() => {
|
|
||||||
setShowDisconnectToast(false);
|
|
||||||
}, 10000);
|
|
||||||
}, 800);
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
setShowDisconnectToast(false);
|
setShowReconnectOverlay(false);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (showTimer) clearTimeout(showTimer);
|
if (showTimer) clearTimeout(showTimer);
|
||||||
if (hideTimer) clearTimeout(hideTimer);
|
|
||||||
};
|
};
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
@@ -707,7 +707,7 @@ const Room = () => {
|
|||||||
router.push("/");
|
router.push("/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error purging room:", error);
|
console.error("Error purging room:", error);
|
||||||
showPopup("Failed to purge room", "warning");
|
setPurgeError("Failed to purge room");
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsPurgeModalOpen(false);
|
setIsPurgeModalOpen(false);
|
||||||
@@ -725,7 +725,7 @@ const Room = () => {
|
|||||||
// Check file size limit
|
// Check file size limit
|
||||||
if (file.size > maxFileSize) {
|
if (file.size > maxFileSize) {
|
||||||
const fileSizeInMB = (file.size / (1024 * 1024)).toFixed(2);
|
const fileSizeInMB = (file.size / (1024 * 1024)).toFixed(2);
|
||||||
showPopup(`File "${file.name}" (${fileSizeInMB}MB) exceeds 10MB limit`, 'warning');
|
setFileSizeError(`File "${file.name}" (${fileSizeInMB}MB) exceeds 10MB limit`);
|
||||||
continue; // Skip this file and continue with others
|
continue; // Skip this file and continue with others
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,6 +795,7 @@ const Room = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<HoverCardProvider>
|
||||||
<div className="relative min-h-screen bg-background dark:bg-background ui-font">
|
<div className="relative min-h-screen bg-background dark:bg-background ui-font">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 transition-all duration-300"
|
className="absolute inset-0 transition-all duration-300"
|
||||||
@@ -810,8 +811,8 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-between p-1 w-full">
|
<div className="flex flex-row items-center justify-between p-1 w-full">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCardTrigger>
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
className="text-foreground bg-secondary px-2 py-0 h-5 rounded-sm text-xs btn-micro"
|
className="text-foreground bg-secondary px-2 py-0 h-5 rounded-sm text-xs btn-micro"
|
||||||
@@ -822,13 +823,13 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
share
|
share
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground">
|
contentClassName="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground"
|
||||||
copy link to this page
|
>
|
||||||
</HoverCardContent>
|
copy link to clipboard
|
||||||
</HoverCard>
|
</BetterHoverCard>
|
||||||
<HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCardTrigger>
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
className="bg-destructive px-2 py-0 h-5 text-xs rounded-sm hover:bg-destructive/80 btn-micro"
|
className="bg-destructive px-2 py-0 h-5 text-xs rounded-sm hover:bg-destructive/80 btn-micro"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -836,13 +837,13 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
purge
|
purge
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground">
|
contentClassName="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground"
|
||||||
permanently delete this room and all its contents
|
>
|
||||||
</HoverCardContent>
|
permanently delete this room
|
||||||
</HoverCard>
|
</BetterHoverCard>
|
||||||
<HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCardTrigger>
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
className="bg-destructive px-2 py-0 h-5 text-xs rounded-sm hover:bg-destructive/80 btn-micro"
|
className="bg-destructive px-2 py-0 h-5 text-xs rounded-sm hover:bg-destructive/80 btn-micro"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@@ -850,15 +851,15 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
exit
|
exit
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground">
|
contentClassName="py-1 px-2 w-auto text-popover-foreground bg-popover text-xs border-foreground"
|
||||||
|
>
|
||||||
return to home
|
return to home
|
||||||
</HoverCardContent>
|
</BetterHoverCard>
|
||||||
</HoverCard>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCardTrigger>
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
className="bg-chart-2 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-2/80 btn-micro"
|
className="bg-chart-2 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-2/80 btn-micro"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -868,13 +869,13 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
upload
|
upload
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
contentClassName="py-1 px-2 w-auto text-xs border-foreground"
|
||||||
|
>
|
||||||
upload files
|
upload files
|
||||||
</HoverCardContent>
|
</BetterHoverCard>
|
||||||
</HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCard>
|
trigger={
|
||||||
<HoverCardTrigger>
|
|
||||||
<Button
|
<Button
|
||||||
className="bg-chart-4 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-4/80 btn-micro"
|
className="bg-chart-4 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-4/80 btn-micro"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -886,41 +887,41 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
theme
|
theme
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
contentClassName="py-1 px-2 w-auto text-xs border-foreground"
|
||||||
{getThemeById(currentThemeId)?.name || "Switch theme"}
|
>
|
||||||
</HoverCardContent>
|
{`switch to ${getThemeById(getNextTheme(currentThemeId)?.id)?.name}`}
|
||||||
</HoverCard>
|
</BetterHoverCard>
|
||||||
|
|
||||||
{/* Panel Controls for mobile and when panels are hidden due to width */}
|
{/* Panel Controls for mobile and when panels are hidden due to width */}
|
||||||
{(isMobile || !showSidePanels) && (
|
{(isMobile || !showSidePanels) && (
|
||||||
<>
|
<>
|
||||||
<HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCardTrigger>
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
className="bg-chart-1 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-1/80 btn-micro"
|
className="bg-chart-1 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-1/80 btn-micro"
|
||||||
onClick={() => setLeftPanelForced(!leftPanelForced)}
|
onClick={() => setLeftPanelForced(!leftPanelForced)}
|
||||||
>
|
>
|
||||||
media
|
media
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground z-[999]">
|
contentClassName="py-1 px-2 w-auto text-xs border-foreground z-[999]"
|
||||||
toggle users & media panel
|
>
|
||||||
</HoverCardContent>
|
show media
|
||||||
</HoverCard>
|
</BetterHoverCard>
|
||||||
<HoverCard>
|
<BetterHoverCard
|
||||||
<HoverCardTrigger>
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
className="bg-chart-3 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-3/80 btn-micro"
|
className="bg-chart-3 px-2 py-0 h-5 text-xs rounded-sm hover:bg-chart-3/80 btn-micro"
|
||||||
onClick={() => setRightPanelForced(!rightPanelForced)}
|
onClick={() => setRightPanelForced(!rightPanelForced)}
|
||||||
>
|
>
|
||||||
notes
|
notes
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
}
|
||||||
<HoverCardContent className="py-1 px-2 w-auto text-xs border-foreground">
|
contentClassName="py-1 px-2 w-auto text-xs border-foreground"
|
||||||
toggle comments panel
|
>
|
||||||
</HoverCardContent>
|
show comments
|
||||||
</HoverCard>
|
</BetterHoverCard>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -935,8 +936,8 @@ const Room = () => {
|
|||||||
<textarea
|
<textarea
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => handleContentChange(e.target.value)}
|
onChange={(e) => handleContentChange(e.target.value)}
|
||||||
className="flex-grow w-full p-3 bg-background text-foreground border border-border rounded resize-none font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
className="flex-grow w-full p-3 bg-background text-foreground border border-border rounded resize-none font-mono text-sm focus:outline-none focus:border-primary"
|
||||||
placeholder="Start typing your code here..."
|
placeholder="Start typing..."
|
||||||
style={{ fontFamily: 'JetBrains Mono, Consolas, Monaco, "Courier New", monospace' }}
|
style={{ fontFamily: 'JetBrains Mono, Consolas, Monaco, "Courier New", monospace' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -983,7 +984,57 @@ const Room = () => {
|
|||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Custom Popup */}
|
{/* File Size Error Modal */}
|
||||||
|
{fileSizeError && (
|
||||||
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={() => setFileSizeError(null)}>
|
||||||
|
<Card className="max-w-md text-center animate-in fade-in slide-in-from-bottom-4 duration-300" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<CardHeader>
|
||||||
|
<TriangleAlert className="mx-auto mb-2 text-warning" size={48} />
|
||||||
|
<CardTitle className="text-lg text-warning">
|
||||||
|
File Too Large
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 text-sm">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{fileSizeError}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setFileSizeError(null)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Purge Error Modal */}
|
||||||
|
{purgeError && (
|
||||||
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={() => setPurgeError(null)}>
|
||||||
|
<Card className="max-w-md text-center animate-in fade-in slide-in-from-bottom-4 duration-300" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<CardHeader>
|
||||||
|
<TriangleAlert className="mx-auto mb-2 text-destructive" size={48} />
|
||||||
|
<CardTitle className="text-lg text-destructive">
|
||||||
|
Error
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 text-sm">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{purgeError}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setPurgeError(null)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom Popup for non-critical messages */}
|
||||||
{popupMessage && (
|
{popupMessage && (
|
||||||
<div className="fixed top-4 right-4 z-50">
|
<div className="fixed top-4 right-4 z-50">
|
||||||
<div className={`px-3 py-2 border rounded-lg shadow-lg animate-in fade-in slide-in-from-top-2 duration-200 ${
|
<div className={`px-3 py-2 border rounded-lg shadow-lg animate-in fade-in slide-in-from-top-2 duration-200 ${
|
||||||
@@ -1011,6 +1062,7 @@ const Room = () => {
|
|||||||
<LeftPanel
|
<LeftPanel
|
||||||
isVisible={isMobile ? leftPanelForced : showLeftPanel}
|
isVisible={isMobile ? leftPanelForced : showLeftPanel}
|
||||||
users={users}
|
users={users}
|
||||||
|
currentUser={currentUser}
|
||||||
mediaFiles={mediaFiles}
|
mediaFiles={mediaFiles}
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
onFileDelete={handleFileDelete}
|
onFileDelete={handleFileDelete}
|
||||||
@@ -1019,26 +1071,24 @@ const Room = () => {
|
|||||||
|
|
||||||
{/* Purge Confirmation Modal */}
|
{/* Purge Confirmation Modal */}
|
||||||
{isPurgeModalOpen && (
|
{isPurgeModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={() => setIsPurgeModalOpen(false)}>
|
||||||
{/* Blurred overlay */}
|
<Card className="max-w-md animate-in fade-in slide-in-from-bottom-4 duration-300" onClick={(e) => e.stopPropagation()}>
|
||||||
<div
|
<CardHeader>
|
||||||
className="absolute inset-0 bg-background/60 backdrop-blur-sm transition-all duration-300"
|
<CardTitle>Purge Room</CardTitle>
|
||||||
onClick={() => setIsPurgeModalOpen(false)}
|
</CardHeader>
|
||||||
/>
|
<CardContent>
|
||||||
{/* Modal */}
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
<div className="relative bg-card border border-border rounded-lg shadow-lg p-6 max-w-md w-full mx-4 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-4">Purge Room</h2>
|
|
||||||
<p className="text-sm text-muted-foreground mb-6">
|
|
||||||
Are you sure you want to permanently delete this room and all its contents?
|
Are you sure you want to permanently delete this room and all its contents?
|
||||||
This action cannot be undone and will remove:
|
This action cannot be undone and will remove:
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-sm text-muted-foreground mb-6 list-disc list-inside space-y-1">
|
<ul className="text-sm text-muted-foreground mb-4 list-disc list-inside space-y-1">
|
||||||
<li>All code content</li>
|
<li>All code content</li>
|
||||||
<li>All comments</li>
|
<li>All comments</li>
|
||||||
<li>All uploaded files</li>
|
<li>All uploaded files</li>
|
||||||
<li>Room history</li>
|
<li>Room history</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="flex gap-3 justify-end">
|
</CardContent>
|
||||||
|
<CardFooter className="flex gap-3 justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setIsPurgeModalOpen(false)}
|
onClick={() => setIsPurgeModalOpen(false)}
|
||||||
@@ -1053,42 +1103,35 @@ const Room = () => {
|
|||||||
>
|
>
|
||||||
Purge Room
|
Purge Room
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</CardFooter>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comments Panel */}
|
{/* Reconnect Overlay */}
|
||||||
{showDisconnectToast && (
|
{showReconnectOverlay && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
{/* Blurred overlay */}
|
<Card className="max-w-md text-center animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||||
<div className="absolute inset-0 bg-background/60 backdrop-blur-sm pointer-events-auto transition-all duration-300" />
|
<CardHeader>
|
||||||
{/* Toast */}
|
<WifiOff className="mx-auto mb-2 text-destructive" size={48} />
|
||||||
<div
|
<CardTitle className="text-lg text-destructive">
|
||||||
className="relative pointer-events-auto flex items-center space-x-2 px-4 py-3 rounded-lg shadow-lg border animate-in fade-in duration-300"
|
Connection Lost
|
||||||
style={{
|
</CardTitle>
|
||||||
background: "var(--popover, var(--card, #fff))",
|
</CardHeader>
|
||||||
color: "var(--popover-foreground, var(--foreground, #222))",
|
<CardContent className="space-y-4 text-sm">
|
||||||
borderColor: "var(--border, #e5e7eb)",
|
<p className="text-muted-foreground">
|
||||||
borderWidth: 1,
|
The connection to the server was lost. Please check your
|
||||||
borderStyle: "solid",
|
internet connection and try to reconnect.
|
||||||
fontWeight: 500,
|
</p>
|
||||||
width: "auto",
|
<Button
|
||||||
minWidth: undefined,
|
|
||||||
maxWidth: undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WifiOff size={18} className="text-destructive" />
|
|
||||||
<span className="text-sm font-medium">Connection Lost</span>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="ml-2 bg-primary/10 hover:bg-primary/20 text-primary rounded p-1 transition-colors"
|
className="w-full"
|
||||||
title="Refresh to reconnect"
|
|
||||||
style={{ display: "flex", alignItems: "center" }}
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={15} />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
</button>
|
Reconnect
|
||||||
</div>
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1098,6 +1141,7 @@ const Room = () => {
|
|||||||
{/* Content Warning Modal */}
|
{/* Content Warning Modal */}
|
||||||
<ContentWarningModal />
|
<ContentWarningModal />
|
||||||
</div>
|
</div>
|
||||||
|
</HoverCardProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
20
client/components/AnimatedAvatar.tsx
Normal file
20
client/components/AnimatedAvatar.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface AnimatedAvatarProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedAvatar: React.FC<AnimatedAvatarProps> = ({ className }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-8 h-8 rounded-full',
|
||||||
|
'bg-gradient-to-r from-purple-400 via-pink-500 to-red-500',
|
||||||
|
'bg-[length:200%_200%]',
|
||||||
|
'animate-gradient-flow',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -25,7 +25,7 @@ export const ContentWarningModal = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||||
<Card className="max-w-md">
|
<Card className="max-w-md animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<TriangleAlert className="mx-auto mb-2 text-yellow-600 dark:text-yellow-400" size={48} />
|
<TriangleAlert className="mx-auto mb-2 text-yellow-600 dark:text-yellow-400" size={48} />
|
||||||
<CardTitle className="text-center text-lg text-yellow-600 dark:text-yellow-400">Content Disclaimer</CardTitle>
|
<CardTitle className="text-center text-lg text-yellow-600 dark:text-yellow-400">Content Disclaimer</CardTitle>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export const DMCAModalComponent = ({ isOpen, onClose }: DMCAModalProps) => {
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 overflow-auto">
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 overflow-auto" onClick={onClose}>
|
||||||
<Card className="max-w-4xl w-full max-h-[90vh] flex flex-col mx-auto my-auto">
|
<Card className="max-w-4xl w-full max-h-[90vh] flex flex-col mx-auto my-auto animate-in fade-in slide-in-from-bottom-4 duration-300" onClick={(e) => e.stopPropagation()}>
|
||||||
<CardHeader className="flex-shrink-0 sticky top-0 bg-card z-10 border-b">
|
<CardHeader className="flex-shrink-0 sticky top-0 bg-card z-10 border-b">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
DMCA Copyright Policy & Takedown Notice
|
DMCA Copyright Policy & Takedown Notice
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export const DisclaimerModalComponent = ({ isOpen, onClose }: DisclaimerModalPro
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 overflow-auto">
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 overflow-auto" onClick={onClose}>
|
||||||
<Card className="max-w-4xl w-full max-h-[90vh] flex flex-col mx-auto my-auto">
|
<Card className="max-w-4xl w-full max-h-[90vh] flex flex-col mx-auto my-auto animate-in fade-in slide-in-from-bottom-4 duration-300" onClick={(e) => e.stopPropagation()}>
|
||||||
<CardHeader className="flex-shrink-0 sticky top-0 bg-card z-10 border-b">
|
<CardHeader className="flex-shrink-0 sticky top-0 bg-card z-10 border-b">
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
Legal Terms & Disclaimers
|
Legal Terms & Disclaimers
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const LegalFooter = ({ onDisclaimerOpen, onDMCAOpen }: LegalFooterProps)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Legal Notice Footer */}
|
{/* Legal Notice Footer */}
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-background/90 backdrop-blur-sm border-t border-border p-4 z-50">
|
<div className="w-full bg-background/90 backdrop-blur-sm border-t border-border p-4">
|
||||||
<div className="max-w-4xl mx-auto text-center text-xs text-muted-foreground">
|
<div className="max-w-4xl mx-auto text-center text-xs text-muted-foreground">
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
By using this service, you agree to our Terms of Service and acknowledge our disclaimers.
|
By using this service, you agree to our Terms of Service and acknowledge our disclaimers.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { MediaModal } from '@/components/MediaModal';
|
import { MediaModal } from '@/components/MediaModal';
|
||||||
|
import { AnimatedAvatar } from '@/components/AnimatedAvatar';
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
Circle,
|
Circle,
|
||||||
@@ -41,6 +42,7 @@ interface LeftPanelProps {
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
users?: ActiveUser[];
|
users?: ActiveUser[];
|
||||||
|
currentUser?: ActiveUser | null;
|
||||||
mediaFiles?: MediaFile[];
|
mediaFiles?: MediaFile[];
|
||||||
onFileUpload?: (files: FileList) => void;
|
onFileUpload?: (files: FileList) => void;
|
||||||
onFileDelete?: (fileId: string) => void;
|
onFileDelete?: (fileId: string) => void;
|
||||||
@@ -50,6 +52,7 @@ interface LeftPanelProps {
|
|||||||
export const LeftPanel: React.FC<LeftPanelProps> = ({
|
export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||||
isVisible,
|
isVisible,
|
||||||
users = [],
|
users = [],
|
||||||
|
currentUser,
|
||||||
mediaFiles = [],
|
mediaFiles = [],
|
||||||
onFileDelete,
|
onFileDelete,
|
||||||
onModalStateChange
|
onModalStateChange
|
||||||
@@ -280,115 +283,8 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
|||||||
isVisible ? 'transform-none' : '-translate-x-full'
|
isVisible ? 'transform-none' : '-translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Users Panel */}
|
|
||||||
<div className="h-1/2 flex flex-col border-b border-border">
|
|
||||||
<div className="flex items-center justify-center py-2 border-b border-border/50 bg-muted/20">
|
|
||||||
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
|
||||||
<Users size={16} />
|
|
||||||
Users
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={usersScrollRef}
|
|
||||||
className={`flex-1 overflow-y-auto hide-scrollbar scroll-shadow p-2 space-y-2 ${
|
|
||||||
usersScrollState.top ? 'scroll-top' : ''
|
|
||||||
} ${usersScrollState.bottom ? 'scroll-bottom' : ''}`}
|
|
||||||
>
|
|
||||||
{activeUsers.length === 0 ? (
|
|
||||||
<div className="text-center text-muted-foreground py-4">
|
|
||||||
<Users size={20} className="mx-auto mb-2 opacity-50" />
|
|
||||||
<p className="text-xs">No active users</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
activeUsers.map((user) => {
|
|
||||||
const { status, color } = getStatusIndicator(user);
|
|
||||||
return (
|
|
||||||
<Card key={user.id} className="bg-background border-border">
|
|
||||||
<CardContent className="p-3">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-medium"
|
|
||||||
style={{ backgroundColor: user.color }}
|
|
||||||
>
|
|
||||||
{user.name.charAt(0).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<Circle
|
|
||||||
size={8}
|
|
||||||
className="absolute -bottom-0.5 -right-0.5 border-2 border-background rounded-full"
|
|
||||||
style={{ color, fill: color }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-foreground">
|
|
||||||
{user.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{formatLastSeen(user.lastSeen)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
style={{ borderColor: user.color, color: user.color }}
|
|
||||||
>
|
|
||||||
{status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{user.currentLine && (
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Line {user.currentLine}
|
|
||||||
</span>
|
|
||||||
{user.isTyping && (
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
<div
|
|
||||||
className="w-1 h-1 rounded-full animate-pulse"
|
|
||||||
style={{ backgroundColor: user.color }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-1 h-1 rounded-full animate-pulse"
|
|
||||||
style={{
|
|
||||||
backgroundColor: user.color,
|
|
||||||
animationDelay: '0.1s'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-1 h-1 rounded-full animate-pulse"
|
|
||||||
style={{
|
|
||||||
backgroundColor: user.color,
|
|
||||||
animationDelay: '0.2s'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4 py-3 border-t border-border bg-muted/20">
|
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
||||||
<span>
|
|
||||||
{activeUsers.filter(u => getStatusIndicator(u).status === 'online').length} online
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{activeUsers.filter(u => u.isTyping).length} typing
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Media Panel */}
|
{/* Media Panel */}
|
||||||
<div className="h-1/2 flex flex-col">
|
<div className="h-[65%] flex flex-col border-b border-border">
|
||||||
<div className="flex items-center justify-center py-2 border-b border-border/50 bg-muted/20">
|
<div className="flex items-center justify-center py-2 border-b border-border/50 bg-muted/20">
|
||||||
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||||
<Upload size={16} />
|
<Upload size={16} />
|
||||||
@@ -543,6 +439,118 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Users Panel */}
|
||||||
|
<div className="h-[35%] flex flex-col">
|
||||||
|
<div className="flex items-center justify-center py-2 border-b border-border/50 bg-muted/20">
|
||||||
|
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||||
|
<Users size={16} />
|
||||||
|
Users
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={usersScrollRef}
|
||||||
|
className={`flex-1 overflow-y-auto hide-scrollbar scroll-shadow p-2 space-y-2 ${
|
||||||
|
usersScrollState.top ? 'scroll-top' : ''
|
||||||
|
} ${usersScrollState.bottom ? 'scroll-bottom' : ''}`}
|
||||||
|
>
|
||||||
|
{activeUsers.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-4">
|
||||||
|
<Users size={20} className="mx-auto mb-2 opacity-50" />
|
||||||
|
<p className="text-xs">No active users</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
activeUsers.map((user) => {
|
||||||
|
const { status, color } = getStatusIndicator(user);
|
||||||
|
const isCurrentUser = currentUser && user.id === currentUser.id;
|
||||||
|
return (
|
||||||
|
<Card key={user.id} className="bg-background border-border">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative">
|
||||||
|
{isCurrentUser ? (
|
||||||
|
<AnimatedAvatar />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-medium"
|
||||||
|
style={{ backgroundColor: user.color }}
|
||||||
|
>
|
||||||
|
{user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Circle
|
||||||
|
size={8}
|
||||||
|
className="absolute -bottom-0.5 -right-0.5 border-2 border-background rounded-full"
|
||||||
|
style={{ color, fill: color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{isCurrentUser ? 'You' : user.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{formatLastSeen(user.lastSeen)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
style={{ borderColor: user.color, color: user.color }}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.currentLine && (
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Line {user.currentLine}
|
||||||
|
</span>
|
||||||
|
{user.isTyping && (
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<div
|
||||||
|
className="w-1 h-1 rounded-full animate-pulse"
|
||||||
|
style={{ backgroundColor: user.color }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-1 h-1 rounded-full animate-pulse"
|
||||||
|
style={{
|
||||||
|
backgroundColor: user.color,
|
||||||
|
animationDelay: '0.1s'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-1 h-1 rounded-full animate-pulse"
|
||||||
|
style={{
|
||||||
|
backgroundColor: user.color,
|
||||||
|
animationDelay: '0.2s'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 py-3 border-t border-border bg-muted/20">
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{activeUsers.filter(u => getStatusIndicator(u).status === 'online').length} online
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{activeUsers.filter(u => u.isTyping).length} typing
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Media Modal */}
|
{/* Media Modal */}
|
||||||
<MediaModal
|
<MediaModal
|
||||||
file={modalFile}
|
file={modalFile}
|
||||||
|
|||||||
@@ -90,12 +90,12 @@ export const MediaModal: React.FC<MediaModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
className="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<Card className="relative max-w-[95vw] max-h-[95vh] w-auto h-auto bg-card border-border shadow-xl flex flex-col">
|
<Card className="relative max-w-[95vw] max-h-[95vh] w-auto h-auto bg-card border-border shadow-xl flex flex-col animate-in fade-in slide-in-from-bottom-4 duration-300" onClick={(e) => e.stopPropagation()}>
|
||||||
<div
|
<div
|
||||||
className="relative flex flex-col h-full"
|
className="relative flex flex-col h-full"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|||||||
95
client/components/ui/BetterHoverCard.tsx
Normal file
95
client/components/ui/BetterHoverCard.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CustomHoverCardProps {
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
holdDelay?: number;
|
||||||
|
contentClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context for managing mutually exclusive hover cards
|
||||||
|
const HoverCardContext = React.createContext<{
|
||||||
|
openId: string | null;
|
||||||
|
setOpenId: (id: string | null) => void;
|
||||||
|
}>({
|
||||||
|
openId: null,
|
||||||
|
setOpenId: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BetterHoverCard = ({ trigger, children, holdDelay = 600, contentClassName }: CustomHoverCardProps) => {
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
const [isTouchHeld, setIsTouchHeld] = React.useState(false);
|
||||||
|
const timerRef = React.useRef<NodeJS.Timeout>();
|
||||||
|
const isTouchDevice = React.useMemo(() => typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0), []);
|
||||||
|
const idRef = React.useRef(Math.random().toString(36).substr(2, 9));
|
||||||
|
const { openId, setOpenId } = React.useContext(HoverCardContext);
|
||||||
|
|
||||||
|
const handlePointerDown = (e: React.PointerEvent) => {
|
||||||
|
if (isTouchDevice && e.pointerType === 'touch') {
|
||||||
|
e.currentTarget.addEventListener('contextmenu', (e) => e.preventDefault(), { once: true });
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setIsTouchHeld(true);
|
||||||
|
setOpenId(idRef.current);
|
||||||
|
}, holdDelay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: React.PointerEvent) => {
|
||||||
|
if (isTouchDevice && e.pointerType === 'touch') {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (isTouchDevice && open && !isTouchHeld) {
|
||||||
|
return; // Ignore open on tap for touch devices
|
||||||
|
}
|
||||||
|
if (open) {
|
||||||
|
setOpenId(idRef.current);
|
||||||
|
} else if (openId === idRef.current) {
|
||||||
|
setOpenId(null);
|
||||||
|
}
|
||||||
|
setIsOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setIsTouchHeld(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync isOpen with context
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsOpen(openId === idRef.current);
|
||||||
|
}, [openId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverCard open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<HoverCardTrigger asChild
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
onPointerLeave={handlePointerUp}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className={cn(contentClassName)}>
|
||||||
|
{children}
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provider component to wrap the app or relevant section
|
||||||
|
export const HoverCardProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [openId, setOpenId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverCardContext.Provider value={{ openId, setOpenId }}>
|
||||||
|
{children}
|
||||||
|
</HoverCardContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,7 +5,9 @@ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
const HoverCard = ({ openDelay = 200, ...props }: React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Root>) => (
|
||||||
|
<HoverCardPrimitive.Root openDelay={openDelay} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ const config = {
|
|||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
"gradient-flow": {
|
||||||
|
'0%': { backgroundPosition: '0% 50%' },
|
||||||
|
'50%': { backgroundPosition: '100% 50%' },
|
||||||
|
'100%': { backgroundPosition: '0% 50%' },
|
||||||
|
},
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
from: { height: "0" },
|
from: { height: "0" },
|
||||||
to: { height: "var(--radix-accordion-content-height)" },
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
@@ -97,6 +102,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
|
"gradient-flow": "gradient-flow 3s ease-in-out infinite",
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user