feat(comments): add high-fidelity skeletons and Framer Motion transitions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1006cc7e3a
commit
a65e4b7ab2
4 changed files with 69 additions and 37 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Loader2 } from 'lucide-react';
|
||||
import { CommentThreadSkeleton } from './CommentThreadSkeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface CommentRepliesListProps {
|
||||
|
|
@ -21,11 +21,9 @@ export function CommentRepliesList({
|
|||
data-testid="comment-replies-list"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className="flex items-center justify-center py-4"
|
||||
data-testid="replies-loading"
|
||||
>
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
<div className="space-y-2" data-testid="replies-loading">
|
||||
<CommentThreadSkeleton />
|
||||
<CommentThreadSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog';
|
||||
import { useUser } from '@/features/auth/hooks/useUser';
|
||||
|
|
@ -139,18 +140,28 @@ export function CommentThread({
|
|||
/>
|
||||
)}
|
||||
|
||||
{showReplies && (
|
||||
<CommentRepliesList isLoading={isLoadingReplies}>
|
||||
{replies.map((reply) => (
|
||||
<CommentThread
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
trackId={trackId}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</CommentRepliesList>
|
||||
)}
|
||||
<AnimatePresence initial={false}>
|
||||
{showReplies && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<CommentRepliesList isLoading={isLoadingReplies}>
|
||||
{replies.map((reply) => (
|
||||
<CommentThread
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
trackId={trackId}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</CommentRepliesList>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MessageCircle, Reply } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const actionMotion = {
|
||||
rest: { scale: 1 },
|
||||
hover: { scale: 1.03 },
|
||||
tap: { scale: 0.97 },
|
||||
};
|
||||
|
||||
export interface CommentThreadActionsProps {
|
||||
canReply: boolean;
|
||||
hasUser: boolean;
|
||||
|
|
@ -27,27 +34,43 @@ export function CommentThreadActions({
|
|||
data-testid="comment-thread-actions"
|
||||
>
|
||||
{canReply && hasUser && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToggleReply}
|
||||
className="h-7 text-xs"
|
||||
<motion.div
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={actionMotion}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Reply className="h-3 w-3 mr-1" />
|
||||
Répondre
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToggleReply}
|
||||
className="h-7 text-xs hover:text-primary"
|
||||
>
|
||||
<Reply className="h-3 w-3 mr-1" />
|
||||
Répondre
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
{repliesCount > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToggleReplies}
|
||||
className="h-7 text-xs"
|
||||
<motion.div
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={actionMotion}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<MessageCircle className="h-3 w-3 mr-1" />
|
||||
{showReplies ? 'Masquer' : 'Afficher'} {repliesCount}{' '}
|
||||
{repliesCount === 1 ? 'réponse' : 'réponses'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToggleReplies}
|
||||
className="h-7 text-xs hover:text-primary"
|
||||
>
|
||||
<MessageCircle className="h-3 w-3 mr-1" />
|
||||
{showReplies ? 'Masquer' : 'Afficher'} {repliesCount}{' '}
|
||||
{repliesCount === 1 ? 'réponse' : 'réponses'}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ export function CommentThreadSkeleton({
|
|||
<div className="h-4 w-24 rounded bg-muted" />
|
||||
<div className="h-3 w-16 rounded bg-muted" />
|
||||
</div>
|
||||
<div className="h-4 w-full max-w-md rounded bg-muted" />
|
||||
<div className="h-4 w-3/4 max-w-sm rounded bg-muted" />
|
||||
<div className="h-4 w-full rounded bg-muted" />
|
||||
<div className="h-4 w-2/3 rounded bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue