Public Access
feat(ui): add follow button, follows card, and inbox page
This commit is contained in:
1 parent
3aed243e35
commit
43c4862606
23 files changed
+603
-2
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Users} from 'lucide-react';
|
||||
import {toast} from 'sonner';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
|
||||
import {fetchFollows, removeFollow} from '../utils/follows.js';
|
||||
import {profileHref} from '../utils/people.js';
|
||||
|
||||
export default function FollowsCard() {
|
||||
const {t} = useTranslation();
|
||||
const [follows, setFollows] = useState(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const {ok, data} = await fetchFollows();
|
||||
setFailed(!ok);
|
||||
if (ok) setFollows(data);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const end = async (id) => {
|
||||
const {ok, status} = await removeFollow(id);
|
||||
if (!ok && status !== 401) toast.error(t('FOLLOW.ERROR'));
|
||||
await load();
|
||||
};
|
||||
|
||||
const link = (displayName) => (
|
||||
<Link to={profileHref(displayName)} className="min-w-0 truncate hover:text-primary hover:underline">{displayName}</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><Users className="size-4 text-primary"/>{t('FOLLOW.TITLE')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{failed && <p className="text-sm text-destructive">{t('FOLLOW.LOAD_ERROR')}</p>}
|
||||
{follows && (
|
||||
<>
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium">{t('FOLLOW.FOLLOWING')}</h3>
|
||||
{follows.following.length === 0 && <p className="text-sm text-muted-foreground">{t('FOLLOW.NO_FOLLOWING')}</p>}
|
||||
<ul className="space-y-1">
|
||||
{follows.following.map((entry) => (
|
||||
<li key={entry.id} className="flex items-center justify-between gap-2 text-sm">
|
||||
{link(entry.displayName)}
|
||||
<Button variant="outline" size="sm" onClick={() => end(entry.id)}>{t('FOLLOW.UNFOLLOW')}</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium">{t('FOLLOW.FOLLOWERS')}</h3>
|
||||
{follows.followers.length === 0 && <p className="text-sm text-muted-foreground">{t('FOLLOW.NO_FOLLOWERS')}</p>}
|
||||
<ul className="space-y-1">
|
||||
{follows.followers.map((entry) => (
|
||||
<li key={entry.id} className="flex items-center justify-between gap-2 text-sm">
|
||||
{entry.displayName
|
||||
? link(entry.displayName)
|
||||
: <span className="min-w-0 truncate text-muted-foreground">{t('FOLLOW.PRIVATE_USER')}</span>}
|
||||
<Button variant="outline" size="sm" onClick={() => end(entry.id)}>{t('FOLLOW.REMOVE')}</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user