veza/apps/web/src/components/social/FeedView.tsx
senke 6fad0ad68d fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- 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>
2026-03-24 21:18:49 +01:00

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>
);
};