diff --git a/package-lock.json b/package-lock.json index 89c7c92..3acbedc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "react-dom": "^19.1.1", "react-icons": "^5.5.0", "react-router-dom": "^7.9.1", - "recharts": "^3.2.1" + "recharts": "^3.2.1", + "recoil": "^0.7.7" }, "devDependencies": { "@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": { "version": "4.0.0", "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" } }, + "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": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", diff --git a/package.json b/package.json index 07db6b6..5458b58 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "react-dom": "^19.1.1", "react-icons": "^5.5.0", "react-router-dom": "^7.9.1", - "recharts": "^3.2.1" + "recharts": "^3.2.1", + "recoil": "^0.7.7" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/src/atoms/siteAtom.ts b/src/atoms/siteAtom.ts new file mode 100644 index 0000000..bcb5037 --- /dev/null +++ b/src/atoms/siteAtom.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const siteAtom = atom({ + key: 'siteAtom', + default: '' +}); diff --git a/src/components/KeywordRow.tsx b/src/components/KeywordRow.tsx index f739f39..6fa1f70 100644 --- a/src/components/KeywordRow.tsx +++ b/src/components/KeywordRow.tsx @@ -7,9 +7,16 @@ import { ActionBadge } from "./shared/ActionBadge"; interface KeywordRowProps { keyword: KeywordRowType; 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 (
@@ -25,7 +32,17 @@ export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
- + {isLoading ? ( +
+ ) : ( +
+ {llmStatus.google.status} +
+ )}
{hasGmb && ( @@ -39,15 +56,45 @@ export function KeywordRow({ keyword, hasGmb }: KeywordRowProps) {
- + {isLoading ? ( +
+ ) : ( +
+ {llmStatus.chatGPT.status} +
+ )}
- + {isLoading ? ( +
+ ) : ( +
+ {llmStatus.gemini.status} +
+ )}
- + {isLoading ? ( +
+ ) : ( +
+ {llmStatus.perplexity.status} +
+ )}
diff --git a/src/components/KeywordSection.tsx b/src/components/KeywordSection.tsx index fb05e16..d464461 100644 --- a/src/components/KeywordSection.tsx +++ b/src/components/KeywordSection.tsx @@ -7,9 +7,16 @@ import MetricTooltip from "./shared/MetricTooltip"; interface KeywordSectionProps { section: KeywordSectionType; hasGmb: boolean; + llmStatuses: Record; + loadingKeywords: Record; } -export function KeywordSection({ section, hasGmb }: KeywordSectionProps) { +export function KeywordSection({ section, hasGmb, llmStatuses, loadingKeywords }: KeywordSectionProps) { const [isExpanded, setIsExpanded] = useState(section.expanded); return ( @@ -78,15 +85,21 @@ export function KeywordSection({ section, hasGmb }: KeywordSectionProps) {
{section.keywords.map((keyword) => ( - + ))}
-
+ {/*
-
+
*/} )}
diff --git a/src/components/shared/Header.tsx b/src/components/shared/Header.tsx index 52b1b3f..72f6e5e 100644 --- a/src/components/shared/Header.tsx +++ b/src/components/shared/Header.tsx @@ -34,6 +34,7 @@ const Header = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMobileNavOpen, setIsMobileNavOpen] = useState(false); const isHeaderVisible = location.pathname !== '/'; + const isLoggedIn = localStorage.getItem('sp_user') !== null; const toggleMenu = () => setIsMenuOpen(!isMenuOpen); const toggleMobileNav = () => setIsMobileNavOpen(!isMobileNavOpen); @@ -121,20 +122,29 @@ const Header = () => { - {/* Right Side - Desktop Only */} -
- {/* Add Keyword Button */} + {/* Right Side - Desktop Only */} +
+ {isLoggedIn ? ( + <> + {/* Add Keyword Button */} + - +
+ + ) : ( + + Log In + + )} - -
- - {/* Profile Dropdown */} + {/* Profile Dropdown */}
- {/* Mobile nav - visible when toggled */} - {isMobileNavOpen && ( -
- {/* Nav links */} + {/* Mobile nav - visible when toggled */} + {isMobileNavOpen && ( +
+ {!isLoggedIn && ( + + Log In + + )} + {/* Nav links */}
{navLinks.map((link, index) => ( + + + , diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 389cefa..3f8bbef 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,8 +1,26 @@ -import React from 'react' +import React, { useState } from 'react' import Herosection from "../assets/herosection.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 [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 (
@@ -130,6 +148,8 @@ const HomePage = () => { type="text" 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]" + value={domain} + onChange={(e) => setDomain(e.target.value)} /> @@ -180,4 +201,4 @@ const HomePage = () => { ) } -export default HomePage \ No newline at end of file +export default HomePage diff --git a/src/pages/Keywords.tsx b/src/pages/Keywords.tsx index 095aa50..b54232b 100644 --- a/src/pages/Keywords.tsx +++ b/src/pages/Keywords.tsx @@ -2,6 +2,8 @@ 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; @@ -16,24 +18,44 @@ 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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [hasGmb, setHasGmb] = useState(false); + const [llmStatuses, setLlmStatuses] = useState>({}); + const [loadingKeywords, setLoadingKeywords] = useState>({}); + 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=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(); 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 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 @@ -102,6 +124,62 @@ const Keywords = () => { setKeywordSections(transformedSections); setLoading(false); + + // Initialize LLM statuses and loading states + const initialStatuses: Record = {}; + const initialLoading: Record = {}; + + 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); @@ -139,7 +217,13 @@ const Keywords = () => {
{keywordSections.map((section) => ( - + ))}
diff --git a/src/pages/login.tsx b/src/pages/login.tsx index f6608eb..062318d 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -44,7 +44,7 @@ const Login = () => { // Redirect or handle successful authentication console.log('Authentication successful:', result.data); // You can redirect here or handle the successful login - window.location.href = '/'; // Example redirect + window.location.href = '/dashboard/keywords'; // Example redirect } else { // Error is already set in the hook, no need to set it again console.error('Authentication failed:', result.error); diff --git a/vite.config.ts b/vite.config.ts index 802e341..4edf158 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,9 @@ import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + optimizeDeps: { + include: ['react', 'react-dom', 'recoil'] + }, css: { postcss: './postcss.config.js', },