Compare commits

..

No commits in common. "main" and "logo-other-changes" have entirely different histories.

13 changed files with 25 additions and 244 deletions

View file

@ -1,8 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npm install *)",
"Bash(npm run *)"
]
}
}

2
.gitignore vendored
View file

@ -11,8 +11,6 @@ node_modules
dist
dist-ssr
*.local
.env
.env.ms
# Editor directories and files
.vscode/*

76
App.tsx
View file

@ -1,16 +1,13 @@
import React, { useState, useEffect } from 'react';
import {
Search, FileDown, Info, LayoutGrid, Zap, Sparkles, Loader2,
TrendingUp, TrendingDown, Minus, Target, Eye, Layers,
BarChart3, Cloud, Activity, Globe, Rocket, ShieldCheck,
Target as TargetIcon, UserCheck, MessageSquare, Copy,
import {
Search, FileDown, Info, LayoutGrid, Zap, Sparkles, Loader2,
TrendingUp, TrendingDown, Minus, Target, Eye, Layers,
BarChart3, Cloud, Activity, Globe, Rocket, ShieldCheck,
Target as TargetIcon, UserCheck, MessageSquare, Copy,
ArrowUpRight, CheckCircle2, Lightbulb, AlertTriangle, Clock,
Users, Newspaper, Smile, Frown, Meh, Star, Compass, LogOut
Users, Newspaper, Smile, Frown, Meh, Star, Compass
} from 'lucide-react';
import { useMsal } from '@azure/msal-react';
import { InteractionRequiredAuthError } from '@azure/msal-browser';
import { loginRequest } from './authConfig';
import { DashboardData, SearchState, CompetitorClaim, MarketMetric, WhitespaceItem, Creator, NewsSentiment, RadarMetric } from './types';
import { fetchMarketInsights } from './geminiService';
@ -63,19 +60,12 @@ const Logo = () => (
);
const App: React.FC = () => {
const { instance, accounts } = useMsal();
const activeAccount = accounts[0];
const handleLogout = () => {
instance.logoutRedirect({ postLogoutRedirectUri: process.env.AZURE_REDIRECT_URI });
};
const [search, setSearch] = useState<SearchState & { country: string }>({
product: '',
category: '',
country: '',
loading: false,
error: null
const [search, setSearch] = useState<SearchState & { country: string }>({
product: '',
category: '',
country: '',
loading: false,
error: null
});
const [data, setData] = useState<DashboardData | null>(null);
const [hoveredClaim, setHoveredClaim] = useState<CompetitorClaim | null>(null);
@ -86,23 +76,10 @@ const App: React.FC = () => {
e?.preventDefault();
if (!search.product || !search.category || search.loading) return;
// Validate session before running the search
try {
await instance.acquireTokenSilent({ ...loginRequest, account: activeAccount });
} catch (err) {
if (err instanceof InteractionRequiredAuthError) {
setSearch(prev => ({ ...prev, error: 'Your session has expired. Redirecting you to sign in again...' }));
setTimeout(() => instance.loginRedirect(loginRequest), 2500);
return;
}
// Transient silent-token error — log but don't block the search
console.warn('Silent token refresh failed (non-interaction):', err);
}
setSearch(prev => ({ ...prev, loading: true, error: null }));
setData(null);
setLoadMessage('Grounded Search Active...');
const messages = [
'Scanning Regional Dynamics...',
'Aggregating Competitor Claims...',
@ -122,9 +99,9 @@ const App: React.FC = () => {
setLastUpdated(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }));
} catch (err: any) {
console.error(err);
setSearch(prev => ({
...prev,
error: err.message || 'System busy. Please try again.'
setSearch(prev => ({
...prev,
error: err.message || 'System busy. Please try again.'
}));
} finally {
clearInterval(interval);
@ -224,23 +201,6 @@ const App: React.FC = () => {
{search.loading ? <Loader2 size={18} className="animate-spin" /> : 'Analyse'}
</button>
</form>
{/* User info + logout */}
{activeAccount && (
<div className="flex items-center gap-3 shrink-0 no-print">
<div className="hidden md:flex flex-col items-end">
<span className="text-[11px] font-black text-black uppercase tracking-tight leading-none">{activeAccount.name?.split(' ')[0]}</span>
<span className="text-[9px] font-bold text-gray-400 uppercase tracking-widest">{activeAccount.username}</span>
</div>
<button
onClick={handleLogout}
title="Sign out"
className="flex items-center justify-center w-10 h-10 bg-gray-50 hover:bg-black hover:text-white text-black rounded-xl border-2 border-black/8 hover:border-black transition-all active:scale-95"
>
<LogOut size={16} />
</button>
</div>
)}
</div>
</header>
@ -527,11 +487,11 @@ const App: React.FC = () => {
<p className="text-[10px] font-black leading-snug">{nuance.strategyTip}</p>
</div>
</div>
{/* <div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex justify-end">
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex justify-end">
<a href={nuance.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-[9px] font-black uppercase text-gray-400 hover:text-black flex items-center gap-1">
Full Source <ArrowUpRight size={10} />
</a>
</div> */}
</div>
</div>
))}
</div>

View file

@ -128,9 +128,6 @@ The dashboard follows a minimalist black, white, and gold (#FFD700) color scheme
| Variable | Description | Required |
|----------|-------------|----------|
| `GEMINI_API_KEY` | Google Generative AI API key | Yes |
| `AZURE_TENANT_ID` | Azure tenant id for sso login | Yes |
| `AZURE_CLIENT_ID` | Azure client id for sso login | Yes |
| `AZURE_REDIRECT_URI` | Azure redirect url after login | Yes |
## License

View file

@ -1,17 +0,0 @@
import { Configuration } from '@azure/msal-browser';
export const msalConfig: Configuration = {
auth: {
clientId: process.env.AZURE_CLIENT_ID!,
authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
redirectUri: process.env.AZURE_REDIRECT_URI!,
postLogoutRedirectUri: process.env.AZURE_REDIRECT_URI!,
},
cache: {
cacheLocation: 'sessionStorage',
},
};
export const loginRequest = {
scopes: ['openid', 'profile', 'email', 'User.Read'],
};

View file

@ -1,36 +0,0 @@
import React, { useEffect } from 'react';
import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { Compass } from 'lucide-react';
import LoginPage from './LoginPage';
const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { instance, inProgress } = useMsal();
useEffect(() => {
// Handle redirect response on app load
instance.handleRedirectPromise().catch(console.error);
}, [instance]);
if (inProgress === InteractionStatus.Startup || inProgress === InteractionStatus.HandleRedirect) {
return (
<div className="min-h-screen bg-white flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative flex items-center justify-center w-12 h-12 bg-black rounded-xl shadow-lg animate-pulse">
<Compass size={24} className="text-[#FFD700]" />
</div>
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-black/30">Authenticating...</p>
</div>
</div>
);
}
return (
<>
<AuthenticatedTemplate>{children}</AuthenticatedTemplate>
<UnauthenticatedTemplate><LoginPage /></UnauthenticatedTemplate>
</>
);
};
export default AuthGuard;

View file

@ -1,64 +0,0 @@
import React from 'react';
import { useMsal } from '@azure/msal-react';
import { Compass } from 'lucide-react';
import { loginRequest } from '../authConfig';
const LoginPage: React.FC = () => {
const { instance } = useMsal();
const handleLogin = () => {
instance.loginRedirect(loginRequest);
};
return (
<div className="min-h-screen bg-white flex flex-col items-center justify-center selection:bg-[#FFD700]">
{/* Gold left accent */}
<div className="fixed top-0 left-0 w-1.5 h-full bg-[#FFD700] pointer-events-none opacity-30" />
<div className="fixed top-0 right-0 w-1.5 h-full bg-black pointer-events-none opacity-5" />
<div className="flex flex-col items-center gap-10 px-8 max-w-sm w-full">
{/* Logo */}
<div className="flex flex-col items-center gap-5">
<div className="relative flex items-center justify-center w-16 h-16 bg-black rounded-2xl shadow-2xl">
<Compass size={32} className="text-[#FFD700]" />
<div className="absolute top-2.5 right-2.5">
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
</div>
</div>
<div className="text-center">
<h1 className="font-black tracking-tight text-xl text-black uppercase leading-tight">APAC Strategy</h1>
<h1 className="font-black tracking-tight text-xl text-black uppercase leading-tight">& Insights Engine</h1>
</div>
<div className="w-12 h-0.5 bg-[#FFD700]" />
<p className="text-[11px] font-bold text-gray-400 uppercase tracking-[0.2em] text-center">
AI-Powered Market Intelligence
</p>
</div>
{/* Sign in box */}
<div className="w-full border-2 border-black/8 rounded-3xl p-8 flex flex-col items-center gap-6 shadow-[0_20px_60px_-20px_rgba(0,0,0,0.1)]">
<p className="text-[11px] font-black uppercase tracking-widest text-black/40">
Sign in to continue
</p>
<button
onClick={handleLogin}
className="w-full flex items-center justify-center gap-3 bg-black text-white hover:bg-[#FFD700] hover:text-black px-6 py-4 rounded-xl font-black uppercase tracking-[0.15em] text-[11px] transition-all active:scale-95 border-2 border-black"
>
Sign in
</button>
<p className="text-[9px] text-gray-400 font-bold uppercase tracking-widest text-center leading-relaxed">
Access restricted to authorised users
</p>
</div>
<p className="text-[9px] text-gray-300 font-bold uppercase tracking-widest">
© BrandTech Group
</p>
</div>
</div>
);
};
export default LoginPage;

View file

@ -2,7 +2,7 @@
import { GoogleGenAI } from "@google/genai";
import { DashboardData } from "./types";
const MODEL_NAME = 'gemini-3-flash-preview';
const MODEL_NAME = 'gemini-3-pro-preview';
export const fetchMarketInsights = async (product: string, category: string): Promise<DashboardData> => {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
@ -75,6 +75,7 @@ export const fetchMarketInsights = async (product: string, category: string): Pr
contents: prompt,
config: {
tools: [{ googleSearch: {} }],
responseMimeType: "application/json",
},
});

View file

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APAC Strat & Intel Engine</title>
<title>Lumina Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
@ -28,6 +28,7 @@
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>

View file

@ -1,13 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { MsalProvider } from '@azure/msal-react';
import { PublicClientApplication } from '@azure/msal-browser';
import { msalConfig } from './authConfig';
import App from './App';
import AuthGuard from './components/AuthGuard';
const msalInstance = new PublicClientApplication(msalConfig);
const rootElement = document.getElementById('root');
if (!rootElement) {
@ -17,10 +11,6 @@ if (!rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<AuthGuard>
<App />
</AuthGuard>
</MsalProvider>
<App />
</React.StrictMode>
);

36
package-lock.json generated
View file

@ -8,8 +8,6 @@
"name": "oho.2---apac-strategy-dashboard",
"version": "0.0.0",
"dependencies": {
"@azure/msal-browser": "^5.8.0",
"@azure/msal-react": "^5.3.1",
"@google/genai": "^1.37.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
@ -22,40 +20,6 @@
"vite": "^6.2.0"
}
},
"node_modules/@azure/msal-browser": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.8.0.tgz",
"integrity": "sha512-X7IZV77bN56l7sbLjkcbQJX1t3U4tgxqztDr/XFbUcUfKk+z2FavcLgKP+OYUNj0wl/pEEtV9lldW9siY8BuHQ==",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "16.5.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "16.5.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.5.1.tgz",
"integrity": "sha512-WS9w9SfI8SEYO7mTnxGeZ3UwQfhAVYCWglYF2/7GNx3ioHiAs2gPkl9eSwVs8cPrmiGh+zi9ai/OOKoq4cyzDw==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-react": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-5.3.1.tgz",
"integrity": "sha512-yR6BFVPyufFDg9qmtCxqpqMH3axxz/9hBL5ZO7zqMH9X9dZvjhcuAO5BL6zhS33VVPpNEZhJflU0LW1NHI2lIw==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@azure/msal-browser": "^5.8.0",
"react": "^16.8.0 || ^17 || ^18 || ^19.2.1"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",

View file

@ -10,11 +10,9 @@
"lint": "tsc --noEmit"
},
"dependencies": {
"@azure/msal-browser": "^5.8.0",
"@azure/msal-react": "^5.3.1",
"@google/genai": "^1.37.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"lucide-react": "^0.562.0",
"react-dom": "^19.2.3"
},
"devDependencies": {

View file

@ -13,10 +13,7 @@ export default defineConfig(({ mode }) => {
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.AZURE_TENANT_ID': JSON.stringify(env.AZURE_TENANT_ID),
'process.env.AZURE_CLIENT_ID': JSON.stringify(env.AZURE_CLIENT_ID),
'process.env.AZURE_REDIRECT_URI': JSON.stringify(env.AZURE_REDIRECT_URI),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {