- Fix 98 TypeScript errors across 37 files: - Service layer double-unwrapping (subscriptionService, distributionService, gearService) - Self-referencing variables in SearchPageResults - FeedView/ExploreView .posts→.items alignment - useQueueSync Zustand subscribe API - AdminAuditLogsView missing interface fields - Toast proxy type, interceptor type narrowing - 22 unused imports/variables removed - 5 storybook mock data fixes - Align frontend API calls with backend endpoints: - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics) - Chat: chatService uses /conversations (was mock data), WS URL from backend token - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros) - Settings: suppress 2FA toast error when endpoint unavailable - Fix marketplace products: seed uses 'active' status (was 'published') - Enrich seed: admin follows all creators (feed has content) - Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%) Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc. - Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
208 lines
7.5 KiB
TypeScript
208 lines
7.5 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Card } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Avatar } from '../ui/avatar';
|
|
import { Post } from '../../types';
|
|
import { PostCard } from './PostCard';
|
|
import { CreatePostModal } from './CreatePostModal';
|
|
import { ImageIcon, Video, Mic2, BarChart, Loader2, ArrowUp, ChevronDown } from 'lucide-react';
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
|
import { socialService } from '../../services/socialService';
|
|
import { logger } from '@/utils/logger';
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
|
|
|
export const FeedView: React.FC = () => {
|
|
const { addToast } = useToast();
|
|
const { data: user } = useUser();
|
|
const [posts, setPosts] = useState<Post[]>([]);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
const [hasNewPosts, setHasNewPosts] = useState(false);
|
|
const [, setIsSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadFeed();
|
|
}, []);
|
|
|
|
// Simulate periodic new-posts check
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
if (posts.length > 0 && !hasNewPosts) {
|
|
setHasNewPosts(true);
|
|
}
|
|
}, 30_000);
|
|
return () => clearInterval(interval);
|
|
}, [posts.length, hasNewPosts]);
|
|
|
|
const loadFeed = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await socialService.getFeed();
|
|
setPosts(res.items as unknown as Post[]);
|
|
setHasNewPosts(false);
|
|
} catch (e) {
|
|
logger.error('Error loading feed', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = useCallback(() => {
|
|
setHasNewPosts(false);
|
|
loadFeed();
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}, []);
|
|
|
|
const handleCreatePost = async (data: {
|
|
content: string;
|
|
visibility: string;
|
|
type: string;
|
|
}) => {
|
|
setIsSubmitting(true);
|
|
try {
|
|
const res = await socialService.createPost(data);
|
|
setPosts([res.post, ...posts]);
|
|
addToast('Post published successfully', 'success');
|
|
} catch (e) {
|
|
addToast('Failed to post', 'error');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const loadMore = async () => {
|
|
setLoadingMore(true);
|
|
try {
|
|
const res = await socialService.getFeed();
|
|
setPosts((prev) => [
|
|
...prev,
|
|
...(res.items as unknown as Post[]).map((p) => ({ ...p, id: `more-${Math.random()}` })),
|
|
]);
|
|
} catch (e) {
|
|
logger.error('Error loading more feed posts', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoadingMore(false);
|
|
}
|
|
};
|
|
|
|
if (loading)
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-24 gap-3">
|
|
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
|
<span className="text-xs text-muted-foreground">Loading feed…</span>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* New Posts Banner */}
|
|
{hasNewPosts && (
|
|
<button
|
|
onClick={handleRefresh}
|
|
className="w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-primary/10 text-primary text-sm font-medium border border-primary/20 hover:bg-primary/20 transition-all animate-fadeIn cursor-pointer"
|
|
>
|
|
<ArrowUp className="w-4 h-4" />
|
|
New posts available
|
|
</button>
|
|
)}
|
|
|
|
{/* Create Post Widget */}
|
|
<Card variant="default" className="border-t-2 border-t-primary/60 p-4 hover:border-t-primary transition-colors">
|
|
<div className="flex gap-3">
|
|
<Avatar
|
|
src={user?.avatar_url || ''}
|
|
alt="Your avatar"
|
|
fallback="You"
|
|
size="md"
|
|
status="online"
|
|
/>
|
|
<div
|
|
className="flex-1 cursor-pointer"
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
<div className="w-full bg-background/50 border border-border rounded-full px-4 py-2.5 text-muted-foreground hover:bg-background hover:border-primary/30 hover:text-foreground transition-all text-sm">
|
|
What are you working on today?
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between items-center mt-3 pl-14">
|
|
<div className="flex gap-1">
|
|
<button
|
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground hover:bg-muted/50 text-xs font-medium cursor-pointer transition-colors rounded-lg px-2.5 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
<ImageIcon className="w-4 h-4" /> Photo
|
|
</button>
|
|
<button
|
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 text-xs font-medium cursor-pointer transition-colors rounded-lg px-2.5 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
<Video className="w-4 h-4" /> Video
|
|
</button>
|
|
<button
|
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-success hover:bg-success/10 text-xs font-medium cursor-pointer transition-colors rounded-lg px-2.5 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
<Mic2 className="w-4 h-4" /> Audio
|
|
</button>
|
|
<button
|
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-warning hover:bg-warning/10 text-xs font-medium cursor-pointer transition-colors rounded-lg px-2.5 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
onClick={() => setShowCreateModal(true)}
|
|
>
|
|
<BarChart className="w-4 h-4" /> Poll
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Posts Feed */}
|
|
<div>
|
|
{posts.map((post, i) => (
|
|
<div
|
|
key={post.id}
|
|
className="animate-fadeIn"
|
|
style={{ animationDelay: `${Math.min(i * 50, 300)}ms`, animationFillMode: 'both' }}
|
|
>
|
|
<PostCard post={post} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Load More Trigger */}
|
|
<div className="text-center py-6">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={loadMore}
|
|
disabled={loadingMore}
|
|
className="gap-2 text-muted-foreground hover:text-foreground"
|
|
>
|
|
{loadingMore ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Loading…
|
|
</>
|
|
) : (
|
|
<>
|
|
<ChevronDown className="w-4 h-4" />
|
|
Load More
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{showCreateModal && (
|
|
<CreatePostModal
|
|
onClose={() => setShowCreateModal(false)}
|
|
onCreate={handleCreatePost}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|