mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed ts error
This commit is contained in:
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;
|
||||
@@ -33,6 +33,7 @@ import { useTitle } from '../hooks/useTitle';
|
||||
import RichText from '../components/RichText';
|
||||
import { formatDate } from '../utils/date';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MentionTextField from '../components/MentionTextField';
|
||||
|
||||
interface Reply {
|
||||
id: number;
|
||||
@@ -297,11 +298,15 @@ const CommunityFeed: React.FC = () => {
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
fullWidth size="small" placeholder={t('community.writeReply')}
|
||||
<MentionTextField
|
||||
placeholder={t('community.writeReply')}
|
||||
value={replyInputs[rating.id] || ''}
|
||||
onChange={(e) => setReplyInputs(prev => ({ ...prev, [rating.id]: e.target.value }))}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleReplySubmit(rating.id)}
|
||||
onChange={(val) => setReplyInputs(prev => ({ ...prev, [rating.id]: val }))}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
handleReplySubmit(rating.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton color="primary" onClick={() => handleReplySubmit(rating.id)} disabled={!replyInputs[rating.id]}>
|
||||
<SendIcon />
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
CardContent,
|
||||
Rating as MuiRating,
|
||||
Button,
|
||||
TextField,
|
||||
Avatar,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
@@ -26,6 +25,7 @@ import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
import VerifiedIcon from '@mui/icons-material/Verified';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import { formatDate } from '../../utils/date';
|
||||
import MentionTextField from '../../components/MentionTextField';
|
||||
|
||||
interface Reply {
|
||||
id: number;
|
||||
@@ -351,16 +351,14 @@ const FlavorDetail: React.FC = () => {
|
||||
<Typography component="legend" gutterBottom color="text.secondary">Score</Typography>
|
||||
<MuiRating max={10} value={newScore} onChange={(_, val) => setNewScore(val)} size="large" />
|
||||
</Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
<MentionTextField
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="Share your thoughts..."
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
sx={{ mb: 3, bgcolor: 'background.paper' }}
|
||||
onChange={(val) => setNewComment(val)}
|
||||
/>
|
||||
<Button variant="contained" type="submit" disabled={!newScore} sx={{ borderRadius: 2 }}>
|
||||
<Button variant="contained" type="submit" disabled={!newScore} sx={{ mt: 3, borderRadius: 2 }}>
|
||||
Submit Review
|
||||
</Button>
|
||||
</form>
|
||||
@@ -380,12 +378,11 @@ const FlavorDetail: React.FC = () => {
|
||||
// Edit Mode
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<MuiRating max={10} value={editScore} onChange={(_, val) => setEditScore(val || 0)} size="large" />
|
||||
<TextField
|
||||
<MentionTextField
|
||||
multiline
|
||||
fullWidth
|
||||
rows={3}
|
||||
value={editComment}
|
||||
onChange={(e) => setEditComment(e.target.value)}
|
||||
onChange={(val) => setEditComment(val)}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="contained" onClick={() => handleUpdateRating(rating.id)}>{t('common.save')}</Button>
|
||||
@@ -479,12 +476,10 @@ const FlavorDetail: React.FC = () => {
|
||||
</Box>
|
||||
{editingReplyId === reply.id ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
<MentionTextField
|
||||
multiline
|
||||
value={editReplyText}
|
||||
onChange={(e) => setEditReplyText(e.target.value)}
|
||||
onChange={(val) => setEditReplyText(val)}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="contained" size="small" onClick={() => handleUpdateReply(reply.id)}>{t('common.save')}</Button>
|
||||
@@ -503,15 +498,17 @@ const FlavorDetail: React.FC = () => {
|
||||
|
||||
{currentUser && (
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
<MentionTextField
|
||||
placeholder={t('community.writeReply')}
|
||||
fullWidth
|
||||
value={replyInputs[rating.id] || ''}
|
||||
onChange={(e) => setReplyInputs({ ...replyInputs, [rating.id]: e.target.value })}
|
||||
sx={{ '& .MuiOutlinedInput-root': { bgcolor: 'background.paper' } }}
|
||||
onChange={(val) => setReplyInputs({ ...replyInputs, [rating.id]: val })}
|
||||
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>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
+1
-19
@@ -14,25 +14,7 @@ export default defineConfig(({ mode }) => {
|
||||
allowedHosts: allowedHosts,
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
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
|
||||
chunkSizeWarningLimit: 2000,
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user