Files
ContentGen_FE/src/components/dashboard/tweet-import-card.tsx
Harun CAN 45a540c530
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
main
2026-03-29 12:44:02 +03:00

387 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Twitter,
Loader2,
Heart,
Repeat2,
Eye,
MessageCircle,
Sparkles,
ArrowRight,
CheckCircle2,
XCircle,
Flame,
Image as ImageIcon,
Link2,
Zap,
} from "lucide-react";
import { useTweet } from "@/hooks/use-tweet";
import { cn } from "@/lib/utils";
interface TweetImportCardProps {
onProjectCreated?: (projectId: string) => void;
}
export function TweetImportCard({ onProjectCreated }: TweetImportCardProps) {
const {
preview,
isLoadingPreview,
isCreatingProject,
error,
createdProject,
isValidTweetUrl,
previewTweet,
createProjectFromTweet,
reset,
} = useTweet();
const [tweetUrl, setTweetUrl] = useState("");
const [customTitle, setCustomTitle] = useState("");
const handlePreview = useCallback(async () => {
if (!tweetUrl.trim()) return;
await previewTweet(tweetUrl.trim());
}, [tweetUrl, previewTweet]);
const handleCreate = useCallback(async () => {
if (!tweetUrl.trim()) return;
const project = await createProjectFromTweet({
tweetUrl: tweetUrl.trim(),
title: customTitle.trim() || undefined,
});
if (project && onProjectCreated) {
onProjectCreated(project.id);
}
}, [tweetUrl, customTitle, createProjectFromTweet, onProjectCreated]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (!preview) {
handlePreview();
} else {
handleCreate();
}
}
};
const isUrlValid = tweetUrl.trim().length > 0 && isValidTweetUrl(tweetUrl.trim());
const formatNumber = (num: number): string => {
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
return num.toString();
};
const viralColor = (score: number) => {
if (score >= 70) return "text-emerald-400";
if (score >= 40) return "text-amber-400";
return "text-[var(--color-text-muted)]";
};
const viralBg = (score: number) => {
if (score >= 70) return "bg-emerald-500/10 border-emerald-500/30";
if (score >= 40) return "bg-amber-500/10 border-amber-500/30";
return "bg-[var(--color-bg-elevated)] border-[var(--color-border-faint)]";
};
return (
<div className="card-surface overflow-hidden">
{/* Header */}
<div className="flex items-center gap-3 p-4 md:p-5 border-b border-[var(--color-border-faint)]">
<div className="w-9 h-9 rounded-xl bg-sky-500/15 flex items-center justify-center">
<Twitter size={18} className="text-sky-400" />
</div>
<div>
<h3 className="font-[family-name:var(--font-display)] text-sm font-semibold">
X/Twitter Import
</h3>
<p className="text-[11px] text-[var(--color-text-ghost)]">
Tweet Video pipeline
</p>
</div>
</div>
{/* URL Input */}
<div className="p-4 md:p-5 space-y-4">
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]">
<Link2 size={16} />
</div>
<input
type="url"
value={tweetUrl}
onChange={(e) => {
setTweetUrl(e.target.value);
if (preview) reset();
}}
onKeyDown={handleKeyDown}
placeholder="https://x.com/user/status/123..."
className="w-full h-11 pl-10 pr-24 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:border-sky-500/50 focus:ring-1 focus:ring-sky-500/25 outline-none transition-all"
/>
<button
onClick={preview ? handleCreate : handlePreview}
disabled={!isUrlValid || isLoadingPreview || isCreatingProject}
className={cn(
"absolute right-1.5 top-1/2 -translate-y-1/2 h-8 px-3 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5",
isUrlValid && !isLoadingPreview && !isCreatingProject
? "bg-sky-500 text-white hover:bg-sky-400 shadow-sm"
: "bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] cursor-not-allowed"
)}
>
{isLoadingPreview ? (
<>
<Loader2 size={13} className="animate-spin" />
Çekiliyor
</>
) : isCreatingProject ? (
<>
<Loader2 size={13} className="animate-spin" />
Üretiliyor
</>
) : preview ? (
<>
<Zap size={13} />
Oluştur
</>
) : (
<>
<ArrowRight size={13} />
Ön İzle
</>
)}
</button>
</div>
{/* Error */}
<AnimatePresence mode="wait">
{error && (
<motion.div
initial={{ opacity: 0, y: -8, height: 0 }}
animate={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -8, height: 0 }}
className="flex items-center gap-2 text-xs text-rose-400 bg-rose-500/10 border border-rose-500/20 rounded-lg px-3 py-2"
>
<XCircle size={14} />
{error}
</motion.div>
)}
</AnimatePresence>
{/* Preview Card */}
<AnimatePresence mode="wait">
{preview && (
<motion.div
initial={{ opacity: 0, y: 12, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 12, scale: 0.97 }}
transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
className="space-y-3"
>
{/* Author + Viral Score */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] overflow-hidden flex items-center justify-center">
{preview.tweet.author.avatarUrl ? (
<img
src={preview.tweet.author.avatarUrl}
alt={preview.tweet.author.name}
className="w-full h-full object-cover"
/>
) : (
<Twitter size={16} className="text-sky-400" />
)}
</div>
<div>
<div className="flex items-center gap-1">
<span className="text-sm font-medium">
{preview.tweet.author.name}
</span>
{preview.tweet.author.verified && (
<CheckCircle2
size={13}
className="text-sky-400 fill-sky-400"
/>
)}
</div>
<span className="text-[11px] text-[var(--color-text-ghost)]">
@{preview.tweet.author.username}
</span>
</div>
</div>
{/* Viral Score Badge */}
<div
className={cn(
"flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-xs font-medium",
viralBg(preview.viralScore)
)}
>
<Flame size={13} className={viralColor(preview.viralScore)} />
<span className={viralColor(preview.viralScore)}>
{preview.viralScore}/100
</span>
</div>
</div>
{/* Tweet Text */}
<div className="bg-[var(--color-bg-elevated)] rounded-xl p-3 border border-[var(--color-border-faint)]">
<p className="text-[13px] text-[var(--color-text-secondary)] leading-relaxed line-clamp-4">
{preview.tweet.text}
</p>
</div>
{/* Media preview */}
{preview.tweet.media.length > 0 && (
<div className="flex gap-1.5 overflow-x-auto pb-1">
{preview.tweet.media.slice(0, 4).map((m, i) => (
<div
key={i}
className="w-16 h-16 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] overflow-hidden flex-shrink-0"
>
{m.type === "photo" ? (
<img
src={m.url}
alt={`Media ${i + 1}`}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-[var(--color-text-ghost)]">
<ImageIcon size={20} />
</div>
)}
</div>
))}
</div>
)}
{/* Metrics */}
<div className="grid grid-cols-4 gap-2">
{[
{
icon: Heart,
value: preview.tweet.metrics.likes,
color: "text-rose-400",
},
{
icon: Repeat2,
value: preview.tweet.metrics.retweets,
color: "text-emerald-400",
},
{
icon: Eye,
value: preview.tweet.metrics.views,
color: "text-sky-400",
},
{
icon: MessageCircle,
value: preview.tweet.metrics.replies,
color: "text-amber-400",
},
].map(({ icon: Icon, value, color }, i) => (
<div
key={i}
className="flex items-center justify-center gap-1.5 py-2 rounded-lg bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-faint)]"
>
<Icon size={13} className={color} />
<span className="text-[11px] font-medium">
{formatNumber(value)}
</span>
</div>
))}
</div>
{/* Info badges */}
<div className="flex flex-wrap gap-1.5">
<span className="badge badge-cyan text-[10px]">
{preview.contentType === "thread"
? "Thread"
: preview.contentType === "quote_tweet"
? "Alıntı"
: "Tweet"}
</span>
<span className="badge badge-violet text-[10px]">
~{preview.estimatedDuration}s video
</span>
{preview.tweet.media.length > 0 && (
<span className="badge badge-amber text-[10px]">
{preview.tweet.media.length} medya
</span>
)}
</div>
{/* Custom Title */}
<input
type="text"
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
placeholder={preview.suggestedTitle}
className="w-full h-10 px-3 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)]/50 focus:border-violet-500/50 focus:ring-1 focus:ring-violet-500/25 outline-none transition-all"
/>
{/* Create Button */}
<button
onClick={handleCreate}
disabled={isCreatingProject}
className={cn(
"w-full h-11 rounded-xl text-sm font-semibold shadow-sm transition-all flex items-center justify-center gap-2",
isCreatingProject
? "bg-violet-600/50 text-violet-200 cursor-not-allowed"
: "bg-gradient-to-r from-violet-600 to-purple-600 text-white hover:from-violet-500 hover:to-purple-500 hover:shadow-lg hover:shadow-violet-500/25 active:scale-[0.98]"
)}
>
{isCreatingProject ? (
<>
<Loader2 size={16} className="animate-spin" />
Senaryo üretiliyor...
</>
) : (
<>
<Sparkles size={16} />
Tweet&apos;ten Video Oluştur
</>
)}
</button>
</motion.div>
)}
</AnimatePresence>
{/* Success */}
<AnimatePresence>
{createdProject && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="flex items-center gap-3 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/25"
>
<CheckCircle2 size={18} className="text-emerald-400 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-300">
Proje oluşturuldu!
</p>
<p className="text-[11px] text-emerald-400/70 truncate">
{createdProject.title}
</p>
</div>
<button
onClick={() => {
if (onProjectCreated) onProjectCreated(createdProject.id);
}}
className="text-[11px] text-emerald-400 hover:text-emerald-300 font-medium transition-colors whitespace-nowrap"
>
Görüntüle
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}