234 أسطر
9.3 KiB
TypeScript
234 أسطر
9.3 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { KeywordSection } from "../components/KeywordSection";
|
|
import ContainerPage from '../components/shared/ContainerPage';
|
|
import HeaderPage from '../components/shared/HeaderPage';
|
|
import { useRecoilValue } from 'recoil';
|
|
import { siteAtom } from '../atoms/siteAtom';
|
|
|
|
interface ApiKeywordResponse {
|
|
domain: string;
|
|
results: {
|
|
bofu: string[];
|
|
mofu: string[];
|
|
tofu: string[];
|
|
};
|
|
}
|
|
|
|
interface GMBResponse {
|
|
has_gmb: boolean;
|
|
}
|
|
|
|
interface LLMMentionStatus {
|
|
status: "Mentioned" | "Not Mentioned" | "N/A" | "Loading" | "Error";
|
|
}
|
|
|
|
interface KeywordLLMStatus {
|
|
google: LLMMentionStatus;
|
|
gemini: LLMMentionStatus;
|
|
perplexity: LLMMentionStatus;
|
|
chatGPT: LLMMentionStatus;
|
|
}
|
|
|
|
const Keywords = () => {
|
|
const [keywordSections, setKeywordSections] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [hasGmb, setHasGmb] = useState<boolean>(false);
|
|
const [llmStatuses, setLlmStatuses] = useState<Record<string, KeywordLLMStatus>>({});
|
|
const [loadingKeywords, setLoadingKeywords] = useState<Record<string, boolean>>({});
|
|
const site = useRecoilValue(siteAtom);
|
|
|
|
console.log("site", site)
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
if (!site) {
|
|
throw new Error('No site domain set');
|
|
}
|
|
|
|
// Fetch GMB status
|
|
const gmbResponse = await fetch(`https://has-gmb-e7d107688dea.hosted.ghaymah.systems/has_gmb?domain=${encodeURIComponent(site)}`);
|
|
const gmbData: GMBResponse = await gmbResponse.json();
|
|
setHasGmb(gmbData.has_gmb);
|
|
|
|
// Fetch keywords
|
|
const keywordsResponse = await fetch(`https://keyword-funnel-api-abbc7b57dc18.hosted.ghaymah.systems/get_categorized_keywords?domain=${encodeURIComponent(site)}`);
|
|
const keywordsData: ApiKeywordResponse = await keywordsResponse.json();
|
|
|
|
// Transform API data to match expected format
|
|
const transformedSections = [
|
|
{
|
|
id: "bofu",
|
|
title: "Transactional, Immediate Business Impact, Highest Competition Opportunities (Bottom of Funnel - BoFu)",
|
|
subtitle: "High Conversion Intent. Immediate Revenue Drivers.",
|
|
expanded: true,
|
|
keywords: keywordsData.results.bofu.map((keyword: string, index: number) => ({
|
|
id: `bofu-${index}`,
|
|
keyword,
|
|
searchVolume: 0,
|
|
googlePos: 0,
|
|
mapsPos: null,
|
|
aiOverview: "invisible",
|
|
chatGPT: "invisible",
|
|
gemini: "invisible",
|
|
perplexity: "invisible",
|
|
competition: "medium",
|
|
promptExplorer: "coming-soon",
|
|
status: "Add-Keyword"
|
|
}))
|
|
},
|
|
{
|
|
id: "mofu",
|
|
title: "Consideration Opportunities (Middle of Funnel - MoFu)",
|
|
subtitle: "Research and Comparison Intent. Nurturing Leads.",
|
|
expanded: false,
|
|
keywords: keywordsData.results.mofu.map((keyword: string, index: number) => ({
|
|
id: `mofu-${index}`,
|
|
keyword,
|
|
searchVolume: 0,
|
|
googlePos: 0,
|
|
mapsPos: null,
|
|
aiOverview: "invisible",
|
|
chatGPT: "invisible",
|
|
gemini: "invisible",
|
|
perplexity: "invisible",
|
|
competition: "medium",
|
|
promptExplorer: "coming-soon",
|
|
status: "Add-Keyword"
|
|
}))
|
|
},
|
|
{
|
|
id: "tofu",
|
|
title: "Educational Opportunities (Top of Funnel - ToFu)",
|
|
subtitle: "Informational Intent. Essential for building E-E-A-T and AI Trust.",
|
|
expanded: false,
|
|
keywords: keywordsData.results.tofu.map((keyword: string, index: number) => ({
|
|
id: `tofu-${index}`,
|
|
keyword,
|
|
searchVolume: 0,
|
|
googlePos: 0,
|
|
mapsPos: null,
|
|
aiOverview: "invisible",
|
|
chatGPT: "invisible",
|
|
gemini: "invisible",
|
|
perplexity: "invisible",
|
|
competition: "medium",
|
|
promptExplorer: "coming-soon",
|
|
status: "Add-Keyword"
|
|
}))
|
|
}
|
|
];
|
|
|
|
setKeywordSections(transformedSections);
|
|
setLoading(false);
|
|
|
|
// Initialize LLM statuses and loading states
|
|
const initialStatuses: Record<string, KeywordLLMStatus> = {};
|
|
const initialLoading: Record<string, boolean> = {};
|
|
|
|
transformedSections.forEach(section => {
|
|
section.keywords.forEach(keyword => {
|
|
initialStatuses[keyword.id] = {
|
|
google: { status: "Loading" },
|
|
gemini: { status: "Loading" },
|
|
perplexity: { status: "Loading" },
|
|
chatGPT: { status: "Loading" }
|
|
};
|
|
initialLoading[keyword.id] = true;
|
|
});
|
|
});
|
|
|
|
setLlmStatuses(initialStatuses);
|
|
setLoadingKeywords(initialLoading);
|
|
|
|
// Process keywords sequentially
|
|
for (const section of transformedSections) {
|
|
for (const keyword of section.keywords) {
|
|
try {
|
|
const response = await fetch(
|
|
`https://llm-mention-api-2407b03f0ba9.hosted.ghaymah.systems/api/check-mention?keyword=${encodeURIComponent(keyword.keyword)}&url=${encodeURIComponent(site)}&llms=google,gemini,perplexity,chatgpt`
|
|
);
|
|
const data = await response.json();
|
|
|
|
setLlmStatuses(prev => ({
|
|
...prev,
|
|
[keyword.id]: {
|
|
google: { status: data.results.google?.status || "N/A" },
|
|
gemini: { status: data.results.gemini?.status || "N/A" },
|
|
perplexity: { status: data.results.perplexity?.status || "N/A" },
|
|
chatGPT: { status: data.results.chatGPT?.status || "N/A" }
|
|
}
|
|
}));
|
|
} catch (err) {
|
|
setLlmStatuses(prev => ({
|
|
...prev,
|
|
[keyword.id]: {
|
|
google: { status: "Error" },
|
|
gemini: { status: "Error" },
|
|
perplexity: { status: "Error" },
|
|
chatGPT: { status: "Error" }
|
|
}
|
|
}));
|
|
} finally {
|
|
setLoadingKeywords(prev => ({
|
|
...prev,
|
|
[keyword.id]: false
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to fetch data');
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<ContainerPage>
|
|
<HeaderPage title="Keyword Visibility Matrix" buttonShow={true} />
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
|
</div>
|
|
</ContainerPage>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<ContainerPage>
|
|
<HeaderPage title="Keyword Visibility Matrix" buttonShow={true} />
|
|
<div className="flex justify-center items-center h-64 text-red-500">
|
|
{error}
|
|
</div>
|
|
</ContainerPage>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ContainerPage>
|
|
<HeaderPage title="Keyword Visibility Matrix" buttonShow={true} />
|
|
|
|
<div className="flex flex-col">
|
|
{keywordSections.map((section) => (
|
|
<KeywordSection
|
|
key={section.id}
|
|
section={section}
|
|
hasGmb={hasGmb}
|
|
llmStatuses={llmStatuses}
|
|
loadingKeywords={loadingKeywords}
|
|
/>
|
|
))}
|
|
</div>
|
|
</ContainerPage>
|
|
)
|
|
}
|
|
|
|
export default Keywords
|