feat(ui): add follow button, follows card, and inbox page
Build, Test & Push Frontend / quality-check (push) Successful in 34s
Build, Test & Push Frontend / docker (push) Successful in 23s

This commit is contained in:
lobo committed 2026-10-06 16:22:22 +02:00
1 parent 3aed243e35
commit 43c4862606
23 files changed
+603 -2

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
import {useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Loader2} from 'lucide-react';
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import {cn} from '@/lib/utils';
import PageContainer from '../components/PageContainer.jsx';
import {formatDate} from '../utils/format.js';
import {digestParts, fetchInbox, markInboxRead} from '../utils/inbox.js';
/**
* The newest notes about the people the user follows. Opening the page marks them read.
*/
export default function InboxPage() {
const {t, i18n} = useTranslation();
const [notes, setNotes] = useState([]);
const [status, setStatus] = useState('loading');
useEffect(() => {
(async () => {
const {ok, status: code, data} = await fetchInbox();
if (code === 401) return;
if (!ok) {
setStatus('error');
return;
}
setNotes(data.results);
setStatus('ready');
// A failed mark is silent: the notes show as unread again on the next visit.
if (data.unread > 0) markInboxRead();
})();
}, []);
const text = (note) => {
const name = note.displayName ?? t('FOLLOW.PRIVATE_USER');
if (note.kind === 'VISIBILITY') return t('INBOX.VISIBILITY', {name});
const parts = digestParts(note).map(({key, count}) => t(`INBOX.PART_${key}`, {count})).join(', ');
return t('INBOX.DIGEST', {name, parts});
};
return (
<PageContainer className="space-y-4 px-4 py-6 md:px-8">
<Card>
<CardHeader>
<CardTitle>{t('INBOX.TITLE')}</CardTitle>
</CardHeader>
<CardContent>
{status === 'loading' && <Loader2 className="size-5 animate-spin text-muted-foreground"/>}
{status === 'error' && <p className="text-sm text-destructive">{t('INBOX.LOAD_ERROR')}</p>}
{status === 'ready' && notes.length === 0 && <p className="text-sm text-muted-foreground">{t('INBOX.EMPTY')}</p>}
{status === 'ready' && notes.length > 0 && (
<ul className="divide-y">
{notes.map((note) => (
<li key={note.id} className="space-y-1 py-3">
<p className="text-xs text-muted-foreground">
{formatDate(note.createdAt, i18n.language, {dateStyle: 'medium'})}
</p>
<p className={cn('break-words text-sm', !note.read && 'font-semibold')}>{text(note)}</p>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</PageContainer>
);
}