| 1 |
import React, { useEffect, useState } from "react"; |
| 2 |
import { __ } from "@wordpress/i18n"; |
| 3 |
import apiFetch from "@wordpress/api-fetch"; |
| 4 |
import { useFAQ } from "../../components/FAQContext"; |
| 5 |
import { useFAQScope } from "../context/ScopeContext"; |
| 6 |
|
| 7 |
/** |
| 8 |
* FAQ Builder summary stat cards (FAQ Groups / Total FAQs / Published / Drafts). |
| 9 |
* |
| 10 |
* Design-only addition: the counts are derived from data the app already has — |
| 11 |
* `groupsTotal` is the FAQ-category total, and the FAQ publish/draft split comes |
| 12 |
* from the existing `docs-faq-count` REST endpoint (no new functionality). |
| 13 |
*/ |
| 14 |
|
| 15 |
// Stroke icons matching the standalone design mockup. |
| 16 |
const Icon = ({ size = 20, children }) => ( |
| 17 |
<svg |
| 18 |
width={size} |
| 19 |
height={size} |
| 20 |
viewBox="0 0 24 24" |
| 21 |
fill="none" |
| 22 |
stroke="currentColor" |
| 23 |
strokeWidth={1.7} |
| 24 |
strokeLinecap="round" |
| 25 |
strokeLinejoin="round" |
| 26 |
> |
| 27 |
{children} |
| 28 |
</svg> |
| 29 |
); |
| 30 |
|
| 31 |
const IconBook = () => ( |
| 32 |
<Icon> |
| 33 |
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5v14zM4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5" /> |
| 34 |
</Icon> |
| 35 |
); |
| 36 |
const IconFAQ = () => ( |
| 37 |
<Icon> |
| 38 |
<path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.7A8.5 8.5 0 1 1 21 11.5z" /> |
| 39 |
<path d="M9.5 9a2.5 2.5 0 0 1 5 .25c0 1.5-2.5 2-2.5 3.75M12 16.5h.01" /> |
| 40 |
</Icon> |
| 41 |
); |
| 42 |
const IconCheck = () => ( |
| 43 |
<Icon> |
| 44 |
<path d="M5 12l5 5L20 7" /> |
| 45 |
</Icon> |
| 46 |
); |
| 47 |
const IconEdit = () => ( |
| 48 |
<Icon> |
| 49 |
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" /> |
| 50 |
</Icon> |
| 51 |
); |
| 52 |
|
| 53 |
// Soft blurred-triangle glow in the top-left corner — the exact SVG the dashboard |
| 54 |
// overview cards (`.betterdocs-card-wrapper.created-docs`) use, with this card's |
| 55 |
// tint injected as the gradient start color so the FAQ cards share that design |
| 56 |
// system (132×88, opacity 0.2, positioned `left top`). |
| 57 |
const glowSvg = (tint) => |
| 58 |
`url("data:image/svg+xml,%3Csvg width='132' height='88' viewBox='0 0 132 88' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg opacity='0.2' filter='url(%23bdFaqBlur)'%3E%3Cpath d='M96 -3.50001L-0.750021 52.3586L-0.750016 -59.3586L96 -3.50001Z' fill='url(%23bdFaqGrad)'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='bdFaqBlur' x='-35.9468' y='-94.5562' width='167.144' height='182.112' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeFlood flood-opacity='0' result='BackgroundImageFix'/%3E%3CfeBlend mode='normal' in='SourceGraphic' in2='BackgroundImageFix' result='shape'/%3E%3CfeGaussianBlur stdDeviation='17.5984' result='effect1_foregroundBlur'/%3E%3C/filter%3E%3ClinearGradient id='bdFaqGrad' x1='96' y1='-3.50001' x2='-33' y2='-3.50001' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='${encodeURIComponent( |
| 59 |
tint |
| 60 |
)}'/%3E%3Cstop offset='1' stop-color='%23FFE600' stop-opacity='0'/%3E%3C/linearGradient%3E%3C/defs%3E%3C/svg%3E")`; |
| 61 |
|
| 62 |
// "#00B884" -> "0, 184, 132" for rgba(var(--rgb), a) backgrounds. |
| 63 |
const hexToRgb = (hex) => { |
| 64 |
const h = String(hex).replace("#", ""); |
| 65 |
return `${parseInt(h.slice(0, 2), 16)}, ${parseInt(h.slice(2, 4), 16)}, ${parseInt(h.slice(4, 6), 16)}`; |
| 66 |
}; |
| 67 |
|
| 68 |
const StatCard = ({ value, label, tint, icon }) => ( |
| 69 |
<div |
| 70 |
className="bd-faq-stat-card" |
| 71 |
style={{ backgroundImage: glowSvg(tint) }} |
| 72 |
> |
| 73 |
<div className="bd-faq-stat-card__body"> |
| 74 |
<h3 className="bd-faq-stat-card__value">{value}</h3> |
| 75 |
<p className="bd-faq-stat-card__label">{label}</p> |
| 76 |
</div> |
| 77 |
<span |
| 78 |
className="bd-faq-stat-card__icon" |
| 79 |
style={{ "--bd-icon-tint": tint, "--bd-icon-tint-rgb": hexToRgb(tint) }} |
| 80 |
> |
| 81 |
{icon} |
| 82 |
</span> |
| 83 |
</div> |
| 84 |
); |
| 85 |
|
| 86 |
const FaqStats = ({ groupsTotal = 0, refreshSignal }) => { |
| 87 |
const [counts, setCounts] = useState({}); |
| 88 |
const { statsVersion } = useFAQ(); |
| 89 |
const { isProduct } = useFAQScope(); |
| 90 |
|
| 91 |
// Re-derive whenever the FAQ list changes (add / delete / draft toggle all |
| 92 |
// run the builder's category refetch, which updates `refreshSignal`), so the |
| 93 |
// stat cards stay live with the rest of the page. |
| 94 |
useEffect(() => { |
| 95 |
let active = true; |
| 96 |
apiFetch({ path: "/betterdocs/v1/docs-faq-count" }) |
| 97 |
.then((res) => { |
| 98 |
if (active && res) { |
| 99 |
setCounts(res); |
| 100 |
} |
| 101 |
}) |
| 102 |
.catch(() => {}); |
| 103 |
return () => { |
| 104 |
active = false; |
| 105 |
}; |
| 106 |
}, [refreshSignal, statsVersion]); |
| 107 |
|
| 108 |
const createdKey = isProduct ? "created_product_faq" : "created_faq"; |
| 109 |
const publishedKey = isProduct ? "published_product_faq" : "published_faq"; |
| 110 |
const total = parseInt(counts[createdKey], 10) || 0; |
| 111 |
const published = parseInt(counts[publishedKey], 10) || 0; |
| 112 |
const drafts = Math.max(total - published, 0); |
| 113 |
// Real FAQ groups only. "Uncategorized" is not a group — it's the bucket for FAQs |
| 114 |
// that have none — so counting it made the card disagree with the group list (e.g. |
| 115 |
// "3 Groups" next to 2 groups + Uncategorized). The FAQs inside it are still counted |
| 116 |
// by the Total / Published / Drafts cards. |
| 117 |
const groups = parseInt(groupsTotal, 10) || 0; |
| 118 |
|
| 119 |
return ( |
| 120 |
<div className="bd-faq-stats"> |
| 121 |
<StatCard |
| 122 |
value={groups} |
| 123 |
label={__( |
| 124 |
isProduct ? "Product FAQ Groups" : "General FAQ Groups", |
| 125 |
"betterdocs" |
| 126 |
)} |
| 127 |
tint="#00B884" |
| 128 |
icon={<IconBook />} |
| 129 |
/> |
| 130 |
<StatCard |
| 131 |
value={total} |
| 132 |
label={__("Total FAQs", "betterdocs")} |
| 133 |
tint="#F59E0B" |
| 134 |
icon={<IconFAQ />} |
| 135 |
/> |
| 136 |
<StatCard |
| 137 |
value={published} |
| 138 |
label={__("Published", "betterdocs")} |
| 139 |
tint="#3B82F6" |
| 140 |
icon={<IconCheck />} |
| 141 |
/> |
| 142 |
<StatCard |
| 143 |
value={drafts} |
| 144 |
label={__("Drafts", "betterdocs")} |
| 145 |
tint="#8B5CF6" |
| 146 |
icon={<IconEdit />} |
| 147 |
/> |
| 148 |
</div> |
| 149 |
); |
| 150 |
}; |
| 151 |
|
| 152 |
export default FaqStats; |
| 153 |
|