mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed wrong redirects
This commit is contained in:
1 parent
8a5402059c
commit
c7ab8b3a67
2 files changed
+88
-119
No files matched your search
@@ -8,7 +8,9 @@ import {
|
||||
Rating as MuiRating,
|
||||
Button,
|
||||
TextField,
|
||||
Avatar
|
||||
Avatar,
|
||||
Alert,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import api from '../../api';
|
||||
import { useTitle } from '../../hooks/useTitle';
|
||||
@@ -49,20 +51,16 @@ const FlavorDetail: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [replyInputs, setReplyInputs] = useState<{[key: number]: string}>({});
|
||||
const [currentUser, setCurrentUser] = useState<string | null>(null);
|
||||
|
||||
// Rating form state
|
||||
const [newScore, setNewScore] = useState<number | null>(null);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
|
||||
// Edit state
|
||||
const [editMode, setEditMode] = useState<number | null>(null);
|
||||
const [editScore, setEditScore] = useState(0);
|
||||
const [editComment, setEditComment] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getUser = async () => {
|
||||
try {
|
||||
const res = await api.get('users/me/');
|
||||
setCurrentUser(res.data.username);
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
getUser();
|
||||
}, []);
|
||||
|
||||
const fetchFlavor = async () => {
|
||||
try {
|
||||
const res = await api.get(`flavors/${id}/`);
|
||||
@@ -75,26 +73,48 @@ const FlavorDetail: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getUser = async () => {
|
||||
try {
|
||||
const res = await api.get('users/me/');
|
||||
setCurrentUser(res.data.username);
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
getUser();
|
||||
fetchFlavor();
|
||||
}, [id]);
|
||||
|
||||
useTitle(flavor?.name || 'Flavor Detail');
|
||||
|
||||
const handleRatingSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newScore) {
|
||||
alert('Please select a score');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.post('ratings/', { flavor: flavor?.id, score: newScore, comment: newComment });
|
||||
setNewScore(null);
|
||||
setNewComment('');
|
||||
fetchFlavor();
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.error || 'Failed to submit rating');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplySubmit = async (ratingId: number) => {
|
||||
const text = replyInputs[ratingId];
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
await api.post(`ratings/${ratingId}/reply/`, { text });
|
||||
setReplyInputs({ ...replyInputs, [ratingId]: '' });
|
||||
fetchFlavor(); // Refresh to show new reply
|
||||
fetchFlavor();
|
||||
} catch (err) {
|
||||
alert('Failed to submit reply');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRating = async (ratingId: number) => {
|
||||
if (!confirm('Are you sure you want to delete your review?')) return;
|
||||
if (!confirm('Are you sure?')) return;
|
||||
try {
|
||||
await api.delete(`ratings/${ratingId}/`);
|
||||
fetchFlavor();
|
||||
@@ -128,95 +148,69 @@ const FlavorDetail: React.FC = () => {
|
||||
← Back to {flavor.category_name}
|
||||
</Button>
|
||||
|
||||
<Card sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, mb: 4, overflow: 'visible' }}>
|
||||
<Card sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, mb: 4 }}>
|
||||
{flavor.image_url && (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', md: 400 },
|
||||
height: 400,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: 'rgba(255,255,255,0.05)',
|
||||
p: 4
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={flavor.image_url}
|
||||
sx={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
filter: 'drop-shadow(0px 10px 20px rgba(0,0,0,0.3))'
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ width: { xs: '100%', md: 400 }, height: 400, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'rgba(255,255,255,0.05)', p: 4 }}>
|
||||
<Box component="img" src={flavor.image_url} sx={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
|
||||
</Box>
|
||||
)}
|
||||
<CardContent sx={{ flex: 1, p: 4 }}>
|
||||
{!flavor.is_available && (
|
||||
<Box sx={{
|
||||
display: 'inline-block',
|
||||
bgcolor: 'error.main',
|
||||
color: 'white',
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
mb: 2,
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Out of Stock
|
||||
</Box>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>Out of Stock</Alert>
|
||||
)}
|
||||
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>{flavor.name}</Typography>
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>{flavor.category_name}</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
||||
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} size="large" />
|
||||
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} size="large" max={10} />
|
||||
<Typography variant="h5" sx={{ ml: 2 }}>{(flavor.average_rating || 0).toFixed(1)} / 10</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ ml: 1 }}>({flavor.ratings.length} ratings)</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body1" sx={{ mb: 4, lineHeight: 1.8 }}>
|
||||
{flavor.description}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ mb: 4, lineHeight: 1.8 }}>{flavor.description}</Typography>
|
||||
|
||||
{flavor.shop_url && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
component="a"
|
||||
href={flavor.shop_url}
|
||||
target="_blank"
|
||||
sx={{ px: 4, py: 1.5 }}
|
||||
>
|
||||
<Button variant="contained" size="large" component="a" href={flavor.shop_url} target="_blank">
|
||||
Buy Now
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* New Rating Form */}
|
||||
{currentUser && flavor.user_rating === null && (
|
||||
<Paper sx={{ p: 3, mb: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>Rate this flavor</Typography>
|
||||
<form onSubmit={handleRatingSubmit}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography component="legend">Score (1-10)</Typography>
|
||||
<MuiRating max={10} value={newScore} onChange={(_, val) => setNewScore(val)} size="large" />
|
||||
</Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Your Comment (Optional)"
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Button variant="contained" type="submit" disabled={!newScore}>Submit Rating</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Typography variant="h4" gutterBottom sx={{ mt: 6, mb: 3 }}>Community Reviews</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{flavor.ratings.length === 0 ? (
|
||||
<Typography color="text.secondary">No reviews yet. Be the first to rate this flavor!</Typography>
|
||||
<Typography color="text.secondary">No reviews yet.</Typography>
|
||||
) : (
|
||||
flavor.ratings.map((rating: Rating) => (
|
||||
<Card key={rating.id} variant="outlined">
|
||||
<CardContent>
|
||||
{editMode === rating.id ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h6">Edit Review</Typography>
|
||||
<MuiRating max={10} value={editScore} onChange={(_, val) => setEditScore(val || 0)} />
|
||||
<TextField
|
||||
multiline
|
||||
fullWidth
|
||||
label="Comment"
|
||||
value={editComment}
|
||||
onChange={(e) => setEditComment(e.target.value)}
|
||||
/>
|
||||
<TextField multiline fullWidth value={editComment} onChange={(e) => setEditComment(e.target.value)} />
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="contained" onClick={() => handleUpdateRating(rating.id)}>Save</Button>
|
||||
<Button onClick={() => setEditMode(null)}>Cancel</Button>
|
||||
@@ -225,23 +219,15 @@ const FlavorDetail: React.FC = () => {
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Avatar sx={{ width: 32, height: 32, mr: 1.5, fontSize: '0.875rem' }}>
|
||||
{rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Avatar sx={{ width: 32, height: 32, mr: 1.5 }}>{rating.user.charAt(0).toUpperCase()}</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>{rating.user}</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<MuiRating value={rating.score} readOnly size="small" />
|
||||
<Typography variant="body2" sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
<MuiRating value={rating.score} readOnly size="small" max={10} />
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
|
||||
{rating.comment && (
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>{rating.comment}</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(rating.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
{rating.comment && <Typography variant="body1" sx={{ mb: 2 }}>{rating.comment}</Typography>}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption" color="text.secondary">{new Date(rating.created_at).toLocaleDateString()}</Typography>
|
||||
{currentUser === rating.user && (
|
||||
<Box>
|
||||
<Button size="small" onClick={() => startEdit(rating)}>Edit</Button>
|
||||
@@ -252,39 +238,21 @@ const FlavorDetail: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Replies Section */}
|
||||
{(rating.replies.length > 0 || true) && ( // Always show reply section essentially
|
||||
<Box sx={{ mt: 2, ml: 4, pl: 2, borderLeft: '2px solid rgba(0,0,0,0.1)' }}>
|
||||
{rating.replies.map((reply: Reply) => (
|
||||
<Box key={reply.id} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.85rem' }}>
|
||||
{reply.user}
|
||||
</Typography>
|
||||
<Typography variant="body2">{reply.text}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(reply.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Write a reply..."
|
||||
fullWidth
|
||||
value={replyInputs[rating.id] || ''}
|
||||
onChange={(e) => setReplyInputs({ ...replyInputs, [rating.id]: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="text"
|
||||
disabled={!replyInputs[rating.id]}
|
||||
onClick={() => handleReplySubmit(rating.id)}
|
||||
>
|
||||
Reply
|
||||
</Button>
|
||||
<Box sx={{ mt: 2, ml: { xs: 2, md: 4 }, pl: 2, borderLeft: '2px solid rgba(0,0,0,0.1)' }}>
|
||||
{rating.replies.map((reply: Reply) => (
|
||||
<Box key={reply.id} sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.85rem' }}>{reply.user}</Typography>
|
||||
<Typography variant="body2">{reply.text}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{new Date(reply.created_at).toLocaleDateString()}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
{currentUser && (
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<TextField size="small" placeholder="Write a reply..." fullWidth value={replyInputs[rating.id] || ''} onChange={(e) => setReplyInputs({ ...replyInputs, [rating.id]: e.target.value })} />
|
||||
<Button size="small" disabled={!replyInputs[rating.id]} onClick={() => handleReplySubmit(rating.id)}>Reply</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user