Twitter Card

A social media card component that mimics Twitter/X post styling with profile information, content display, and interactive elements like likes, retweets, and bookmarks with smooth GSAP animations.

React
Tailwind CSS
Social Media
Cards
GSAP
UI
DiMaacUI
DiMaac@DiMaacUI·2h
Just shipped a new feature for DiMaac UI! 🚀
DiMaacUI
DiMaacUI
DiMaac@DiMaacUI·5h
Hot take: Your side project doesn't need: ❌ Microservices ❌ Kubernetes ❌ Redis ❌ GraphQL ❌ Multiple databases It needs: ✅ To solve a real problem ✅ To ship fast ✅ To get users ✅ To iterate based on feedback Stop over-engineering. Start shipping. 🚢
lylia_agent47
Lylia@lylia_agent47·8h
🌙

3:06 AM

when the world sleeps,

creativity awakens

trust the process

muscle_therapist
Baraka@muscle_therapist·1d
Training legs today. Training code tomorrow. Both require: • Consistency over intensity • Progressive overload • Proper form • Recovery time • Long-term thinking Whether you're building muscle or building apps, the principles are the same. Stay disciplined. 💪 #FitnessAndCode
lylia_agent47
Lylia@lylia_agent47·1d
⚡️

The 1% Rule

1% better every day

= 37x better in a year

compound growth

DiMaacUI
DiMaac@DiMaacUI·2d
Real talk: The best developers I know aren't the ones who memorized every algorithm or framework. They're the ones who: • Ask great questions • Admit when they don't know • Help others without ego • Ship features that users love • Debug with curiosity, not frustration Be the dev people want on their team. Technical skills are teachable. Character isn't. 💙
muscle_therapist
Baraka@muscle_therapist·3d
Your body is a temple. Your code is a cathedral. Both need: ✓ Strong foundations ✓ Regular maintenance ✓ Attention to detail ✓ Sustainable practices Don't skip leg day. Don't skip code reviews. Both will catch up with you eventually. 🏋️‍♂️💻
DiMaacUI
DiMaac@DiMaacUI·4d
Unpopular opinion: You don't need to learn every new JavaScript framework that comes out. Master the fundamentals: • Vanilla JS • CSS • HTML • HTTP • Browser APIs Frameworks come and go. Fundamentals are forever. Build on rock, not sand. 🏗️

Using CLI

npx dimaac add TwitterCard

Manual Installation

npm install react @gsap/react

lib/utils.ts

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
} 

components/ui/TwitterCard.tsx

"use client"

import Image from 'next/image';
import React, { useState, useRef } from 'react'
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
import { cn } from '@/lib/utils';

interface TwitterCardProps {
    image?: string;
    profileImage: string;
    isVerified: boolean;
    content: string | React.ReactNode;
    username: string;
    displayName: string;
    timestamp: string;
    className?: string;
}

const TwitterCard = (props: TwitterCardProps) => {
    const [isLiked, setIsLiked] = useState(false);
    const [isRetweeted, setIsRetweeted] = useState(false);
    const [isBookmarked, setIsBookmarked] = useState(false);
    
    const containerRef = useRef<HTMLDivElement>(null);
    const heartRef = useRef<SVGSVGElement>(null);
    const retweetRef = useRef<SVGSVGElement>(null);

    const { contextSafe } = useGSAP({ scope: containerRef });

    const animateIcon = contextSafe((iconRef: React.RefObject<SVGSVGElement | null>) => {
        if (iconRef.current) {
            gsap.fromTo(iconRef.current, 
                { scale: 1 },
                { 
                    scale: 1.2,
                    duration: 0.1,
                    ease: "power2.out",
                    onComplete: () => {
                        gsap.to(iconRef.current, {
                            scale: 1,
                            duration: 0.15,
                            ease: "power2.out"
                        });
                    }
                }
            );
        }
    });

    const handleLikeClick = (e: React.MouseEvent) => {
        e.stopPropagation();
        const newLikedState = !isLiked;
        setIsLiked(newLikedState);
        if (newLikedState) {
            animateIcon(heartRef);
        }
    };

    const handleRetweetClick = (e: React.MouseEvent) => {
        e.stopPropagation();
        const newRetweetState = !isRetweeted;
        setIsRetweeted(newRetweetState);
        if (newRetweetState) {
            animateIcon(retweetRef);
        }
    };

    return (
        <div 
            ref={containerRef}
            className={cn("w-full max-w-xl flex gap-3 p-4 border border-white/10 rounded-2xl transition-colors", props.className)} 
            style={{
                fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
            }}
        >
            {/* Profile Image */}
            <div className="flex-shrink-0">
                <Image
                    src={props.profileImage}
                    alt={props.username}
                    className="w-10 h-10 rounded-full object-cover"
                    width={48}
                    height={48}
                />
            </div>

            {/* Content */}
            <div className="flex-1 min-w-0">
                {/* Header */}
                <div className="flex items-center gap-1 mb-2">
                    <span className="font-bold text-white hover:underline cursor-pointer truncate">
                        {props.displayName}
                    </span>
                    {props.isVerified && (
                        <svg viewBox="0 0 22 22" aria-label="Verified account" className="w-5 h-5 fill-[#1d9bf0]">
                            <g>
                                <path d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z"></path>
                            </g>
                        </svg>
                    )}
                    <span className="text-white/50">@{props.username}</span>
                    <span className="text-white/50">·</span>
                    <span className="text-white/50 hover:underline cursor-pointer">{props.timestamp}</span>
                </div>

                {/* Tweet Content */}
                {typeof props.content === 'string' ? (
                    <>
                        <div className="text-white mb-3 whitespace-pre-wrap break-words">
                            {props.content}
                        </div>
                        {props.image && (
                            <div className="mb-1 rounded-2xl overflow-hidden border border-white/10">
                                <Image 
                                    src={props.image} 
                                    alt={props.username} 
                                    className="w-full h-auto object-cover" 
                                    width={600} 
                                    height={400} 
                                />
                            </div>
                        )}
                    </>
                ) : (
                    <div className="mb-1 rounded-2xl overflow-hidden border border-white/10">
                        {props.content}
                    </div>
                )}

                {/* Actions */}
                <div className="flex items-center justify-between w-full">
                    {/* Reply */}
                    <button 
                        className="flex items-center gap-2 text-white/50 hover:text-[#1d9bf0] transition-colors cursor-pointer"
                        type="button"
                    >
                        <div className="p-2 rounded-full transition-colors">
                            <svg viewBox="0 0 24 24" aria-hidden="true" className="w-[18px] h-[18px] fill-current">
                                <g><path d="M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01zm8.005-6c-3.317 0-6.005 2.69-6.005 6 0 3.37 2.77 6.08 6.138 6.01l.351-.01h1.761v2.3l5.087-2.81c1.951-1.08 3.163-3.13 3.163-5.36 0-3.39-2.744-6.13-6.129-6.13H9.756z"></path></g>
                            </svg>
                        </div>
                    </button>

                    {/* Retweet */}
                    <button 
                        onClick={handleRetweetClick}
                        className={`flex items-center gap-2 transition-colors cursor-pointer ${
                            isRetweeted ? 'text-[#00ba7c]' : 'text-white/50 hover:text-[#00ba7c]'
                        }`}
                        type="button"
                    >
                        <div className="p-2 rounded-full transition-colors">
                            <svg 
                                ref={retweetRef}
                                viewBox="0 0 24 24" 
                                aria-hidden="true"
                                className="w-[18px] h-[18px] fill-current"
                                style={{ transformOrigin: 'center center' }}
                            >
                                <g><path d="M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z"></path></g>
                            </svg>
                        </div>
                    </button>

                    {/* Like */}
                    <button
                        onClick={handleLikeClick}
                        className={`flex items-center gap-2 transition-colors cursor-pointer ${
                            isLiked ? 'text-[#f91880]' : 'text-white/50 hover:text-[#f91880]'
                        }`}
                        type="button"
                    >
                        <div className="p-2 rounded-full transition-colors">
                            <svg
                                ref={heartRef}
                                viewBox="0 0 24 24"
                                aria-hidden="true"
                                className="w-[18px] h-[18px] fill-current"
                                style={{ transformOrigin: 'center center' }}
                            >
                                {isLiked ? (
                                    <g><path d="M20.884 13.19c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"></path></g>
                                ) : (
                                    <g><path d="M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z"></path></g>
                                )}
                            </svg>
                        </div>
                    </button>

                    {/* Views */}
                    <button 
                        className="flex items-center gap-2 text-white/50 hover:text-[#1d9bf0] transition-colors cursor-pointer"
                        type="button"
                    >
                        <div className="p-2 rounded-full transition-colors">
                            <svg viewBox="0 0 24 24" aria-hidden="true" className="w-[18px] h-[18px] fill-current">
                                <g><path d="M8.75 21V3h2v18h-2zM18 21V8.5h2V21h-2zM4 21l.004-10h2L6 21H4zm9.248 0v-7h2v7h-2z"></path></g>
                            </svg>
                        </div>
                    </button>

                    {/* Share & Bookmark */}
                    <div className="flex items-center gap-0">
                        <button
                            onClick={(e) => {
                                e.stopPropagation();
                                setIsBookmarked(!isBookmarked);
                            }}
                            className={`transition-colors cursor-pointer ${
                                isBookmarked ? 'text-[#1d9bf0]' : 'text-white/50 hover:text-[#1d9bf0]'
                            }`}
                            type="button"
                        >
                            <div className="p-2 rounded-full transition-colors">
                                <svg 
                                    viewBox="0 0 24 24" 
                                    aria-hidden="true"
                                    className="w-[18px] h-[18px] fill-current"
                                >
                                    <g><path d="M4 4.5C4 3.12 5.119 2 6.5 2h11C18.881 2 20 3.12 20 4.5v18.44l-8-5.71-8 5.71V4.5zM6.5 4c-.276 0-.5.22-.5.5v14.56l6-4.29 6 4.29V4.5c0-.28-.224-.5-.5-.5h-11z"></path></g>
                                </svg>
                            </div>
                        </button>
                        <button 
                            className="text-white/50 hover:text-[#1d9bf0] transition-colors cursor-pointer"
                            type="button"
                        >
                            <div className="p-2 rounded-full transition-colors">
                                <svg viewBox="0 0 24 24" aria-hidden="true" className="w-[18px] h-[18px] fill-current">
                                    <g><path d="M12 2.59l5.7 5.7-1.41 1.42L13 6.41V16h-2V6.41l-3.3 3.3-1.41-1.42L12 2.59zM21 15l-.02 3.51c0 1.38-1.12 2.49-2.5 2.49H5.5C4.11 21 3 19.88 3 18.5V15h2v3.5c0 .28.22.5.5.5h12.98c.28 0 .5-.22.5-.5L19 15h2z"></path></g>
                                </svg>
                            </div>
                        </button>
                    </div>
                </div>
            </div>
        </div>
    )
}

export default TwitterCard