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:
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 (
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -12,8 +12,8 @@ export const DMCAModalComponent = ({ isOpen, onClose }: DMCAModalProps) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 overflow-auto">
|
||||
<Card className="max-w-4xl w-full max-h-[90vh] flex flex-col mx-auto my-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 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">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
DMCA Copyright Policy & Takedown Notice
|
||||
|
||||
@@ -12,8 +12,8 @@ export const DisclaimerModalComponent = ({ isOpen, onClose }: DisclaimerModalPro
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 overflow-auto">
|
||||
<Card className="max-w-4xl w-full max-h-[90vh] flex flex-col mx-auto my-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 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">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
Legal Terms & Disclaimers
|
||||
|
||||
@@ -9,7 +9,7 @@ export const LegalFooter = ({ onDisclaimerOpen, onDMCAOpen }: LegalFooterProps)
|
||||
return (
|
||||
<>
|
||||
{/* 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">
|
||||
<p className="mb-2">
|
||||
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 { Badge } from '@/components/ui/badge';
|
||||
import { MediaModal } from '@/components/MediaModal';
|
||||
import { AnimatedAvatar } from '@/components/AnimatedAvatar';
|
||||
import {
|
||||
Users,
|
||||
Circle,
|
||||
@@ -41,6 +42,7 @@ interface LeftPanelProps {
|
||||
isVisible: boolean;
|
||||
className?: string;
|
||||
users?: ActiveUser[];
|
||||
currentUser?: ActiveUser | null;
|
||||
mediaFiles?: MediaFile[];
|
||||
onFileUpload?: (files: FileList) => void;
|
||||
onFileDelete?: (fileId: string) => void;
|
||||
@@ -50,6 +52,7 @@ interface LeftPanelProps {
|
||||
export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
isVisible,
|
||||
users = [],
|
||||
currentUser,
|
||||
mediaFiles = [],
|
||||
onFileDelete,
|
||||
onModalStateChange
|
||||
@@ -280,115 +283,8 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
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 */}
|
||||
<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">
|
||||
<h3 className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||
<Upload size={16} />
|
||||
@@ -543,6 +439,118 @@ export const LeftPanel: React.FC<LeftPanelProps> = ({
|
||||
</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 */}
|
||||
<MediaModal
|
||||
file={modalFile}
|
||||
|
||||
@@ -90,12 +90,12 @@ export const MediaModal: React.FC<MediaModalProps> = ({
|
||||
|
||||
return (
|
||||
<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}
|
||||
onKeyDown={handleKeyDown}
|
||||
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
|
||||
className="relative flex flex-col h-full"
|
||||
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"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
const HoverCard = ({ openDelay = 200, ...props }: React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Root>) => (
|
||||
<HoverCardPrimitive.Root openDelay={openDelay} {...props} />
|
||||
);
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
|
||||
Reference in New Issue
Block a user