Rating added

This commit is contained in:
Sidharth Prabhu
2026-04-17 14:44:00 +05:30
parent ab18786b41
commit 8a41d650a2
17 changed files with 1219 additions and 26 deletions

View File

@@ -0,0 +1,162 @@
.feedback-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 5000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.feedback-modal {
background: white;
width: 100%;
max-width: 450px;
border-radius: 32px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
max-height: 90vh;
}
.feedback-header {
padding: 24px;
text-align: center;
background: linear-gradient(135deg, var(--primary) 0%, #4338ca 100%);
color: white;
}
.feedback-header h2 {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 4px;
}
.feedback-header p {
font-size: 0.9rem;
opacity: 0.9;
font-weight: 500;
}
.feedback-body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.rating-item-card {
background: var(--bg);
border-radius: 20px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid var(--border);
}
.rating-item-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.rating-item-name {
font-weight: 700;
color: var(--text-dark);
font-size: 1rem;
}
.stars-container {
display: flex;
gap: 8px;
justify-content: center;
}
.star-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
color: #e2e8f0;
}
.star-btn.active {
color: #fbbf24;
transform: scale(1.1);
}
.star-btn:active {
transform: scale(0.9);
}
.feedback-comment-section {
margin-top: 8px;
}
.feedback-comment-section label {
display: block;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-mid);
margin-bottom: 8px;
}
.feedback-comment-section textarea {
width: 100%;
border-radius: 16px;
border: 2px solid var(--border);
padding: 12px 16px;
font-family: inherit;
font-size: 0.95rem;
resize: none;
min-height: 80px;
outline: none;
transition: all 0.2s;
}
.feedback-comment-section textarea:focus {
border-color: var(--primary);
background: white;
}
.feedback-footer {
padding: 24px;
display: flex;
gap: 12px;
background: white;
border-top: 1px solid var(--border);
}
.btn-submit-feedback {
flex: 2;
background: var(--primary);
color: white;
border: none;
padding: 16px;
border-radius: 16px;
font-weight: 700;
font-size: 1rem;
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3);
}
.btn-submit-feedback:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-skip-feedback {
flex: 1;
background: var(--bg);
color: var(--text-mid);
border: 1px solid var(--border);
padding: 16px;
border-radius: 16px;
font-weight: 700;
font-size: 1rem;
}

View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { Star, Send, X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import './FeedbackModal.css';
interface OrderItem {
productId: number;
productName: string;
price: number;
quantity: number;
}
interface Order {
id: number;
orderNumber: string;
items: OrderItem[];
}
interface FeedbackModalProps {
order: Order;
userName: string;
userId: number;
onClose: () => void;
onSubmit: (data: any) => Promise<void>;
}
const FeedbackModal: React.FC<FeedbackModalProps> = ({ order, userName, userId, onClose, onSubmit }) => {
const [ratings, setRatings] = useState<Record<number, number>>(
order.items.reduce((acc, item) => ({ ...acc, [item.productId]: 0 }), {})
);
const [comment, setComment] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSetRating = (productId: number, rating: number) => {
setRatings(prev => ({ ...prev, [productId]: rating }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Check if at least one item is rated
const hasAtLeastOneRating = Object.values(ratings).some(r => r > 0);
if (!hasAtLeastOneRating) {
alert('Please provide at least one rating');
return;
}
setIsSubmitting(true);
const itemRatings = order.items.map(item => ({
productId: item.productId,
productName: item.productName,
rating: ratings[item.productId] || 0
}));
const feedbackData = {
order: { id: order.id },
userId: userId,
userName: userName,
itemRatings: itemRatings,
comment: comment
};
try {
await onSubmit(feedbackData);
onClose();
} catch (error) {
console.error('Error submitting feedback:', error);
alert('Failed to submit feedback. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="feedback-overlay" onClick={onClose}>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
className="feedback-modal"
onClick={e => e.stopPropagation()}
>
<div className="feedback-header">
<h2>Rate Your Meal</h2>
<p>How was your experience with Order #{order.orderNumber.split('-')[1]}?</p>
</div>
<form onSubmit={handleSubmit} className="feedback-body">
{order.items.map((item) => (
<div key={item.productId} className="rating-item-card">
<div className="rating-item-info">
<span className="rating-item-name">{item.productName}</span>
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest opacity-60">
{item.quantity} {item.quantity > 1 ? 'Units' : 'Unit'}
</span>
</div>
<div className="stars-container">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
className={`star-btn ${ratings[item.productId] >= star ? 'active' : ''}`}
onClick={() => handleSetRating(item.productId, star)}
>
<Star size={32} fill={ratings[item.productId] >= star ? "currentColor" : "none"} strokeWidth={2} />
</button>
))}
</div>
</div>
))}
<div className="feedback-comment-section">
<label>Additional Comments</label>
<textarea
placeholder="What did you like or what can we improve?"
value={comment}
onChange={(e) => setComment(e.target.value)}
maxLength={200}
/>
</div>
</form>
<div className="feedback-footer">
<button type="button" className="btn-skip-feedback" onClick={onClose}>
Skip
</button>
<button
type="submit"
className="btn-submit-feedback"
disabled={isSubmitting}
onClick={handleSubmit}
>
{isSubmitting ? 'Sending...' : 'Submit Rating'}
</button>
</div>
</motion.div>
</div>
);
};
export default FeedbackModal;

View File

@@ -48,6 +48,7 @@ export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children
// Map BaseItems to Categories
const mappedCategories: Category[] = baseItemsData.map((item: any, index: number) => ({
id: item.id.toString(),
name: item.name,
description: item.description,
emoji: EMOJIS[index % EMOJIS.length],
image: item.imageData?.trim() ? (item.imageData.trim().startsWith('data:') ? item.imageData.trim() : `data:image/png;base64,${item.imageData.trim()}`) : '',

View File

@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
import Header from '../components/Header';
import ItemCard from '../components/ItemCard';
import CartTab from '../components/CartTab';
import BottomNav from '../components/BottomNav';
import { useFood } from '../contexts/FoodContext';
const CategoryScreen: React.FC = () => {
@@ -39,6 +40,7 @@ const CategoryScreen: React.FC = () => {
</div>
</main>
<CartTab />
<BottomNav />
</div>
);
};

View File

@@ -273,3 +273,62 @@
text-align: center;
color: var(--text-mid);
}
/* Feedback Snackbar */
.feedback-snackbar {
position: fixed;
bottom: 80px; /* Above bottom nav */
left: 16px;
right: 16px;
background: white;
border-radius: 20px;
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border: 1px solid var(--primary-light);
z-index: 4000;
cursor: pointer;
animation: snackbarSlideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes snackbarSlideUp {
from { transform: translateY(100px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.feedback-snackbar-content {
display: flex;
align-items: center;
gap: 12px;
}
.feedback-snackbar-icon {
width: 40px;
height: 40px;
background: var(--primary-light);
color: var(--primary);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.feedback-snackbar-text h4 {
font-size: 14px;
font-weight: 800;
color: var(--text-dark);
margin: 0;
}
.feedback-snackbar-text p {
font-size: 11px;
color: var(--text-mid);
margin: 0;
font-weight: 600;
}
.feedback-snackbar-action {
color: var(--primary);
}

View File

@@ -1,12 +1,14 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Search, RefreshCcw, X } from 'lucide-react';
import { Search, RefreshCcw, X, Star, ChevronRight } from 'lucide-react';
import Header from '../components/Header';
import ItemCard from '../components/ItemCard';
import CartTab from '../components/CartTab';
import BottomNav from '../components/BottomNav';
import FeedbackModal from '../components/FeedbackModal';
import { useFood } from '../contexts/FoodContext';
import { useAuth } from '../contexts/AuthContext';
import { motion, AnimatePresence } from 'framer-motion';
import './HomeScreen.css';
const HomeScreen: React.FC = () => {
@@ -14,6 +16,74 @@ const HomeScreen: React.FC = () => {
const { categories, stalls, foodItems, isLoading, error, refreshData } = useFood();
const { user } = useAuth();
const [searchQuery, setSearchQuery] = useState('');
// Feedback state
const [unratedOrder, setUnratedOrder] = useState<any>(null);
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
const [showFeedbackSnackbar, setShowFeedbackSnackbar] = useState(false);
useEffect(() => {
if (user?.id) {
checkForUnratedOrder();
}
}, [user]);
const checkForUnratedOrder = async () => {
// Don't show if already dismissed in this session
if (sessionStorage.getItem('feedback_dismissed')) return;
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/latest-unrated/${user?.id}`);
if (response.status === 200) {
const order = await response.json();
console.log('Unrated order found:', order);
setUnratedOrder(order);
setShowFeedbackSnackbar(true);
} else {
console.log('No unrated orders or error status:', response.status);
}
} catch (error) {
console.error('Error checking for unrated orders:', error);
}
};
const handleSkipFeedback = async () => {
if (!unratedOrder) return;
try {
await fetch(`http://${window.location.hostname}:8080/api/feedback/skip/${unratedOrder.id}`, {
method: 'POST'
});
setShowFeedbackModal(false);
setShowFeedbackSnackbar(false);
setUnratedOrder(null);
sessionStorage.setItem('feedback_dismissed', 'true');
} catch (error) {
console.error('Error skipping feedback:', error);
// Fallback: still hide it locally
setShowFeedbackModal(false);
setShowFeedbackSnackbar(false);
}
};
const handleFeedbackSubmit = async (feedbackData: any) => {
try {
const response = await fetch(`http://${window.location.hostname}:8080/api/feedback/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedbackData)
});
if (response.ok) {
setShowFeedbackModal(false);
setShowFeedbackSnackbar(false);
setUnratedOrder(null);
sessionStorage.setItem('feedback_dismissed', 'true'); // Don't show for others in this session
}
} catch (error) {
throw error;
}
};
const popularItems = useMemo(() => foodItems.filter(item => item.isPopular), [foodItems]);
@@ -179,6 +249,43 @@ const HomeScreen: React.FC = () => {
</main>
<CartTab />
<BottomNav />
<AnimatePresence>
{showFeedbackSnackbar && unratedOrder && !showFeedbackModal && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="feedback-snackbar"
onClick={() => setShowFeedbackModal(true)}
>
<div className="feedback-snackbar-content">
<div className="feedback-snackbar-icon">
<Star size={20} fill="currentColor" />
</div>
<div className="feedback-snackbar-text">
<h4>Rate your last meal</h4>
<p>Order #{unratedOrder.displayOrderId || unratedOrder.id}</p>
</div>
</div>
<div className="feedback-snackbar-action">
<ChevronRight size={20} />
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showFeedbackModal && unratedOrder && (
<FeedbackModal
order={unratedOrder}
userId={user?.id || 0}
userName={user?.name || 'User'}
onClose={handleSkipFeedback}
onSubmit={handleFeedbackSubmit}
/>
)}
</AnimatePresence>
</div>
);
};