function Stars({value,size,onChange}){
size=size||16;
return (
{[1,2,3,4,5].map(n=>(
onChange(n):undefined}
style={{width:size,height:size,cursor:onChange?'pointer':'default',color:n<=value?'var(--gold-deep)':'var(--border-default)',fill:n<=value?'var(--gold-deep)':'none'}}>
))}
);
}
function RatingBar({star,count,total}){
const pct=total?Math.round((count/total)*100):0;
return (
);
}
function MediaPicker({images,setImages,video,setVideo}){
function onImg(e){
const files=Array.from(e.target.files||[]).slice(0,5-images.length);
files.forEach(f=>{
if(f.size>10*1024*1024){alert(f.name+' exceeds the 10 MB image limit.');return;}
setImages(prev=>prev.length>=5?prev:[...prev,{name:f.name,type:f.type,size:f.size,url:URL.createObjectURL(f)}]);
});
e.target.value='';
}
function onVid(e){
const f=e.target.files&&e.target.files[0]; if(!f)return;
if(f.size>50*1024*1024){alert('Video exceeds the 50 MB limit.');return;}
setVideo({name:f.name,type:f.type,size:f.size,url:URL.createObjectURL(f)});
e.target.value='';
}
return (
Photos & Video (optional)
{images.map((im,i)=>(
setImages(images.filter((_,j)=>j!==i))} style={{position:'absolute',top:2,right:2,background:'rgba(31,31,31,.7)',color:'#fff',border:'none',borderRadius:'50%',width:18,height:18,fontSize:11,cursor:'pointer'}}>×
))}
{images.length<5&&(
+ )}
{!video&&(
+Video )}
{video&&(
VideosetVideo(null)} style={{position:'absolute',top:2,right:2,background:'rgba(31,31,31,.7)',color:'#fff',border:'none',borderRadius:'50%',width:18,height:18,fontSize:11,cursor:'pointer'}}>×
)}
Up to 5 images (JPG/PNG/WEBP, 10MB each) and 1 video (MP4/MOV, 50MB).
);
}
function WriteReviewModal({eligible,existing,onClose,onSaved}){
const [rating,setRating]=React.useState(existing?existing.rating:5);
const [title,setTitle]=React.useState(existing?existing.title:'');
const [description,setDescription]=React.useState(existing?existing.description:'');
const [pros,setPros]=React.useState(existing?existing.pros:'');
const [cons,setCons]=React.useState(existing?existing.cons:'');
const [recommend,setRecommend]=React.useState(existing?existing.recommend:true);
const [images,setImages]=React.useState(existing?existing.images||[]:[]);
const [video,setVideo]=React.useState(existing?existing.video||null:null);
const [error,setError]=React.useState('');
function submit(){
if(!title.trim()||!description.trim()){setError('Please add a title and a short description.');return;}
const user=BB_AUTH.currentUser();
if(existing){
const res=ReviewStore.update(existing.id,{rating,title:title.trim(),description:description.trim(),pros:pros.trim(),cons:cons.trim(),recommend,images,video});
if(res&&res.error){setError(res.error);return;}
}else{
ReviewStore.submit({productId:eligible.productId,productName:eligible.productName,customerId:user.id,customerEmail:user.email,customerName:user.name,orderId:eligible.orderId,orderNumber:eligible.orderNumber,rating,title:title.trim(),description:description.trim(),pros:pros.trim(),cons:cons.trim(),recommend,images,video});
}
onSaved&&onSaved();
onClose();
}
return (
e.stopPropagation()}>
{existing?'Edit Your Review':'Write a Review'}
{eligible?eligible.productName:existing.productName}
setTitle(e.target.value)} maxLength={80} placeholder="Review title" style={{width:'100%',padding:'10px 12px',border:'1px solid var(--border-default)',borderRadius:'var(--radius-sm)',fontSize:14,marginBottom:12,fontFamily:'inherit'}}/>
setPros(e.target.value)} placeholder="Pros (optional)" style={{padding:'10px 12px',border:'1px solid var(--border-default)',borderRadius:'var(--radius-sm)',fontSize:14,fontFamily:'inherit'}}/>
setCons(e.target.value)} placeholder="Cons (optional)" style={{padding:'10px 12px',border:'1px solid var(--border-default)',borderRadius:'var(--radius-sm)',fontSize:14,fontFamily:'inherit'}}/>
Would you recommend this?
setRecommend(true)}/> Yes
setRecommend(false)}/> No
{error&&{error}
}
{existing?'Save Changes':'Submit Review'}
Cancel
);
}
function ReviewCard({r,onHelpful,voted}){
return (
{r.featured&&
Featured Experience }
{r.description}
{(r.pros||r.cons)&&(
{r.pros&&Pros: {r.pros} }
{r.cons&&Cons: {r.cons} }
)}
{(r.images&&r.images.length>0)&&(
{r.images.map((im,i)=>(
))}
)}
{r.customerName}
{r.verifiedPurchase&&Verified Purchase }
{new Date(r.createdDate).toLocaleDateString('en-IN',{year:'numeric',month:'short',day:'numeric'})}
{r.recommend?'Recommends this product':'Does not recommend'}
onHelpful(r.id)} disabled={voted} style={{border:'none',background:'none',cursor:voted?'default':'pointer',color:voted?'var(--text-muted)':'var(--gold-deep)',fontSize:12,padding:0}}>Helpful ({r.helpfulCount||0})
);
}
function ReviewsSection({product}){
const [tick,setTick]=React.useState(0);
const [sort,setSort]=React.useState('newest');
const [verifiedOnly,setVerifiedOnly]=React.useState(false);
const [withImages,setWithImages]=React.useState(false);
const [showWrite,setShowWrite]=React.useState(false);
const user=typeof BB_AUTH!=='undefined'?BB_AUTH.currentUser():null;
const stats=ReviewStore.productStats(product.id);
const list=ReviewStore.forProduct(product.id,{sort,verifiedOnly,withImages});
const eligible=user?ReviewStore.canReview(user.id,user.email,product.id):null;
const myVoterKey=user?(user.id||user.email):null;
function helpful(id){ ReviewStore.voteHelpful(id,user&&user.id,user&&user.email); setTick(t=>t+1); }
return (
Customer Experience
Reviews
{eligible&&setShowWrite(true)} style={{border:'1px solid var(--gold-deep)',color:'var(--gold-deep)',background:'none',borderRadius:'var(--radius-md)',padding:'10px 20px',fontSize:13,letterSpacing:'var(--tracking-wide)',textTransform:'uppercase',cursor:'pointer'}}>Write a Review }
{stats.total>0?(
{stats.average}
{stats.total} review{stats.total===1?'':'s'}
{[5,4,3,2,1].map(s=>())}
):(
No reviews yet — be the first to share your experience.
)}
{stats.total>0&&(
setSort(e.target.value)} style={{padding:'8px 10px',border:'1px solid var(--border-default)',borderRadius:'var(--radius-sm)',fontSize:13}}>
Newest Highest Rating Lowest Rating Most Helpful
setVerifiedOnly(e.target.checked)}/> Verified Only
setWithImages(e.target.checked)}/> With Photos
)}
{list.map(r=>())}
{showWrite&&eligible&&
setShowWrite(false)} onSaved={()=>setTick(t=>t+1)}/>}
);
}
Object.assign(window,{Stars,RatingBar,MediaPicker,WriteReviewModal,ReviewCard,ReviewsSection});