498 lines
19 KiB
TypeScript
498 lines
19 KiB
TypeScript
"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; |