veza/apps/web/src/components/social/FeedView.tsx
senke 12a78616df refactor(web): zero out @typescript-eslint/no-unused-vars (134 → 0)
Two-step cleanup of the no-unused-vars warning bucket :

1. Widened the rule's ignore patterns in eslint.config.js so the
   `_`-prefix convention works uniformly across all four contexts
   (function args, local vars, caught errors, destructured arrays).
   The argsIgnorePattern was already `^_` ; added varsIgnorePattern,
   caughtErrorsIgnorePattern, destructuredArrayIgnorePattern with
   the same `^_` regex. Knocked 17 warnings out instantly because the
   codebase had already adopted `_xxx` for unused locals and was
   waiting on this config change.

2. Fixed the remaining 117 cases across 99 files by pattern :
   * 26 catch-binding cases : `catch (e) {…}` → `catch {…}` (TS 4.0+
     optional binding, ES2019). Cleaner than `catch (_e)` for the
     dozen "swallow and toast" error handlers that don't read the
     error.
   * 58 unused imports removed (incl. one literal `electron`
     contextBridge import that crept in from a phantom port-attempt).
   * 28 destructure / assignment cases : prefixed with `_` where the
     name documents the contract (test fixtures, hook return tuples
     where one slot isn't used yet) ; deleted outright when the
     assignment had no side effect and no documentary value.
   * 3 function param cases : prefixed with `_`.
   * 2 self-recursive `requestAnimationFrame` blocks that were dead
     code (an interval-based alternative did the work) : deleted.

`tsc --noEmit` reports 0 errors after the changes. ESLint total
dropped from 1240 to 1108. Updated the baseline in
.github/workflows/ci.yml in the next commit.

Pattern decisions logged inline so future maintainers know that
`_`-prefix isn't slop — it's the documented, lint-aware way to mark
"intentionally unused" without having to remove the name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:05:32 +02: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 {
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 shadow-[0_0_8px_rgba(26,26,30,0.05)] 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>
);
};