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