working table
هذا الالتزام موجود في:
@@ -6,11 +6,12 @@ import { ActionBadge } from "./shared/ActionBadge";
|
|||||||
|
|
||||||
interface KeywordRowProps {
|
interface KeywordRowProps {
|
||||||
keyword: KeywordRowType;
|
keyword: KeywordRowType;
|
||||||
|
hasGmb: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeywordRow({ keyword }: KeywordRowProps) {
|
export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
|
||||||
return (
|
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" >
|
<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">
|
<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} />
|
<MetricBadge type="google" value={keyword.googlePos} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{hasGmb && (
|
||||||
{keyword.mapsPos && <MetricBadge type="maps" value={keyword.mapsPos} />}
|
<div>
|
||||||
</div>
|
{keyword.mapsPos && <MetricBadge type="maps" value={keyword.mapsPos} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<VisibilityIndicator platform="aiOverview" visible={keyword.aiOverview === "visible"} />
|
<VisibilityIndicator platform="aiOverview" visible={keyword.aiOverview === "visible"} />
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import MetricTooltip from "./shared/MetricTooltip";
|
|||||||
|
|
||||||
interface KeywordSectionProps {
|
interface KeywordSectionProps {
|
||||||
section: KeywordSectionType;
|
section: KeywordSectionType;
|
||||||
|
hasGmb: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeywordSection({ section }: KeywordSectionProps) {
|
export function KeywordSection({ section, hasGmb }: KeywordSectionProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(section.expanded);
|
const [isExpanded, setIsExpanded] = useState(section.expanded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,9 +47,11 @@ export function KeywordSection({ section }: KeywordSectionProps) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-xs font-semibold text-grey-800 leading-[18px]">G Pos #</span>
|
<span className="text-xs font-semibold text-grey-800 leading-[18px]">G Pos #</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{hasGmb && (
|
||||||
<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]">Maps</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs font-semibold text-grey-800 leading-[18px]">AI Overview</span>
|
<span className="text-xs font-semibold text-grey-800 leading-[18px]">AI Overview</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +78,7 @@ export function KeywordSection({ section }: KeywordSectionProps) {
|
|||||||
|
|
||||||
<div className="bg-grey-50">
|
<div className="bg-grey-50">
|
||||||
{section.keywords.map((keyword) => (
|
{section.keywords.map((keyword) => (
|
||||||
<KeywordRow key={keyword.id} keyword={keyword} />
|
<KeywordRow key={keyword.id} keyword={keyword} hasGmb={hasGmb} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,149 @@
|
|||||||
import React from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { keywordSections } from "../assets/keyword-data";
|
|
||||||
import { KeywordSection } from "../components/KeywordSection";
|
import { KeywordSection } from "../components/KeywordSection";
|
||||||
import ContainerPage from '../components/shared/ContainerPage';
|
import ContainerPage from '../components/shared/ContainerPage';
|
||||||
import HeaderPage from '../components/shared/HeaderPage';
|
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 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 (
|
return (
|
||||||
<ContainerPage>
|
<ContainerPage>
|
||||||
<HeaderPage title="Keyword Visibility Matrix" buttonShow={true} />
|
<HeaderPage title="Keyword Visibility Matrix" buttonShow={true} />
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{keywordSections.map((section) => (
|
{keywordSections.map((section) => (
|
||||||
<KeywordSection key={section.id} section={section} />
|
<KeywordSection key={section.id} section={section} hasGmb={hasGmb} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ContainerPage>
|
</ContainerPage>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Keywords
|
export default Keywords
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم