660 lines
23 KiB
TypeScript
660 lines
23 KiB
TypeScript
"use client";
|
|
|
|
import axios from "axios";
|
|
import { ApiServerBaseUrl } from "@/utils/baseurl.utils";
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { getSocialAuthStatus } from "@/utils/commonFunction.utils";
|
|
import SocialCommentItem from "@/components/SocialCommentItem";
|
|
import {
|
|
Image as ImageIcon,
|
|
Video,
|
|
MessageSquare,
|
|
Heart,
|
|
Calendar,
|
|
ExternalLink,
|
|
Send,
|
|
RefreshCw,
|
|
ChevronLeft,
|
|
MoreVertical,
|
|
BarChart3,
|
|
Users,
|
|
Clock,
|
|
Shield,
|
|
AlertCircle,
|
|
CheckCircle
|
|
} from "lucide-react";
|
|
|
|
type Reply = {
|
|
id: string;
|
|
text: string;
|
|
timestamp: string;
|
|
username: string;
|
|
hidden?: boolean;
|
|
like_count?: number;
|
|
};
|
|
|
|
type Comment = {
|
|
id: string;
|
|
text: string;
|
|
username: string;
|
|
timestamp: string;
|
|
like_count?: number;
|
|
hidden?: boolean;
|
|
replies?: { data: Reply[] };
|
|
};
|
|
|
|
type MediaDetails = {
|
|
id: string;
|
|
media_url: string;
|
|
caption?: string;
|
|
media_type: string;
|
|
timestamp: string;
|
|
like_count?: number;
|
|
comments_count?: number;
|
|
permalink?: string;
|
|
};
|
|
|
|
const MediaDetailsPage = ({ params }: { params: { id: string } }) => {
|
|
const { id } = params;
|
|
const router = useRouter();
|
|
|
|
const [media, setMedia] = useState<MediaDetails | null>(null);
|
|
const [comments, setComments] = useState<Comment[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [commentsLoading, setCommentsLoading] = useState(false);
|
|
const [newCommentText, setNewCommentText] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
async function validate() {
|
|
const userEmail = localStorage.getItem("user_email");
|
|
if (!userEmail) {
|
|
router.push("/social-media-connect");
|
|
return;
|
|
}
|
|
|
|
const storedUser = localStorage.getItem("userDetails");
|
|
if (!storedUser) {
|
|
router.push("/social-media-connect");
|
|
return;
|
|
}
|
|
|
|
const user = JSON.parse(storedUser);
|
|
const role = user?.role;
|
|
|
|
if (role === "customer") {
|
|
const session = localStorage.getItem("payment_session");
|
|
if (!session) {
|
|
router.push("/pricing");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const res = await getSocialAuthStatus(userEmail);
|
|
if (!res?.connected) {
|
|
router.push("/social-media-connect");
|
|
return;
|
|
}
|
|
}
|
|
|
|
validate();
|
|
}, []);
|
|
|
|
// Fetch Media Details
|
|
useEffect(() => {
|
|
const fetchMedia = async () => {
|
|
try {
|
|
const res = await axios.get(`${ApiServerBaseUrl}/social/media/${id}?userId=${localStorage.getItem(
|
|
"user_email"
|
|
)}`);
|
|
setMedia(res.data);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || "Unable to load media");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchMedia();
|
|
}, [id]);
|
|
|
|
// Fetch Comments
|
|
const loadComments = async () => {
|
|
setCommentsLoading(true);
|
|
try {
|
|
const res = await axios.get(
|
|
`${ApiServerBaseUrl}/social/media/${id}/comments?userId=${localStorage.getItem(
|
|
"user_email"
|
|
)}`
|
|
);
|
|
setComments(res.data?.data || []);
|
|
} catch {
|
|
console.error("Failed to load comments");
|
|
} finally {
|
|
setCommentsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (media) loadComments();
|
|
}, [media]);
|
|
|
|
// Post new top-level comment
|
|
const postNewComment = async () => {
|
|
if (!newCommentText.trim()) {
|
|
alert("Comment cannot be empty.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await axios.post(
|
|
`${ApiServerBaseUrl}/social/media/${id}/comments?userId=${localStorage.getItem(
|
|
"user_email"
|
|
)}`,
|
|
{ message: newCommentText }
|
|
);
|
|
|
|
const newComment: Comment = {
|
|
id: res.data?.comment_id || res.data?.id || `temp-${Date.now()}`,
|
|
text: newCommentText,
|
|
username: "Me",
|
|
timestamp: new Date().toISOString(),
|
|
replies: { data: [] }
|
|
};
|
|
|
|
setComments((prev) => [newComment, ...prev]);
|
|
setNewCommentText("");
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.error || "Failed to post comment.");
|
|
}
|
|
};
|
|
|
|
// Reply handler
|
|
const handleReply = async (commentId: string, text: string) => {
|
|
try {
|
|
const res = await axios.post(
|
|
`${ApiServerBaseUrl}/social/comments/${commentId}/reply?userId=${localStorage.getItem(
|
|
"user_email"
|
|
)}`,
|
|
{ message: text }
|
|
);
|
|
|
|
const newReply: Reply = {
|
|
id: res.data?.reply_id || res.data?.id || `temp-${Date.now()}`,
|
|
text: text,
|
|
username: "Me",
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
setComments((prev) =>
|
|
prev.map((cm) =>
|
|
cm.id === commentId
|
|
? {
|
|
...cm,
|
|
replies: {
|
|
data: [...(cm.replies?.data || []), newReply],
|
|
},
|
|
}
|
|
: cm
|
|
)
|
|
);
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.error || "Reply failed.");
|
|
}
|
|
};
|
|
|
|
// Delete handler
|
|
const handleDelete = async (id: string, isReply: boolean = false, parentId?: string) => {
|
|
const confirmed = window.confirm("Are you sure you want to delete this?");
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
await axios.delete(`${ApiServerBaseUrl}/social/comments/${id}?userId=${localStorage.getItem(
|
|
"user_email"
|
|
)}`);
|
|
|
|
if (isReply && parentId) {
|
|
setComments((prev) =>
|
|
prev.map((cm) => {
|
|
if (cm.id === parentId) {
|
|
return {
|
|
...cm,
|
|
replies: {
|
|
data: cm.replies?.data.filter((r) => r.id !== id) || [],
|
|
},
|
|
};
|
|
}
|
|
return cm;
|
|
})
|
|
);
|
|
} else {
|
|
setComments((prev) => prev.filter((c) => c.id !== id));
|
|
}
|
|
} catch (err: any) {
|
|
alert("Delete failed: " + (err.response?.data?.error || err.message));
|
|
}
|
|
};
|
|
|
|
// Hide/Unhide handler
|
|
const handleHide = async (id: string, currentStatus: boolean, isReply: boolean = false) => {
|
|
try {
|
|
const newHideStatus = !currentStatus;
|
|
|
|
await axios.post(`${ApiServerBaseUrl}/social/comments/${id}/hide?userId=${localStorage.getItem("user_email")}`, {
|
|
hide: newHideStatus,
|
|
});
|
|
|
|
if (isReply) {
|
|
setComments((prev) =>
|
|
prev.map((cm) => {
|
|
const replyIndex = cm.replies?.data.findIndex(r => r.id === id);
|
|
if (replyIndex !== undefined && replyIndex !== -1 && cm.replies?.data) {
|
|
const updatedReplies = [...cm.replies.data];
|
|
updatedReplies[replyIndex] = { ...updatedReplies[replyIndex], hidden: newHideStatus };
|
|
return { ...cm, replies: { data: updatedReplies } };
|
|
}
|
|
return cm;
|
|
})
|
|
);
|
|
} else {
|
|
setComments((prev) =>
|
|
prev.map((c) => (c.id === id ? { ...c, hidden: newHideStatus } : c))
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
alert(err.response?.data?.error || "Hide/Unhide failed.");
|
|
}
|
|
};
|
|
|
|
// Edit handler
|
|
const handleEdit = async (id: string, newText: string, isReply: boolean = false, parentId?: string) => {
|
|
if (id.toString().startsWith("temp-") || id.toString().startsWith("new-")) {
|
|
alert("Please refresh the page to get the real ID from Instagram before editing this comment again.");
|
|
return;
|
|
}
|
|
|
|
const confirmed = window.confirm(
|
|
"Compatible Edit Mode:\n\nInstagram does not allow editing comments directly.\n\nProceeding will DELETE the original comment (losing likes/replies) and POST a new one with the updated text.\n\nDo you want to continue?"
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
await axios.delete(`${ApiServerBaseUrl}/social/comments/${id}?userId=${localStorage.getItem("user_email")}`);
|
|
|
|
let res;
|
|
if (isReply && parentId) {
|
|
res = await axios.post(
|
|
`${ApiServerBaseUrl}/social/comments/${parentId}/reply?userId=${localStorage.getItem("user_email")}`,
|
|
{ message: newText }
|
|
);
|
|
} else {
|
|
res = await axios.post(
|
|
`${ApiServerBaseUrl}/social/media/${media?.id}/comments?userId=${localStorage.getItem("user_email")}`,
|
|
{ message: newText }
|
|
);
|
|
}
|
|
|
|
const newId = res.data?.id || res.data?.comment_id || res.data?.reply_id;
|
|
const safeId = newId || `new-${Date.now()}`;
|
|
|
|
if (isReply && parentId) {
|
|
setComments((prev) =>
|
|
prev.map((cm) => {
|
|
if (cm.id === parentId) {
|
|
const oldReplies = cm.replies?.data || [];
|
|
const filteredReplies = oldReplies.filter((r) => r.id !== id);
|
|
|
|
const newReplyObj: Reply = {
|
|
id: safeId,
|
|
text: newText,
|
|
username: "Me",
|
|
timestamp: new Date().toISOString(),
|
|
hidden: false,
|
|
like_count: 0
|
|
};
|
|
|
|
return {
|
|
...cm,
|
|
replies: { data: [...filteredReplies, newReplyObj] }
|
|
};
|
|
}
|
|
return cm;
|
|
})
|
|
);
|
|
} else {
|
|
setComments((prev) => {
|
|
const filtered = prev.filter((c) => c.id !== id);
|
|
const newCommentObj: Comment = {
|
|
id: safeId,
|
|
text: newText,
|
|
username: "Me",
|
|
timestamp: new Date().toISOString(),
|
|
like_count: 0,
|
|
hidden: false,
|
|
replies: { data: [] }
|
|
};
|
|
return [newCommentObj, ...filtered];
|
|
});
|
|
}
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
alert("Edit failed: " + (err.response?.data?.error || err.message));
|
|
}
|
|
};
|
|
|
|
// Refresh comments
|
|
const handleRefreshComments = async () => {
|
|
setRefreshing(true);
|
|
await loadComments();
|
|
setTimeout(() => setRefreshing(false), 1000);
|
|
};
|
|
|
|
// Format date
|
|
const formatDate = (timestamp: string) => {
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
if (loading) return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0a0a0a] to-[#111111] flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-pink-500 mx-auto mb-4"></div>
|
|
<p className="text-gray-300">Loading media details...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (error) return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0a0a0a] to-[#111111] p-6">
|
|
<div className="max-w-4xl mx-auto">
|
|
<div className="bg-red-900/20 backdrop-blur-xl rounded-2xl p-8 border border-red-500/30">
|
|
<div className="flex items-center gap-4">
|
|
<AlertCircle className="w-12 h-12 text-red-400" />
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-white mb-2">Error Loading Media</h2>
|
|
<p className="text-gray-300">{error}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (!media) return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0a0a0a] to-[#111111] flex items-center justify-center">
|
|
<p className="text-gray-300">No media found.</p>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-b from-[#0a0a0a] to-[#111111] p-4 md:p-6">
|
|
{/* Floating background bubbles */}
|
|
<div className="absolute top-20 left-[5%] w-24 h-24 rounded-full bg-pink-500/20 blur-xl animate-float"></div>
|
|
<div className="absolute top-40 right-[10%] w-32 h-32 rounded-full bg-blue-500/15 blur-xl animate-float-slow"></div>
|
|
|
|
<div className="max-w-6xl mx-auto relative z-10">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<button
|
|
onClick={() => router.push("/social-media-posts")}
|
|
className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-6 transition-colors"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
Back to Media Library
|
|
</button>
|
|
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">Post Details</h1>
|
|
<p className="text-gray-300">Manage comments and view post details</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={handleRefreshComments}
|
|
disabled={refreshing}
|
|
className="px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-gray-300 hover:text-white transition-all duration-300 flex items-center gap-2"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
|
|
Refresh Comments
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="grid lg:grid-cols-3 gap-8">
|
|
{/* Left Column - Media */}
|
|
<div className="lg:col-span-2">
|
|
{/* Media Card */}
|
|
<div className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl overflow-hidden mb-8">
|
|
{/* Media Display */}
|
|
<div className="relative">
|
|
{media.media_type === "VIDEO" ? (
|
|
<video
|
|
src={media.media_url}
|
|
controls
|
|
className="w-full h-auto max-h-[600px] object-contain bg-black"
|
|
poster={media.media_url}
|
|
/>
|
|
) : (
|
|
<img
|
|
src={media.media_url}
|
|
alt="Instagram Media"
|
|
className="w-full h-auto max-h-[600px] object-contain bg-black"
|
|
/>
|
|
)}
|
|
<div className="absolute top-4 right-4">
|
|
<div className="px-3 py-1 rounded-full bg-black/80 backdrop-blur-sm text-sm font-medium text-white flex items-center gap-2">
|
|
{media.media_type === "VIDEO" ? (
|
|
<Video className="w-4 h-4" />
|
|
) : (
|
|
<ImageIcon className="w-4 h-4" />
|
|
)}
|
|
<span className="capitalize">{media.media_type.toLowerCase()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Media Details */}
|
|
<div className="p-6">
|
|
{media.caption && (
|
|
<div className="mb-6">
|
|
<h3 className="text-white font-semibold mb-2">Caption</h3>
|
|
<p className="text-gray-300 leading-relaxed">{media.caption}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-white/10">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Heart className="w-4 h-4 text-pink-400" />
|
|
<span className="text-gray-400 text-sm">Likes</span>
|
|
</div>
|
|
<div className="text-2xl font-bold text-white">{media.like_count || 0}</div>
|
|
</div>
|
|
|
|
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-white/10">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<MessageSquare className="w-4 h-4 text-blue-400" />
|
|
<span className="text-gray-400 text-sm">Comments</span>
|
|
</div>
|
|
<div className="text-2xl font-bold text-white">{media.comments_count || 0}</div>
|
|
</div>
|
|
|
|
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-white/10">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Calendar className="w-4 h-4 text-green-400" />
|
|
<span className="text-gray-400 text-sm">Posted</span>
|
|
</div>
|
|
<div className="text-lg font-bold text-white">
|
|
{new Date(media.timestamp).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-white/10">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Clock className="w-4 h-4 text-purple-400" />
|
|
<span className="text-gray-400 text-sm">Time</span>
|
|
</div>
|
|
<div className="text-lg font-bold text-white">
|
|
{new Date(media.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* View on Instagram Button */}
|
|
{media.permalink && (
|
|
<a
|
|
href={media.permalink}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center justify-center gap-2 w-full py-3 rounded-xl bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-700 hover:to-purple-700 text-white font-semibold transition-all duration-300 hover:scale-105"
|
|
>
|
|
<ExternalLink className="w-5 h-5" />
|
|
View on Instagram
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* New Comment Input */}
|
|
<div className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl p-6 mb-8">
|
|
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-3">
|
|
<MessageSquare className="w-5 h-5 text-blue-400" />
|
|
Add a Comment
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<textarea
|
|
placeholder="Write your comment here..."
|
|
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-gray-400 focus:outline-none focus:border-blue-500/30 focus:ring-1 focus:ring-blue-500/30 min-h-[120px] resize-none"
|
|
value={newCommentText}
|
|
onChange={(e) => setNewCommentText(e.target.value)}
|
|
/>
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-gray-400 text-sm">
|
|
Comments are posted directly to Instagram
|
|
</p>
|
|
<button
|
|
onClick={postNewComment}
|
|
disabled={!newCommentText.trim()}
|
|
className="px-6 py-3 rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold transition-all duration-300 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
Post Comment
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column - Comments & Info */}
|
|
<div className="space-y-8">
|
|
{/* Comments Header */}
|
|
<div className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-xl rounded-2xl border border-white/10 shadow-2xl p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-xl font-bold text-white flex items-center gap-3">
|
|
<Users className="w-5 h-5 text-blue-400" />
|
|
Comments
|
|
</h3>
|
|
<div className="px-3 py-1 rounded-full bg-blue-500/20 text-blue-400 text-sm font-medium border border-blue-500/30">
|
|
{comments.length} total
|
|
</div>
|
|
</div>
|
|
|
|
{/* Comments List */}
|
|
<div className="space-y-4 max-h-[600px] overflow-y-auto pr-2">
|
|
{commentsLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
|
</div>
|
|
) : comments.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<MessageSquare className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
<p className="text-gray-400">No comments yet</p>
|
|
<p className="text-gray-500 text-sm mt-1">Be the first to comment!</p>
|
|
</div>
|
|
) : (
|
|
comments.map((comment) => (
|
|
<SocialCommentItem
|
|
key={comment.id}
|
|
comment={comment}
|
|
onReply={handleReply}
|
|
onDelete={handleDelete}
|
|
onHide={handleHide}
|
|
onEdit={handleEdit}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info Card */}
|
|
<div className="bg-gradient-to-br from-blue-900/20 to-indigo-900/20 rounded-2xl p-6 border border-blue-500/30 backdrop-blur-xl">
|
|
<h4 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Shield className="w-5 h-5 text-blue-400" />
|
|
Comment Management
|
|
</h4>
|
|
<ul className="space-y-3 text-sm">
|
|
<li className="flex items-start gap-2 text-gray-300">
|
|
<div className="w-5 h-5 rounded-full bg-green-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
<CheckCircle className="w-3 h-3 text-green-400" />
|
|
</div>
|
|
<span>All actions are manual and user-controlled</span>
|
|
</li>
|
|
<li className="flex items-start gap-2 text-gray-300">
|
|
<div className="w-5 h-5 rounded-full bg-green-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
<CheckCircle className="w-3 h-3 text-green-400" />
|
|
</div>
|
|
<span>No automated responses or AI-generated comments</span>
|
|
</li>
|
|
<li className="flex items-start gap-2 text-gray-300">
|
|
<div className="w-5 h-5 rounded-full bg-green-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
<CheckCircle className="w-3 h-3 text-green-400" />
|
|
</div>
|
|
<span>Direct moderation tools for community management</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Animation Styles */}
|
|
<style jsx>{`
|
|
@keyframes float {
|
|
0%, 100% {
|
|
transform: translateY(0) translateX(0);
|
|
}
|
|
50% {
|
|
transform: translateY(-20px) translateX(10px);
|
|
}
|
|
}
|
|
@keyframes float-slow {
|
|
0%, 100% {
|
|
transform: translateY(0) translateX(0);
|
|
}
|
|
50% {
|
|
transform: translateY(-30px) translateX(-15px);
|
|
}
|
|
}
|
|
.animate-float {
|
|
animation: float 8s ease-in-out infinite;
|
|
}
|
|
.animate-float-slow {
|
|
animation: float-slow 10s ease-in-out infinite;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MediaDetailsPage; |