working table
هذا الالتزام موجود في:
@@ -6,11 +6,12 @@ import { ActionBadge } from "./shared/ActionBadge";
|
||||
|
||||
interface KeywordRowProps {
|
||||
keyword: KeywordRowType;
|
||||
hasGmb: boolean;
|
||||
}
|
||||
|
||||
export function KeywordRow({ keyword }: KeywordRowProps) {
|
||||
export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 justify-items-cente px-6 pr-[52px] py-3 border-t border-grey-300 min-h-[54px] hover:bg-gray-100 transition-colors">
|
||||
<div className={`grid ${hasGmb ? 'grid-cols-12' : 'grid-cols-11'} gap-4 justify-items-cente px-6 pr-[52px] py-3 border-t border-grey-300 min-h-[54px] hover:bg-gray-100 transition-colors`}>
|
||||
|
||||
<div className="flex items-center gap-1 mr-20 col-span-2" >
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className="shrink-0">
|
||||
@@ -27,9 +28,11 @@ export function KeywordRow({ keyword }: KeywordRowProps) {
|
||||
<MetricBadge type="google" value={keyword.googlePos} />
|
||||
</div>
|
||||
|
||||
{hasGmb && (
|
||||
<div>
|
||||
{keyword.mapsPos && <MetricBadge type="maps" value={keyword.mapsPos} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<VisibilityIndicator platform="aiOverview" visible={keyword.aiOverview === "visible"} />
|
||||
|
||||
@@ -6,9 +6,10 @@ import MetricTooltip from "./shared/MetricTooltip";
|
||||
|
||||
interface KeywordSectionProps {
|
||||
section: KeywordSectionType;
|
||||
hasGmb: boolean;
|
||||
}
|
||||
|
||||
export function KeywordSection({ section }: KeywordSectionProps) {
|
||||
export function KeywordSection({ section, hasGmb }: KeywordSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(section.expanded);
|
||||
|
||||
return (
|
||||
@@ -46,9 +47,11 @@ export function KeywordSection({ section }: KeywordSectionProps) {
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-grey-800 leading-[18px]">G Pos #</span>
|
||||
</div>
|
||||
{hasGmb && (
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-grey-800 leading-[18px]">Maps</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-grey-800 leading-[18px]">AI Overview</span>
|
||||
</div>
|
||||
@@ -75,7 +78,7 @@ export function KeywordSection({ section }: KeywordSectionProps) {
|
||||
|
||||
<div className="bg-grey-50">
|
||||
{section.keywords.map((keyword) => (
|
||||
<KeywordRow key={keyword.id} keyword={keyword} />
|
||||
<KeywordRow key={keyword.id} keyword={keyword} hasGmb={hasGmb} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,18 +1,145 @@
|
||||
import React from 'react'
|
||||
import { keywordSections } from "../assets/keyword-data";
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { KeywordSection } from "../components/KeywordSection";
|
||||
import ContainerPage from '../components/shared/ContainerPage';
|
||||
import HeaderPage from '../components/shared/HeaderPage';
|
||||
|
||||
interface ApiKeywordResponse {
|
||||
domain: string;
|
||||
results: {
|
||||
bofu: string[];
|
||||
mofu: string[];
|
||||
tofu: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface GMBResponse {
|
||||
has_gmb: boolean;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch GMB status
|
||||
const gmbResponse = await fetch('https://has-gmb-e7d107688dea.hosted.ghaymah.systems/has_gmb?domain=https://www.rjmedspa.com');
|
||||
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=https://www.rjmedspa.com/');
|
||||
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);
|
||||
} 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} />
|
||||
<KeywordSection key={section.id} section={section} hasGmb={hasGmb} />
|
||||
))}
|
||||
</div>
|
||||
</ContainerPage>
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم