Initial commit: Meshkee dashboards monorepo.

Includes business, customer, and super-admin apps with shared packages and production deploy scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-07-22 13:48:53 +03:30
co-authored by Cursor
commit f566387c61
509 changed files with 62690 additions and 0 deletions
@@ -0,0 +1,446 @@
import { useEffect, useState } from 'react'
import { Check, Star, ThumbsUp, Trash2, XCircle } from 'lucide-react'
import { formatCommentDate } from '../data/productComments'
import { ApiError } from '../lib/api'
import {
deleteComment,
listProductComments,
updateCommentApproval,
} from '../services/commentService'
import {
deleteExpertReview,
formatReviewDate,
listProductExpertReviews,
updateExpertReviewApproval,
} from '../services/expertReviewService'
import { getProductTechnicalInfo } from '../services/productTechnicalInfoService'
import type { ProductComment } from '../types/comment'
import type { ProductExpertReview } from '../types/expertReview'
import type { ProductTechnicalValue } from '../types/technicalForm'
import styles from './ProductDetailsTabs.module.css'
type DetailTab = 'technical' | 'comments' | 'reviews'
interface ProductDetailsTabsProps {
productId: string
commentCount: number
}
function formatTechnicalValue(item: ProductTechnicalValue): string {
if (item.type === 'multi_select') {
const labels = item.optionLabels ?? (Array.isArray(item.value) ? item.value : [])
return labels.length ? labels.join(', ') : '—'
}
if (item.type === 'select') {
return item.optionLabel ?? (item.value ? String(item.value) : '—')
}
return item.value ? String(item.value) : '—'
}
export function ProductDetailsTabs({ productId, commentCount }: ProductDetailsTabsProps) {
const [activeTab, setActiveTab] = useState<DetailTab>('technical')
const [technicalValues, setTechnicalValues] = useState<ProductTechnicalValue[]>([])
const [technicalMessage, setTechnicalMessage] = useState('')
const [technicalLoading, setTechnicalLoading] = useState(false)
const [technicalError, setTechnicalError] = useState('')
const [comments, setComments] = useState<ProductComment[]>([])
const [commentsLoading, setCommentsLoading] = useState(false)
const [commentsError, setCommentsError] = useState('')
const [reviews, setReviews] = useState<ProductExpertReview[]>([])
const [reviewsLoading, setReviewsLoading] = useState(false)
const [reviewsError, setReviewsError] = useState('')
const [actionId, setActionId] = useState<string | null>(null)
useEffect(() => {
if (activeTab !== 'technical') return
const controller = new AbortController()
void loadTechnicalInfo(controller.signal)
return () => controller.abort()
}, [activeTab, productId])
useEffect(() => {
if (activeTab !== 'comments') return
const controller = new AbortController()
void loadComments(controller.signal)
return () => controller.abort()
}, [activeTab, productId])
useEffect(() => {
if (activeTab !== 'reviews') return
const controller = new AbortController()
void loadReviews(controller.signal)
return () => controller.abort()
}, [activeTab, productId])
async function loadComments(signal?: AbortSignal) {
setCommentsLoading(true)
setCommentsError('')
try {
const data = await listProductComments(productId, 1, 100, signal)
setComments(data.items)
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
setCommentsError(err.message)
} else {
setCommentsError('Unable to load comments.')
}
} finally {
setCommentsLoading(false)
}
}
async function loadReviews(signal?: AbortSignal) {
setReviewsLoading(true)
setReviewsError('')
try {
const data = await listProductExpertReviews(productId, 1, 100, signal)
setReviews(data.items)
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
setReviewsError(err.message)
} else {
setReviewsError('Unable to load expert reviews.')
}
} finally {
setReviewsLoading(false)
}
}
async function loadTechnicalInfo(signal?: AbortSignal) {
setTechnicalLoading(true)
setTechnicalError('')
setTechnicalMessage('')
try {
const data = await getProductTechnicalInfo(productId, signal)
setTechnicalValues(data.values)
if (data.message) {
setTechnicalMessage(data.message)
} else if (!data.form) {
setTechnicalMessage('No technical form is defined for this product category.')
} else if (!data.values.some((item) => formatTechnicalValue(item) !== '—')) {
setTechnicalMessage('No technical data has been added for this product yet.')
}
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
setTechnicalError(err.message)
} else {
setTechnicalError('Unable to load technical info.')
}
} finally {
setTechnicalLoading(false)
}
}
async function toggleCommentApproval(comment: ProductComment) {
setActionId(comment.id)
setCommentsError('')
try {
const updated = await updateCommentApproval(comment.id, !comment.approved)
setComments((prev) => prev.map((item) => (item.id === comment.id ? updated : item)))
} catch (err) {
if (err instanceof ApiError) {
setCommentsError(err.message)
} else {
setCommentsError('Unable to update comment approval.')
}
} finally {
setActionId(null)
}
}
async function removeComment(commentId: string) {
setActionId(commentId)
setCommentsError('')
try {
await deleteComment(commentId)
setComments((prev) => prev.filter((item) => item.id !== commentId))
} catch (err) {
if (err instanceof ApiError) {
setCommentsError(err.message)
} else {
setCommentsError('Unable to delete comment.')
}
} finally {
setActionId(null)
}
}
async function toggleReviewApproval(review: ProductExpertReview) {
setActionId(review.id)
setReviewsError('')
try {
const updated = await updateExpertReviewApproval(review.id, !review.approved)
setReviews((prev) => prev.map((item) => (item.id === review.id ? updated : item)))
} catch (err) {
if (err instanceof ApiError) {
setReviewsError(err.message)
} else {
setReviewsError('Unable to update expert review approval.')
}
} finally {
setActionId(null)
}
}
async function removeReview(reviewId: string) {
setActionId(reviewId)
setReviewsError('')
try {
await deleteExpertReview(reviewId)
setReviews((prev) => prev.filter((item) => item.id !== reviewId))
} catch (err) {
if (err instanceof ApiError) {
setReviewsError(err.message)
} else {
setReviewsError('Unable to delete expert review.')
}
} finally {
setActionId(null)
}
}
const displayedCommentCount = comments.length > 0 ? comments.length : commentCount
return (
<section className={styles.tabsSection}>
<div className={styles.tabList} role="tablist" aria-label="Product details">
<button
type="button"
role="tab"
aria-selected={activeTab === 'technical'}
className={`${styles.tab} ${activeTab === 'technical' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('technical')}
>
Technical Info
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'comments'}
className={`${styles.tab} ${activeTab === 'comments' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('comments')}
>
Comments
{displayedCommentCount > 0 && (
<span className={styles.tabBadge}>{displayedCommentCount}</span>
)}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'reviews'}
className={`${styles.tab} ${activeTab === 'reviews' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('reviews')}
>
Expert Reviews
{reviews.length > 0 && (
<span className={styles.tabBadge}>{reviews.length}</span>
)}
</button>
</div>
<div className={styles.tabPanel} role="tabpanel">
{activeTab === 'technical' && (
<>
{technicalLoading ? (
<p className={styles.emptyText}>Loading technical info...</p>
) : technicalError ? (
<p className={styles.errorText}>{technicalError}</p>
) : technicalMessage && technicalValues.length === 0 ? (
<p className={styles.emptyText}>{technicalMessage}</p>
) : technicalValues.length === 0 ? (
<p className={styles.emptyText}>No technical data available.</p>
) : (
<dl className={styles.techList}>
{technicalValues.map((item) => (
<div key={item.fieldKey} className={styles.techRow}>
<dt>{item.fieldLabel}</dt>
<dd>{formatTechnicalValue(item)}</dd>
</div>
))}
</dl>
)}
{technicalMessage && technicalValues.length > 0 && (
<p className={styles.hintText}>{technicalMessage}</p>
)}
</>
)}
{activeTab === 'comments' && (
<>
{commentsError && <p className={styles.errorText}>{commentsError}</p>}
{commentsLoading ? (
<p className={styles.emptyText}>Loading comments...</p>
) : comments.length === 0 ? (
<p className={styles.emptyText}>No comments yet for this product.</p>
) : (
<div className={styles.commentList}>
{comments.map((comment) => (
<article
key={comment.id}
className={`${styles.comment} ${comment.approved ? styles.commentApproved : ''}`}
>
<div className={styles.commentHeader}>
<div>
<div className={styles.author}>{comment.author}</div>
<div className={styles.meta}>{formatCommentDate(comment.createdAt)}</div>
</div>
<span className={styles.likes}>
<ThumbsUp size={13} />
{comment.likesCount}
</span>
</div>
<p className={styles.commentText}>{comment.text}</p>
{!comment.approved && (
<span className={styles.pendingBadge}>Pending approval</span>
)}
<div className={styles.itemActions}>
{comment.approved ? (
<button
type="button"
className={styles.rejectBtn}
disabled={actionId === comment.id}
onClick={() => void toggleCommentApproval(comment)}
>
<XCircle size={14} />
Reject
</button>
) : (
<button
type="button"
className={styles.approveBtn}
disabled={actionId === comment.id}
onClick={() => void toggleCommentApproval(comment)}
>
<Check size={14} />
Approve
</button>
)}
<button
type="button"
className={styles.removeBtn}
disabled={actionId === comment.id}
onClick={() => void removeComment(comment.id)}
>
<Trash2 size={14} />
Remove
</button>
</div>
</article>
))}
</div>
)}
</>
)}
{activeTab === 'reviews' && (
<>
{reviewsError && <p className={styles.errorText}>{reviewsError}</p>}
{reviewsLoading ? (
<p className={styles.emptyText}>Loading expert reviews...</p>
) : reviews.length === 0 ? (
<p className={styles.emptyText}>No expert reviews yet.</p>
) : (
<div className={styles.reviewList}>
{reviews.map((review) => (
<article
key={review.id}
className={`${styles.review} ${review.approved ? styles.reviewApproved : ''}`}
>
<div className={styles.reviewHeader}>
<div>
<div className={styles.reviewTitle}>{review.authorName}</div>
<div className={styles.meta}>{formatReviewDate(review.createdAt)}</div>
</div>
<div className={styles.rating} aria-label={`Rating ${review.rate} out of 10`}>
<Star size={14} fill="currentColor" />
{review.rate}/10
</div>
</div>
{(review.positivePoints.length > 0 || review.negativePoints.length > 0) && (
<div className={styles.pointsGrid}>
{review.positivePoints.length > 0 && (
<div className={styles.pointsBlock}>
<h4 className={styles.pointsHeading}>Positive points</h4>
<ul className={styles.pointsList}>
{review.positivePoints.map((point) => (
<li key={point}>{point}</li>
))}
</ul>
</div>
)}
{review.negativePoints.length > 0 && (
<div className={styles.pointsBlock}>
<h4 className={styles.pointsHeading}>Negative points</h4>
<ul className={`${styles.pointsList} ${styles.pointsListNegative}`}>
{review.negativePoints.map((point) => (
<li key={point}>{point}</li>
))}
</ul>
</div>
)}
</div>
)}
<p className={styles.reviewSummary}>{review.text}</p>
{!review.approved && (
<span className={styles.pendingBadge}>Pending approval</span>
)}
<div className={styles.itemActions}>
{review.approved ? (
<button
type="button"
className={styles.rejectBtn}
disabled={actionId === review.id}
onClick={() => void toggleReviewApproval(review)}
>
<XCircle size={14} />
Reject
</button>
) : (
<button
type="button"
className={styles.approveBtn}
disabled={actionId === review.id}
onClick={() => void toggleReviewApproval(review)}
>
<Check size={14} />
Approve
</button>
)}
<button
type="button"
className={styles.removeBtn}
disabled={actionId === review.id}
onClick={() => void removeReview(review.id)}
>
<Trash2 size={14} />
Remove
</button>
</div>
</article>
))}
</div>
)}
</>
)}
</div>
</section>
)
}