Adding atom
هذا الالتزام موجود في:
27
package-lock.json
مولّد
27
package-lock.json
مولّد
@@ -18,7 +18,8 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.9.1",
|
"react-router-dom": "^7.9.1",
|
||||||
"recharts": "^3.2.1"
|
"recharts": "^3.2.1",
|
||||||
|
"recoil": "^0.7.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
@@ -3329,6 +3330,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hamt_plus": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -4395,6 +4401,25 @@
|
|||||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recoil": {
|
||||||
|
"version": "0.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
|
||||||
|
"integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"hamt_plus": "1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.13.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.9.1",
|
"react-router-dom": "^7.9.1",
|
||||||
"recharts": "^3.2.1"
|
"recharts": "^3.2.1",
|
||||||
|
"recoil": "^0.7.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
|||||||
6
src/atoms/siteAtom.ts
Normal file
6
src/atoms/siteAtom.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const siteAtom = atom<string>({
|
||||||
|
key: 'siteAtom',
|
||||||
|
default: ''
|
||||||
|
});
|
||||||
@@ -7,9 +7,16 @@ import { ActionBadge } from "./shared/ActionBadge";
|
|||||||
interface KeywordRowProps {
|
interface KeywordRowProps {
|
||||||
keyword: KeywordRowType;
|
keyword: KeywordRowType;
|
||||||
hasGmb: boolean;
|
hasGmb: boolean;
|
||||||
|
llmStatus: {
|
||||||
|
google: { status: string };
|
||||||
|
gemini: { status: string };
|
||||||
|
perplexity: { status: string };
|
||||||
|
chatGPT: { status: string };
|
||||||
|
};
|
||||||
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
|
export function KeywordRow({ keyword, hasGmb, llmStatus, isLoading }: KeywordRowProps) {
|
||||||
return (
|
return (
|
||||||
<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={`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`}>
|
||||||
|
|
||||||
@@ -25,7 +32,17 @@ export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MetricBadge type="google" value={keyword.googlePos} />
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse h-4 w-4 rounded-full bg-gray-200"></div>
|
||||||
|
) : (
|
||||||
|
<div className={`text-xs font-medium ${
|
||||||
|
llmStatus.google.status === "Mentioned" ? "text-green-500" :
|
||||||
|
llmStatus.google.status === "Not Mentioned" ? "text-red-500" :
|
||||||
|
"text-gray-500"
|
||||||
|
}`}>
|
||||||
|
{llmStatus.google.status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasGmb && (
|
{hasGmb && (
|
||||||
@@ -39,15 +56,45 @@ export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<VisibilityIndicator platform="chatGPT" visible={keyword.chatGPT === "visible"} />
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse h-4 w-4 rounded-full bg-gray-200"></div>
|
||||||
|
) : (
|
||||||
|
<div className={`text-xs font-medium ${
|
||||||
|
llmStatus.chatGPT.status === "Mentioned" ? "text-green-500" :
|
||||||
|
llmStatus.chatGPT.status === "Not Mentioned" ? "text-red-500" :
|
||||||
|
"text-gray-500"
|
||||||
|
}`}>
|
||||||
|
{llmStatus.chatGPT.status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<VisibilityIndicator platform="gemini" visible={keyword.gemini === "visible"} />
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse h-4 w-4 rounded-full bg-gray-200"></div>
|
||||||
|
) : (
|
||||||
|
<div className={`text-xs font-medium ${
|
||||||
|
llmStatus.gemini.status === "Mentioned" ? "text-green-500" :
|
||||||
|
llmStatus.gemini.status === "Not Mentioned" ? "text-red-500" :
|
||||||
|
"text-gray-500"
|
||||||
|
}`}>
|
||||||
|
{llmStatus.gemini.status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<VisibilityIndicator platform="perplexity" visible={keyword.perplexity === "visible"} />
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse h-4 w-4 rounded-full bg-gray-200"></div>
|
||||||
|
) : (
|
||||||
|
<div className={`text-xs font-medium ${
|
||||||
|
llmStatus.perplexity.status === "Mentioned" ? "text-green-500" :
|
||||||
|
llmStatus.perplexity.status === "Not Mentioned" ? "text-red-500" :
|
||||||
|
"text-gray-500"
|
||||||
|
}`}>
|
||||||
|
{llmStatus.perplexity.status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ import MetricTooltip from "./shared/MetricTooltip";
|
|||||||
interface KeywordSectionProps {
|
interface KeywordSectionProps {
|
||||||
section: KeywordSectionType;
|
section: KeywordSectionType;
|
||||||
hasGmb: boolean;
|
hasGmb: boolean;
|
||||||
|
llmStatuses: Record<string, {
|
||||||
|
google: { status: string };
|
||||||
|
gemini: { status: string };
|
||||||
|
perplexity: { status: string };
|
||||||
|
chatGPT: { status: string };
|
||||||
|
}>;
|
||||||
|
loadingKeywords: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeywordSection({ section, hasGmb }: KeywordSectionProps) {
|
export function KeywordSection({ section, hasGmb, llmStatuses, loadingKeywords }: KeywordSectionProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(section.expanded);
|
const [isExpanded, setIsExpanded] = useState(section.expanded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,15 +85,21 @@ export function KeywordSection({ section, hasGmb }: 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} hasGmb={hasGmb} />
|
<KeywordRow
|
||||||
|
key={keyword.id}
|
||||||
|
keyword={keyword}
|
||||||
|
hasGmb={hasGmb}
|
||||||
|
llmStatus={llmStatuses[keyword.id]}
|
||||||
|
isLoading={loadingKeywords[keyword.id]}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 py-3.5 px-2 bg-[#FAFAFC]">
|
{/* <div className="flex items-center justify-center gap-2 py-3.5 px-2 bg-[#FAFAFC]">
|
||||||
<button className="text-sm font-medium text-[#4C60E5] underline leading-[17px] hover:text-primary/80 transition-colors">
|
<button className="text-sm font-medium text-[#4C60E5] underline leading-[17px] hover:text-primary/80 transition-colors">
|
||||||
Load 10 More Keywords
|
Load 10 More Keywords
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const Header = () => {
|
|||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
|
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
|
||||||
const isHeaderVisible = location.pathname !== '/';
|
const isHeaderVisible = location.pathname !== '/';
|
||||||
|
const isLoggedIn = localStorage.getItem('sp_user') !== null;
|
||||||
|
|
||||||
const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
|
const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
|
||||||
const toggleMobileNav = () => setIsMobileNavOpen(!isMobileNavOpen);
|
const toggleMobileNav = () => setIsMobileNavOpen(!isMobileNavOpen);
|
||||||
@@ -123,16 +124,25 @@ const Header = () => {
|
|||||||
|
|
||||||
{/* Right Side - Desktop Only */}
|
{/* Right Side - Desktop Only */}
|
||||||
<div className="hidden md:flex items-center space-x-4 relative">
|
<div className="hidden md:flex items-center space-x-4 relative">
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<>
|
||||||
{/* Add Keyword Button */}
|
{/* Add Keyword Button */}
|
||||||
|
|
||||||
<button className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-xl font-medium shadow-md hover:bg-blue-700 transition-colors duration-200 "
|
<button className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-xl font-medium shadow-md hover:bg-blue-700 transition-colors duration-200 "
|
||||||
onClick={_ => setShowOverlay(true)}>
|
onClick={_ => setShowOverlay(true)}>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
<span>Add Keywords</span>
|
<span>Add Keywords</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<div className="w-0 h-6 outline outline-1 outline-offset-[-0.50px] outline-indigo-200"></div>
|
<div className="w-0 h-6 outline outline-1 outline-offset-[-0.50px] outline-indigo-200"></div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-xl font-medium shadow-md hover:bg-blue-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Profile Dropdown */}
|
{/* Profile Dropdown */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -193,6 +203,14 @@ const Header = () => {
|
|||||||
{/* Mobile nav - visible when toggled */}
|
{/* Mobile nav - visible when toggled */}
|
||||||
{isMobileNavOpen && (
|
{isMobileNavOpen && (
|
||||||
<div className="md:hidden px-4 pb-4">
|
<div className="md:hidden px-4 pb-4">
|
||||||
|
{!isLoggedIn && (
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="w-full mb-4 flex justify-center px-4 py-2 bg-blue-600 text-white rounded-xl font-medium shadow-md hover:bg-blue-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
{/* Nav links */}
|
{/* Nav links */}
|
||||||
<div className="flex flex-col gap-2 rounded-xl bg-blue-100/30 p-3 mt-2">
|
<div className="flex flex-col gap-2 rounded-xl bg-blue-100/30 p-3 mt-2">
|
||||||
{navLinks.map((link, index) => (
|
{navLinks.map((link, index) => (
|
||||||
|
|||||||
10
src/main.tsx
10
src/main.tsx
@@ -12,6 +12,13 @@ import { setContext } from '@apollo/client/link/context'
|
|||||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
|
||||||
import { createClient } from 'graphql-ws'
|
import { createClient } from 'graphql-ws'
|
||||||
import { getMainDefinition } from '@apollo/client/utilities'
|
import { getMainDefinition } from '@apollo/client/utilities'
|
||||||
|
import {
|
||||||
|
RecoilRoot,
|
||||||
|
atom,
|
||||||
|
selector,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
} from 'recoil';
|
||||||
|
|
||||||
// Initialize Nhost client
|
// Initialize Nhost client
|
||||||
const nhost = new NhostClient({
|
const nhost = new NhostClient({
|
||||||
@@ -99,7 +106,10 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
<NhostProvider nhost={nhost}>
|
<NhostProvider nhost={nhost}>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
|
<RecoilRoot>
|
||||||
|
|
||||||
<App />
|
<App />
|
||||||
|
</RecoilRoot>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
</NhostProvider>,
|
</NhostProvider>,
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import Herosection from "../assets/herosection.svg"
|
import Herosection from "../assets/herosection.svg"
|
||||||
import DashboardPreview from "../assets/MacBookPro.svg"
|
import DashboardPreview from "../assets/MacBookPro.svg"
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { useSetRecoilState } from 'recoil'
|
||||||
|
import { siteAtom } from '../atoms/siteAtom'
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
|
const [domain, setDomain] = useState('')
|
||||||
|
const setSite = useSetRecoilState(siteAtom)
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleDomainSubmit = () => {
|
||||||
|
// Basic validation - should start with http:// or https://
|
||||||
|
if (!domain.match(/^https?:\/\//)) {
|
||||||
|
alert('Please enter a valid URL starting with http:// or https://')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSite(domain)
|
||||||
|
navigate('/login')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white overflow-x-hidden">
|
<div className="min-h-screen bg-white overflow-x-hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -130,6 +148,8 @@ const HomePage = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter Your Website URL"
|
placeholder="Enter Your Website URL"
|
||||||
className="flex-1 px-4 py-2 sm:py-0 text-sm md:text-base text-[#65677D] bg-transparent outline-none placeholder:text-[#65677D]"
|
className="flex-1 px-4 py-2 sm:py-0 text-sm md:text-base text-[#65677D] bg-transparent outline-none placeholder:text-[#65677D]"
|
||||||
|
value={domain}
|
||||||
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="px-3 md:px-4 py-2.5 rounded-[67px] text-white text-xs md:text-sm font-medium whitespace-nowrap"
|
className="px-3 md:px-4 py-2.5 rounded-[67px] text-white text-xs md:text-sm font-medium whitespace-nowrap"
|
||||||
@@ -137,6 +157,7 @@ const HomePage = () => {
|
|||||||
background: 'linear-gradient(180deg, #4A81FC 0%, #334EFD 100%)',
|
background: 'linear-gradient(180deg, #4A81FC 0%, #334EFD 100%)',
|
||||||
boxShadow: '0 1px 3px 0 rgba(2, 10, 66, 0.38), 0 0 0 1px #4D64FB'
|
boxShadow: '0 1px 3px 0 rgba(2, 10, 66, 0.38), 0 0 0 1px #4D64FB'
|
||||||
}}
|
}}
|
||||||
|
onClick={handleDomainSubmit}
|
||||||
>
|
>
|
||||||
Let Us Show You How
|
Let Us Show You How
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react'
|
|||||||
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';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { siteAtom } from '../atoms/siteAtom';
|
||||||
|
|
||||||
interface ApiKeywordResponse {
|
interface ApiKeywordResponse {
|
||||||
domain: string;
|
domain: string;
|
||||||
@@ -16,24 +18,44 @@ interface GMBResponse {
|
|||||||
has_gmb: boolean;
|
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 Keywords = () => {
|
||||||
const [keywordSections, setKeywordSections] = useState<any[]>([]);
|
const [keywordSections, setKeywordSections] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [hasGmb, setHasGmb] = useState<boolean>(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
throw new Error('No site domain set');
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch GMB status
|
// Fetch GMB status
|
||||||
const gmbResponse = await fetch('https://has-gmb-e7d107688dea.hosted.ghaymah.systems/has_gmb?domain=https://www.rjmedspa.com');
|
const gmbResponse = await fetch(`https://has-gmb-e7d107688dea.hosted.ghaymah.systems/has_gmb?domain=${encodeURIComponent(site)}`);
|
||||||
const gmbData: GMBResponse = await gmbResponse.json();
|
const gmbData: GMBResponse = await gmbResponse.json();
|
||||||
setHasGmb(gmbData.has_gmb);
|
setHasGmb(gmbData.has_gmb);
|
||||||
|
|
||||||
// Fetch keywords
|
// Fetch keywords
|
||||||
const keywordsResponse = await fetch('https://keyword-funnel-api-abbc7b57dc18.hosted.ghaymah.systems/get_categorized_keywords?domain=https://www.rjmedspa.com/');
|
const keywordsResponse = await fetch(`https://keyword-funnel-api-abbc7b57dc18.hosted.ghaymah.systems/get_categorized_keywords?domain=${encodeURIComponent(site)}`);
|
||||||
const keywordsData: ApiKeywordResponse = await keywordsResponse.json();
|
const keywordsData: ApiKeywordResponse = await keywordsResponse.json();
|
||||||
|
|
||||||
// Transform API data to match expected format
|
// Transform API data to match expected format
|
||||||
@@ -102,6 +124,62 @@ const Keywords = () => {
|
|||||||
|
|
||||||
setKeywordSections(transformedSections);
|
setKeywordSections(transformedSections);
|
||||||
setLoading(false);
|
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) {
|
} catch (err) {
|
||||||
setError('Failed to fetch data');
|
setError('Failed to fetch data');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -139,7 +217,13 @@ const Keywords = () => {
|
|||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{keywordSections.map((section) => (
|
{keywordSections.map((section) => (
|
||||||
<KeywordSection key={section.id} section={section} hasGmb={hasGmb} />
|
<KeywordSection
|
||||||
|
key={section.id}
|
||||||
|
section={section}
|
||||||
|
hasGmb={hasGmb}
|
||||||
|
llmStatuses={llmStatuses}
|
||||||
|
loadingKeywords={loadingKeywords}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ContainerPage>
|
</ContainerPage>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const Login = () => {
|
|||||||
// Redirect or handle successful authentication
|
// Redirect or handle successful authentication
|
||||||
console.log('Authentication successful:', result.data);
|
console.log('Authentication successful:', result.data);
|
||||||
// You can redirect here or handle the successful login
|
// You can redirect here or handle the successful login
|
||||||
window.location.href = '/'; // Example redirect
|
window.location.href = '/dashboard/keywords'; // Example redirect
|
||||||
} else {
|
} else {
|
||||||
// Error is already set in the hook, no need to set it again
|
// Error is already set in the hook, no need to set it again
|
||||||
console.error('Authentication failed:', result.error);
|
console.error('Authentication failed:', result.error);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['react', 'react-dom', 'recoil']
|
||||||
|
},
|
||||||
css: {
|
css: {
|
||||||
postcss: './postcss.config.js',
|
postcss: './postcss.config.js',
|
||||||
},
|
},
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم