fixed ts error

This commit is contained in:
nvrl committed 2026-02-17 19:31:09 +01:00
1 parent 02f2866deb
commit 0d7344f5e9
4 files changed
+225 -42

No files matched your search

@@ -0,0 +1,199 @@
import React, { useState, useEffect, useRef } from 'react';
import {
TextField,
Popper,
Paper,
MenuList,
MenuItem,
Avatar,
ListItemAvatar,
ListItemText,
Box,
alpha
} from '@mui/material';
import api from '../api';
interface UserSuggestion {
id: number;
username: string;
avatar: string | null;
}
interface MentionTextFieldProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
fullWidth?: boolean;
multiline?: boolean;
rows?: number;
size?: 'small' | 'medium';
onKeyDown?: (e: React.KeyboardEvent) => void;
}
const MentionTextField: React.FC<MentionTextFieldProps> = ({
value,
onChange,
placeholder,
fullWidth = true,
multiline = false,
rows,
size = 'small',
onKeyDown
}) => {
const [suggestions, setSuggestions] = useState<UserSuggestion[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
// Fetch users for suggestions (we could cache this or search on demand)
const [allUsers, setAllUsers] = useState<UserSuggestion[]>([]);
useEffect(() => {
const fetchUsers = async () => {
try {
const res = await api.get('flavors/search/');
const users = (res.data || [])
.filter((item: any) => item.type === 'user')
.map((item: any) => ({
id: item.id,
username: item.name,
avatar: item.image_url
}));
setAllUsers(users);
} catch (err) {
console.error('Failed to fetch users for mentions');
}
};
fetchUsers();
}, []);
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
const selectionStart = e.target.selectionStart || 0;
const textBeforeCursor = newValue.substring(0, selectionStart);
const lastAtPos = textBeforeCursor.lastIndexOf('@');
if (lastAtPos !== -1) {
const query = textBeforeCursor.substring(lastAtPos + 1);
// Ensure there's no space or newline between @ and the cursor
if (!query.includes(' ') && !query.includes('\n')) {
const filtered = allUsers.filter(u =>
u.username.toLowerCase().startsWith(query.toLowerCase())
);
setSuggestions(filtered);
setShowSuggestions(filtered.length > 0);
setSelectedIndex(0);
setAnchorEl(inputRef.current);
return;
}
}
setShowSuggestions(false);
};
const handleSelectSuggestion = (user: UserSuggestion) => {
const selectionStart = inputRef.current?.selectionStart || 0;
const textBeforeCursor = value.substring(0, selectionStart);
const textAfterCursor = value.substring(selectionStart);
const lastAtPos = textBeforeCursor.lastIndexOf('@');
const newValue =
value.substring(0, lastAtPos) +
'@' + user.username + ' ' +
textAfterCursor;
onChange(newValue);
setShowSuggestions(false);
// Return focus to input and move cursor after the mention
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
const newPos = lastAtPos + user.username.length + 2; // +1 for @, +1 for space
inputRef.current.setSelectionRange(newPos, newPos);
}
}, 0);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (showSuggestions) {
if (e.key === 'Tab' || e.key === 'Enter') {
e.preventDefault();
handleSelectSuggestion(suggestions[selectedIndex]);
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % suggestions.length);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => (prev - 1 + suggestions.length) % suggestions.length);
return;
}
if (e.key === 'Escape') {
setShowSuggestions(false);
return;
}
}
if (onKeyDown) onKeyDown(e);
};
return (
<Box sx={{ position: 'relative', width: fullWidth ? '100%' : 'auto' }}>
<TextField
inputRef={inputRef}
fullWidth={fullWidth}
multiline={multiline}
rows={rows}
size={size}
placeholder={placeholder}
value={value}
onChange={handleTextChange}
onKeyDown={handleKeyDown}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
autoComplete="off"
/>
<Popper
open={showSuggestions}
anchorEl={anchorEl}
placement="bottom-start"
style={{ zIndex: 2000, width: 250 }}
>
<Paper elevation={8} sx={{ mt: 1, borderRadius: 2, overflow: 'hidden', border: '1px solid', borderColor: 'divider' }}>
<MenuList dense sx={{ py: 0 }}>
{suggestions.map((user, index) => (
<MenuItem
key={user.id}
selected={index === selectedIndex}
onClick={() => handleSelectSuggestion(user)}
sx={{
py: 1,
'&.Mui-selected': {
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.15),
'&:hover': { bgcolor: (theme) => alpha(theme.palette.primary.main, 0.25) }
}
}}
>
<ListItemAvatar sx={{ minWidth: 40 }}>
<Avatar src={user.avatar || undefined} sx={{ width: 28, height: 28, fontSize: '0.8rem' }}>
{user.username.charAt(0).toUpperCase()}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={user.username}
primaryTypographyProps={{ variant: 'body2', fontWeight: 'bold' }}
/>
</MenuItem>
))}
</MenuList>
</Paper>
</Popper>
</Box>
);
};
export default MentionTextField;
+9 -4
View File
@@ -33,6 +33,7 @@ import { useTitle } from '../hooks/useTitle';
import RichText from '../components/RichText'; import RichText from '../components/RichText';
import { formatDate } from '../utils/date'; import { formatDate } from '../utils/date';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MentionTextField from '../components/MentionTextField';
interface Reply { interface Reply {
id: number; id: number;
@@ -297,11 +298,15 @@ const CommunityFeed: React.FC = () => {
</Box> </Box>
))} ))}
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}> <Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<TextField <MentionTextField
fullWidth size="small" placeholder={t('community.writeReply')} placeholder={t('community.writeReply')}
value={replyInputs[rating.id] || ''} value={replyInputs[rating.id] || ''}
onChange={(e) => setReplyInputs(prev => ({ ...prev, [rating.id]: e.target.value }))} onChange={(val) => setReplyInputs(prev => ({ ...prev, [rating.id]: val }))}
onKeyDown={(e) => e.key === 'Enter' && handleReplySubmit(rating.id)} onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleReplySubmit(rating.id);
}
}}
/> />
<IconButton color="primary" onClick={() => handleReplySubmit(rating.id)} disabled={!replyInputs[rating.id]}> <IconButton color="primary" onClick={() => handleReplySubmit(rating.id)} disabled={!replyInputs[rating.id]}>
<SendIcon /> <SendIcon />
+16 -19
View File
@@ -8,7 +8,6 @@ import {
CardContent, CardContent,
Rating as MuiRating, Rating as MuiRating,
Button, Button,
TextField,
Avatar, Avatar,
Paper, Paper,
CircularProgress, CircularProgress,
@@ -26,6 +25,7 @@ import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import VerifiedIcon from '@mui/icons-material/Verified'; import VerifiedIcon from '@mui/icons-material/Verified';
import StarIcon from '@mui/icons-material/Star'; import StarIcon from '@mui/icons-material/Star';
import { formatDate } from '../../utils/date'; import { formatDate } from '../../utils/date';
import MentionTextField from '../../components/MentionTextField';
interface Reply { interface Reply {
id: number; id: number;
@@ -351,16 +351,14 @@ const FlavorDetail: React.FC = () => {
<Typography component="legend" gutterBottom color="text.secondary">Score</Typography> <Typography component="legend" gutterBottom color="text.secondary">Score</Typography>
<MuiRating max={10} value={newScore} onChange={(_, val) => setNewScore(val)} size="large" /> <MuiRating max={10} value={newScore} onChange={(_, val) => setNewScore(val)} size="large" />
</Box> </Box>
<TextField <MentionTextField
fullWidth
multiline multiline
rows={3} rows={3}
placeholder="Share your thoughts..." placeholder="Share your thoughts..."
value={newComment} value={newComment}
onChange={(e) => setNewComment(e.target.value)} onChange={(val) => setNewComment(val)}
sx={{ mb: 3, bgcolor: 'background.paper' }}
/> />
<Button variant="contained" type="submit" disabled={!newScore} sx={{ borderRadius: 2 }}> <Button variant="contained" type="submit" disabled={!newScore} sx={{ mt: 3, borderRadius: 2 }}>
Submit Review Submit Review
</Button> </Button>
</form> </form>
@@ -380,12 +378,11 @@ const FlavorDetail: React.FC = () => {
// Edit Mode // Edit Mode
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<MuiRating max={10} value={editScore} onChange={(_, val) => setEditScore(val || 0)} size="large" /> <MuiRating max={10} value={editScore} onChange={(_, val) => setEditScore(val || 0)} size="large" />
<TextField <MentionTextField
multiline multiline
fullWidth
rows={3} rows={3}
value={editComment} value={editComment}
onChange={(e) => setEditComment(e.target.value)} onChange={(val) => setEditComment(val)}
/> />
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button variant="contained" onClick={() => handleUpdateRating(rating.id)}>{t('common.save')}</Button> <Button variant="contained" onClick={() => handleUpdateRating(rating.id)}>{t('common.save')}</Button>
@@ -479,12 +476,10 @@ const FlavorDetail: React.FC = () => {
</Box> </Box>
{editingReplyId === reply.id ? ( {editingReplyId === reply.id ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
<TextField <MentionTextField
size="small"
fullWidth
multiline multiline
value={editReplyText} value={editReplyText}
onChange={(e) => setEditReplyText(e.target.value)} onChange={(val) => setEditReplyText(val)}
/> />
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button variant="contained" size="small" onClick={() => handleUpdateReply(reply.id)}>{t('common.save')}</Button> <Button variant="contained" size="small" onClick={() => handleUpdateReply(reply.id)}>{t('common.save')}</Button>
@@ -503,15 +498,17 @@ const FlavorDetail: React.FC = () => {
{currentUser && ( {currentUser && (
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}> <Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<TextField <MentionTextField
size="small"
placeholder={t('community.writeReply')} placeholder={t('community.writeReply')}
fullWidth
value={replyInputs[rating.id] || ''} value={replyInputs[rating.id] || ''}
onChange={(e) => setReplyInputs({ ...replyInputs, [rating.id]: e.target.value })} onChange={(val) => setReplyInputs({ ...replyInputs, [rating.id]: val })}
sx={{ '& .MuiOutlinedInput-root': { bgcolor: 'background.paper' } }} onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleReplySubmit(rating.id);
}
}}
/> />
<Button variant="contained" size="small" disabled={!replyInputs[rating.id]} onClick={() => handleReplySubmit(rating.id)}>{t('common.reply')}</Button> <Button variant="contained" size="small" disabled={!replyInputs[rating.id]} onClick={() => handleReplySubmit(rating.id)} sx={{ height: 40, alignSelf: 'flex-start' }}>{t('common.reply')}</Button>
</Box> </Box>
)} )}
</CardContent> </CardContent>
+1 -19
View File
@@ -14,25 +14,7 @@ export default defineConfig(({ mode }) => {
allowedHosts: allowedHosts, allowedHosts: allowedHosts,
}, },
build: { build: {
rollupOptions: { chunkSizeWarningLimit: 2000,
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('@mui')) {
return 'vendor-mui';
}
if (id.includes('react')) {
return 'vendor-react';
}
if (id.includes('axios') || id.includes('i18next')) {
return 'vendor-utils';
}
return 'vendor';
}
},
},
},
chunkSizeWarningLimit: 1000, // Increase limit slightly as well
}, },
} }
}) })