mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed deletion
This commit is contained in:
1 parent
d0aff29a71
commit
1fb318f6a1
7 files changed
+493
-212
No files matched your search
@@ -60,6 +60,16 @@ export const useCommunityFeed = (page: number) =>
|
||||
placeholderData: (prev) => prev,
|
||||
});
|
||||
|
||||
export const useRecentRatings = (enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: ['recentRatings'],
|
||||
queryFn: async (): Promise<FeedRating[]> => {
|
||||
const res = await api.get<FeedRating[] | { results: FeedRating[] }>('ratings/recent/');
|
||||
return Array.isArray(res.data) ? res.data : (res.data.results ?? []);
|
||||
},
|
||||
enabled,
|
||||
});
|
||||
|
||||
export const useFollowingList = () =>
|
||||
useQuery({
|
||||
queryKey: ['followingList'],
|
||||
|
||||
@@ -293,6 +293,12 @@ const resources = {
|
||||
searchFriends: 'Search friends...',
|
||||
writeReply: 'Add a reply...',
|
||||
rated: 'rated',
|
||||
tabFollowing: 'Following',
|
||||
tabDiscover: 'Discover',
|
||||
discoverSubtitle: 'See what the whole community is rating right now.',
|
||||
discoverEmpty: 'No recent activity yet.',
|
||||
discoverEmptyHint: 'Be the first to rate a flavor!',
|
||||
browseDiscover: 'Browse Discover',
|
||||
},
|
||||
support: {
|
||||
title: 'Help & Support',
|
||||
@@ -830,6 +836,12 @@ const resources = {
|
||||
searchFriends: 'Freunde suchen...',
|
||||
writeReply: 'Eine Antwort hinzufügen...',
|
||||
rated: 'bewertete',
|
||||
tabFollowing: 'Folge ich',
|
||||
tabDiscover: 'Entdecken',
|
||||
discoverSubtitle: 'Sieh, was die ganze Community gerade bewertet.',
|
||||
discoverEmpty: 'Noch keine Aktivität.',
|
||||
discoverEmptyHint: 'Sei der Erste, der einen Geschmack bewertet!',
|
||||
browseDiscover: 'Entdecken durchstöbern',
|
||||
},
|
||||
support: {
|
||||
title: 'Hilfe & Support',
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Box,
|
||||
CardContent,
|
||||
Avatar,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Button,
|
||||
Grid,
|
||||
@@ -15,14 +14,20 @@ import {
|
||||
IconButton,
|
||||
Collapse,
|
||||
Link as MuiLink,
|
||||
Tabs,
|
||||
Tab,
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import CommentIcon from '@mui/icons-material/Comment';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import WhatshotIcon from '@mui/icons-material/Whatshot';
|
||||
import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
|
||||
import ExploreIcon from '@mui/icons-material/Explore';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useCommunityFeed,
|
||||
useFollowedTopFlavors,
|
||||
useRecentRatings,
|
||||
type FeedRating,
|
||||
} from '../api/queries/useCommunityFeed';
|
||||
import { useCreateReply } from '../api/mutations/useRatingMutations';
|
||||
@@ -44,6 +49,8 @@ import {
|
||||
} from '../components/ui';
|
||||
import { useToast } from '../hooks/useToast';
|
||||
|
||||
type FeedTab = 'following' | 'discover';
|
||||
|
||||
const SidebarCard: React.FC<{
|
||||
icon: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
@@ -69,11 +76,41 @@ const SidebarCard: React.FC<{
|
||||
</GlassCard>
|
||||
);
|
||||
|
||||
const FeedSkeleton: React.FC = () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<GlassCard key={i} intensity="default">
|
||||
<CardContent sx={{ p: { xs: 2, sm: 3 } }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Skeleton variant="circular" width={48} height={48} />
|
||||
<Box>
|
||||
<Skeleton variant="text" width={120} height={24} />
|
||||
<Skeleton variant="text" width={80} height={16} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Skeleton variant="rounded" width={48} height={28} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Skeleton variant="text" width="40%" />
|
||||
<Skeleton variant="text" />
|
||||
<Skeleton variant="text" width="80%" />
|
||||
</Box>
|
||||
<Skeleton variant="rounded" width={70} height={70} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</GlassCard>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const CommunityFeed: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
useTitle(t('community.title'));
|
||||
const navigate = useNavigate();
|
||||
const { notify } = useToast();
|
||||
const [tab, setTab] = useState<FeedTab>('following');
|
||||
const [page, setPage] = useState(1);
|
||||
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
|
||||
const [expandedReplies, setExpandedReplies] = useState<Record<number, boolean>>({});
|
||||
@@ -81,21 +118,29 @@ const CommunityFeed: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const isAuthed = !!user;
|
||||
const { data: feed, isLoading: feedLoading, error: feedError } = useCommunityFeed(page);
|
||||
const {
|
||||
data: discover,
|
||||
isLoading: discoverLoading,
|
||||
error: discoverError,
|
||||
} = useRecentRatings(tab === 'discover');
|
||||
const { data: topFollowed = [] } = useFollowedTopFlavors();
|
||||
const createReply = useCreateReply();
|
||||
|
||||
const ratings: FeedRating[] = feed?.ratings ?? [];
|
||||
const followingRatings: FeedRating[] = feed?.ratings ?? [];
|
||||
const discoverRatings: FeedRating[] = discover ?? [];
|
||||
const ratings = tab === 'following' ? followingRatings : discoverRatings;
|
||||
const totalPages = feed?.totalPages ?? 1;
|
||||
const loading = feedLoading;
|
||||
const loading = tab === 'following' ? feedLoading : discoverLoading;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthed) navigate('/login');
|
||||
}, [isAuthed, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const status = (feedError as { response?: { status?: number } } | null)?.response?.status;
|
||||
const err = tab === 'following' ? feedError : discoverError;
|
||||
const status = (err as { response?: { status?: number } } | null)?.response?.status;
|
||||
if (status === 401) navigate('/login');
|
||||
}, [feedError, navigate]);
|
||||
}, [feedError, discoverError, tab, navigate]);
|
||||
|
||||
const handlePageChange = (_: React.ChangeEvent<unknown>, value: number) => {
|
||||
setPage(value);
|
||||
@@ -117,211 +162,253 @@ const CommunityFeed: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && ratings.length === 0)
|
||||
const renderCard = (rating: FeedRating) => {
|
||||
const expanded = !!expandedReplies[rating.id];
|
||||
const latestReply = rating.replies[rating.replies.length - 1];
|
||||
return (
|
||||
<PageShell>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</PageShell>
|
||||
<GlassCard key={rating.id} intensity="default">
|
||||
<CardContent sx={{ p: { xs: 2, sm: 3 } }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none' }}>
|
||||
<Avatar
|
||||
src={rating.user_avatar || undefined}
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
border: '2px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</Link>
|
||||
<Box>
|
||||
<Link
|
||||
to={`/profile/${rating.user}`}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
|
||||
{rating.user}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{formatDate(rating.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<RatingBadge score={rating.score} />
|
||||
</Box>
|
||||
|
||||
{user?.is_superuser && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/admin-panel/rating/${rating.id}`)}
|
||||
sx={{ mb: 2, borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
{t('admin.manageRating')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{t('community.rated')}{' '}
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/flavor/${rating.flavor}`}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
fontWeight: 700,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{rating.flavor_name}
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
{rating.comment ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ lineHeight: 1.6, fontStyle: 'italic', color: 'text.primary' }}
|
||||
>
|
||||
<RichText text={rating.comment} />
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||
{t('common.noComment')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ flexShrink: 0 }}>
|
||||
<FlavorThumb src={rating.flavor_image} name={rating.flavor_name} size={70} radius={1} />
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<CommentIcon fontSize="small" />}
|
||||
onClick={() => handleReplyToggle(rating.id)}
|
||||
sx={{ textTransform: 'none', color: 'text.secondary' }}
|
||||
>
|
||||
{rating.replies.length > 0
|
||||
? `${rating.replies.length} ${t('common.replies')}`
|
||||
: t('common.reply')}
|
||||
</Button>
|
||||
|
||||
{!expanded && latestReply && (
|
||||
<Box
|
||||
onClick={() => handleReplyToggle(rating.id)}
|
||||
sx={{
|
||||
mt: 1,
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: 'action.hover',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
alignItems: 'baseline',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8rem', flexShrink: 0 }}>
|
||||
{latestReply.user}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" noWrap sx={{ minWidth: 0 }}>
|
||||
<RichText text={latestReply.text} />
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ mt: 2, pl: 2, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
{rating.replies.map((reply) => (
|
||||
<Box key={reply.id} sx={{ mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.8rem' }}>
|
||||
{reply.user}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatDate(reply.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
<RichText text={reply.text} />
|
||||
</Typography>
|
||||
{user?.is_superuser && (
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ minWidth: 0, py: 0, fontSize: '0.7rem' }}
|
||||
onClick={() => navigate(`/admin-panel/reply/${reply.id}`)}
|
||||
>
|
||||
{t('admin.manageReply')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<MentionTextField
|
||||
placeholder={t('community.writeReply')}
|
||||
multiline
|
||||
rows={2}
|
||||
value={replyInputs[rating.id] || ''}
|
||||
onChange={(val) => setReplyInputs((prev) => ({ ...prev, [rating.id]: val }))}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
handleReplySubmit(rating.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleReplySubmit(rating.id)}
|
||||
disabled={!replyInputs[rating.id]}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
const subtitle =
|
||||
tab === 'discover' ? t('community.discoverSubtitle') : t('community.subtitle');
|
||||
|
||||
return (
|
||||
<PageShell hero={<HeroBackdrop variant="minimal" />}>
|
||||
<BackButton />
|
||||
|
||||
<SectionHeader title={t('community.title')} subtitle={t('community.subtitle')} />
|
||||
<SectionHeader title={t('community.title')} subtitle={subtitle} />
|
||||
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(_, v: FeedTab) => setTab(v)}
|
||||
sx={{ mb: 3, '& .MuiTab-root': { textTransform: 'none', fontWeight: 700 } }}
|
||||
>
|
||||
<Tab
|
||||
value="following"
|
||||
icon={<PeopleAltIcon fontSize="small" />}
|
||||
iconPosition="start"
|
||||
label={t('community.tabFollowing')}
|
||||
/>
|
||||
<Tab
|
||||
value="discover"
|
||||
icon={<ExploreIcon fontSize="small" />}
|
||||
iconPosition="start"
|
||||
label={t('community.tabDiscover')}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={{ xs: 12, lg: 8 }}>
|
||||
{ratings.length === 0 ? (
|
||||
<EmptyState
|
||||
title={t('community.quietFeed')}
|
||||
subtitle={t('community.followMore')}
|
||||
action={
|
||||
<Button variant="contained" component={Link} to="/" sx={{ borderRadius: 2 }}>
|
||||
{t('home.exploreFlavors')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{loading && ratings.length === 0 ? (
|
||||
<FeedSkeleton />
|
||||
) : ratings.length === 0 ? (
|
||||
tab === 'following' ? (
|
||||
<EmptyState
|
||||
title={t('community.quietFeed')}
|
||||
subtitle={t('community.followMore')}
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ExploreIcon />}
|
||||
onClick={() => setTab('discover')}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
{t('community.browseDiscover')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={t('community.discoverEmpty')}
|
||||
subtitle={t('community.discoverEmptyHint')}
|
||||
action={
|
||||
<Button variant="contained" component={Link} to="/" sx={{ borderRadius: 2 }}>
|
||||
{t('home.exploreFlavors')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
{ratings.map((rating) => (
|
||||
<GlassCard key={rating.id} intensity="default">
|
||||
<CardContent sx={{ p: { xs: 2, sm: 3 } }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none' }}>
|
||||
<Avatar
|
||||
src={rating.user_avatar || undefined}
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
border: '2px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</Link>
|
||||
<Box>
|
||||
<Link
|
||||
to={`/profile/${rating.user}`}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 700, lineHeight: 1.2 }}
|
||||
>
|
||||
{rating.user}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
{formatDate(rating.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<RatingBadge score={rating.score} />
|
||||
</Box>
|
||||
|
||||
{user?.is_superuser && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/admin-panel/rating/${rating.id}`)}
|
||||
sx={{ mb: 2, borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
{t('admin.manageRating')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{t('community.rated')}{' '}
|
||||
<MuiLink
|
||||
component={Link}
|
||||
to={`/flavor/${rating.flavor}`}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
fontWeight: 700,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{rating.flavor_name}
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
{rating.comment ? (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ lineHeight: 1.6, fontStyle: 'italic', color: 'text.primary' }}
|
||||
>
|
||||
<RichText text={rating.comment} />
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: 'italic' }}
|
||||
>
|
||||
{t('common.noComment')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ flexShrink: 0 }}>
|
||||
<FlavorThumb
|
||||
src={rating.flavor_image}
|
||||
name={rating.flavor_name}
|
||||
size={70}
|
||||
radius={1}
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<CommentIcon fontSize="small" />}
|
||||
onClick={() => handleReplyToggle(rating.id)}
|
||||
sx={{ textTransform: 'none', color: 'text.secondary' }}
|
||||
>
|
||||
{rating.replies.length > 0
|
||||
? `${rating.replies.length} ${t('common.replies')}`
|
||||
: t('common.reply')}
|
||||
</Button>
|
||||
|
||||
<Collapse in={expandedReplies[rating.id]}>
|
||||
<Box sx={{ mt: 2, pl: 2, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
{rating.replies.map((reply) => (
|
||||
<Box key={reply.id} sx={{ mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, fontSize: '0.8rem' }}
|
||||
>
|
||||
{reply.user}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatDate(reply.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
<RichText text={reply.text} />
|
||||
</Typography>
|
||||
{user?.is_superuser && (
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ minWidth: 0, py: 0, fontSize: '0.7rem' }}
|
||||
onClick={() => navigate(`/admin-panel/reply/${reply.id}`)}
|
||||
>
|
||||
{t('admin.manageReply')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<MentionTextField
|
||||
placeholder={t('community.writeReply')}
|
||||
multiline
|
||||
rows={2}
|
||||
value={replyInputs[rating.id] || ''}
|
||||
onChange={(val) =>
|
||||
setReplyInputs((prev) => ({ ...prev, [rating.id]: val }))
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
handleReplySubmit(rating.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleReplySubmit(rating.id)}
|
||||
disabled={!replyInputs[rating.id]}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
</GlassCard>
|
||||
))}
|
||||
{totalPages > 1 && (
|
||||
{ratings.map(renderCard)}
|
||||
{tab === 'following' && totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
|
||||
Reference in new issue
Block a user