2026-02-21 19:04:54 +00:00

498 lines
19 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 axios from "axios";
import { ApiServerBaseUrl } from "@/utils/baseurl.utils";
import React, { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { getSocialAuthStatus } from "@/utils/commonFunction.utils";
import {
Image as ImageIcon,
Video,
Album,
Heart,
MessageCircle,
ExternalLink,
BarChart3,
Calendar,
Filter,
Search,
RefreshCw,
ChevronRight,
Eye,
MoreVertical,
CheckCircle
} from "lucide-react";
type MediaItem = {
id: string;
caption?: string;
media_type: string;
media_url: string;
thumbnail_url?: string;
permalink: string;
timestamp: string;
like_count?: number;
comments_count?: number;
children?: { data: MediaItem[] };
};
const LIMIT = 12;
const Posts = () => {
const router = useRouter();
const [media, setMedia] = useState<MediaItem[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState("");
const [afterCursor, setAfterCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(true);
const [isInitialLoaded, setIsInitialLoaded] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [refreshing, setRefreshing] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);
/* 🔐 Validate + first fetch */
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;
}
fetchInitialMedia();
}
validate();
}, []);
/* ✅ FIRST FETCH ONLY */
const fetchInitialMedia = async () => {
try {
setLoading(true);
const res: any = await axios.get(
`${ApiServerBaseUrl}/social/media`,
{
params: {
userId: localStorage.getItem("user_email"),
limit: LIMIT,
},
}
);
setMedia(res.data?.data || []);
setAfterCursor(res.data?.paging?.cursors?.after || null);
setHasMore(!!res.data?.paging?.cursors?.after);
setIsInitialLoaded(true);
} catch (err: any) {
setError(err.response?.data?.message || err.message);
} finally {
setLoading(false);
}
};
/* NEXT PAGE FETCH */
const fetchMoreMedia = async () => {
if (!afterCursor || loadingMore) return;
try {
setLoadingMore(true);
const res: any = await axios.get(
`${ApiServerBaseUrl}/social/media`,
{
params: {
userId: localStorage.getItem("user_email"),
limit: LIMIT,
after: afterCursor,
},
}
);
setMedia((prev) => [...prev, ...(res.data?.data || [])]);
setAfterCursor(res.data?.paging?.cursors?.after || null);
setHasMore(!!res.data?.paging?.cursors?.after);
} catch (err: any) {
setError(err.response?.data?.message || err.message);
} finally {
setLoadingMore(false);
}
};
/* 👀 OBSERVER — ONLY AFTER INITIAL LOAD */
useEffect(() => {
if (!observerRef.current || !isInitialLoaded || !hasMore) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
fetchMoreMedia();
}
},
{ threshold: 1 }
);
observer.observe(observerRef.current);
return () => observer.disconnect();
}, [afterCursor, isInitialLoaded, hasMore]);
/* 🔄 REFRESH */
const handleRefresh = async () => {
setRefreshing(true);
await fetchInitialMedia();
setTimeout(() => setRefreshing(false), 1000);
};
/* 🔍 FILTERED MEDIA */
const filteredMedia = media.filter(item =>
!searchQuery ||
item.caption?.toLowerCase().includes(searchQuery.toLowerCase())
);
/* 📱 GET MEDIA TYPE ICON */
const getMediaTypeIcon = (type: string) => {
switch (type) {
case "IMAGE": return <ImageIcon className="w-4 h-4" />;
case "VIDEO": return <Video className="w-4 h-4" />;
case "CAROUSEL_ALBUM": return <Album className="w-4 h-4" />;
default: return <ImageIcon className="w-4 h-4" />;
}
};
/* 📅 FORMAT DATE */
const formatDate = (timestamp: string) => {
const date = new Date(timestamp);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
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-7xl mx-auto relative z-10">
{/* Header */}
<div className="mb-10">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center shadow-lg shadow-pink-500/30">
<ImageIcon className="w-8 h-8 text-white" />
</div>
<div>
<h1 className="text-3xl md:text-4xl font-bold text-white">
Instagram Media Library
</h1>
<p className="text-gray-300 mt-1">
Browse and manage all your Instagram posts
</p>
</div>
</div>
<div className="flex items-center gap-4">
<button
onClick={handleRefresh}
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
</button>
</div>
</div>
{/* Search and Filters */}
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Search posts by caption..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-gray-400 focus:outline-none focus:border-pink-500/30 focus:ring-1 focus:ring-pink-500/30"
/>
</div>
</div>
<div className="flex gap-2">
<button className="px-4 py-3 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<Filter className="w-4 h-4" />
Filter
</button>
<button className="px-4 py-3 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<Calendar className="w-4 h-4" />
Date
</button>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="bg-gradient-to-br from-gray-900/50 to-black/50 backdrop-blur-sm rounded-xl p-4 border border-white/10">
<div className="text-sm text-gray-400 mb-1">Total Posts</div>
<div className="text-2xl font-bold text-white">{media.length}</div>
</div>
<div className="bg-gradient-to-br from-gray-900/50 to-black/50 backdrop-blur-sm rounded-xl p-4 border border-white/10">
<div className="text-sm text-gray-400 mb-1">Images</div>
<div className="text-2xl font-bold text-white">
{media.filter(m => m.media_type === "IMAGE").length}
</div>
</div>
<div className="bg-gradient-to-br from-gray-900/50 to-black/50 backdrop-blur-sm rounded-xl p-4 border border-white/10">
<div className="text-sm text-gray-400 mb-1">Videos</div>
<div className="text-2xl font-bold text-white">
{media.filter(m => m.media_type === "VIDEO").length}
</div>
</div>
<div className="bg-gradient-to-br from-gray-900/50 to-black/50 backdrop-blur-sm rounded-xl p-4 border border-white/10">
<div className="text-sm text-gray-400 mb-1">Carousels</div>
<div className="text-2xl font-bold text-white">
{media.filter(m => m.media_type === "CAROUSEL_ALBUM").length}
</div>
</div>
</div>
</div>
{/* Loading State */}
{loading && !isInitialLoaded && (
<div className="flex items-center justify-center py-20">
<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 your Instagram posts...</p>
</div>
</div>
)}
{/* Error State */}
{error && (
<div className="bg-red-900/20 backdrop-blur-xl rounded-2xl p-8 border border-red-500/30 mb-8">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center">
<div className="text-red-400">!</div>
</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">Error Loading Posts</h3>
<p className="text-gray-300">{error}</p>
</div>
</div>
</div>
)}
{/* Media Grid */}
{!loading && (
<div className="mb-10">
{filteredMedia.length === 0 ? (
<div className="text-center py-20">
<ImageIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-xl font-bold text-white mb-2">No posts found</h3>
<p className="text-gray-400">
{searchQuery ? "No posts match your search." : "No Instagram posts available."}
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredMedia.map((item) => (
<div
key={item.id}
className="group bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-xl rounded-2xl border border-white/10 hover:border-pink-500/30 transition-all duration-300 hover:scale-[1.02] overflow-hidden shadow-lg hover:shadow-2xl"
>
{/* Media Type Badge */}
<div className="absolute top-3 right-3 z-10">
<div className="px-3 py-1 rounded-full bg-black/80 backdrop-blur-sm text-xs font-medium text-white flex items-center gap-1">
{getMediaTypeIcon(item.media_type)}
<span className="capitalize">{item.media_type.toLowerCase().replace('_', ' ')}</span>
</div>
</div>
{/* Media Preview */}
<div className="relative h-64 overflow-hidden bg-black">
{item.media_type === "IMAGE" && (
<img
src={item.media_url}
alt="Instagram post"
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
)}
{item.media_type === "VIDEO" && (
<div className="relative h-full">
<video
src={item.media_url}
poster={item.thumbnail_url}
className="w-full h-full object-cover"
loop
muted
playsInline
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center">
<Video className="w-6 h-6 text-white" />
</div>
</div>
</div>
)}
{item.media_type === "CAROUSEL_ALBUM" && item.children?.data && (
<div className="relative h-full">
<img
src={item.children.data[0].media_url}
alt="Carousel preview"
className="w-full h-full object-cover"
/>
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-black/60 text-xs text-white">
{item.children.data.length} items
</div>
</div>
)}
{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute bottom-4 left-4 right-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-white">
<Heart className="w-4 h-4" />
<span className="text-sm">{item.like_count || 0}</span>
</div>
<div className="flex items-center gap-1 text-white">
<MessageCircle className="w-4 h-4" />
<span className="text-sm">{item.comments_count || 0}</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Content */}
<div className="p-5">
{/* Date */}
<div className="flex items-center gap-2 text-gray-400 text-sm mb-3">
<Calendar className="w-4 h-4" />
{formatDate(item.timestamp)}
</div>
{/* Caption */}
{item.caption && (
<p className="text-gray-300 text-sm mb-4 line-clamp-2">
{item.caption}
</p>
)}
{/* Actions */}
<div className="flex gap-2">
<a
href={item.permalink}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-white text-sm font-medium transition-all duration-300 hover:scale-105 flex items-center justify-center gap-2"
title="View on Instagram"
>
<ExternalLink className="w-4 h-4" />
View
</a>
<button
onClick={() => router.push(`/social-media-posts/${item.id}`)}
className="flex-1 py-2 rounded-xl bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-700 hover:to-purple-700 text-white text-sm font-medium transition-all duration-300 hover:scale-105 flex items-center justify-center gap-2"
title="Manage comments and details"
>
<BarChart3 className="w-4 h-4" />
Details
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Load More Trigger */}
{hasMore && isInitialLoaded && (
<div ref={observerRef} className="py-8">
<div className="text-center">
{loadingMore ? (
<div className="flex items-center justify-center gap-3 text-gray-400">
<div className="animate-spin rounded-full h-6 w-6 border-t-2 border-b-2 border-pink-500"></div>
<span>Loading more posts...</span>
</div>
) : (
<p className="text-gray-400">Scroll to load more posts</p>
)}
</div>
</div>
)}
{/* No More Posts */}
{!hasMore && isInitialLoaded && media.length > 0 && (
<div className="text-center py-12">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-gray-900 to-black border border-white/10 flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-green-400" />
</div>
<h3 className="text-xl font-bold text-white mb-2">All posts loaded</h3>
<p className="text-gray-400">You've reached the end of your Instagram media</p>
</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 Posts;