diff --git a/Frontend/public/aossielogo.png b/Frontend/public/aossielogo.png new file mode 100644 index 0000000..b2421da Binary files /dev/null and b/Frontend/public/aossielogo.png differ diff --git a/Frontend/src/components/ui/checkbox.tsx b/Frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..43ac6c4 --- /dev/null +++ b/Frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } \ No newline at end of file diff --git a/Frontend/src/components/ui/empty-state.tsx b/Frontend/src/components/ui/empty-state.tsx new file mode 100644 index 0000000..513ddf3 --- /dev/null +++ b/Frontend/src/components/ui/empty-state.tsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { + Plus, + BarChart3, + Users, + FileText, + Settings, + Link, + TrendingUp, + Calendar, + Search, + Database +} from 'lucide-react'; + +export interface EmptyStateProps { + type?: 'analytics' | 'content' | 'contracts' | 'audience' | 'exports' | 'alerts' | 'search' | 'generic'; + title?: string; + message?: string; + actionLabel?: string; + onAction?: () => void; + secondaryActionLabel?: string; + onSecondaryAction?: () => void; + className?: string; + size?: 'sm' | 'md' | 'lg'; + showIllustration?: boolean; +} + +const EmptyState: React.FC = ({ + type = 'generic', + title, + message, + actionLabel, + onAction, + secondaryActionLabel, + onSecondaryAction, + className = '', + size = 'md', + showIllustration = true +}) => { + const getEmptyStateConfig = () => { + switch (type) { + case 'analytics': + return { + icon: , + defaultTitle: 'No Analytics Data', + defaultMessage: 'Connect your social media accounts and link content to start tracking performance metrics.', + iconBg: 'bg-blue-100', + actionLabel: 'Connect Accounts', + actionIcon: , + secondaryActionLabel: 'Learn More' + }; + case 'content': + return { + icon: , + defaultTitle: 'No Content Linked', + defaultMessage: 'Link your social media content to contracts to start tracking performance and ROI.', + iconBg: 'bg-green-100', + actionLabel: 'Link Content', + actionIcon: , + secondaryActionLabel: 'View Guide' + }; + case 'contracts': + return { + icon: , + defaultTitle: 'No Contracts Yet', + defaultMessage: 'Create your first sponsorship contract to start collaborating with brands and creators.', + iconBg: 'bg-purple-100', + actionLabel: 'Create Contract', + actionIcon: , + secondaryActionLabel: 'Browse Templates' + }; + case 'audience': + return { + icon: , + defaultTitle: 'No Audience Data', + defaultMessage: 'Connect your social accounts and link content to view detailed audience demographics and insights.', + iconBg: 'bg-orange-100', + actionLabel: 'Connect Accounts', + actionIcon: , + secondaryActionLabel: 'Link Content' + }; + case 'exports': + return { + icon: , + defaultTitle: 'No Exports Yet', + defaultMessage: 'Export your analytics data to create custom reports and share insights with stakeholders.', + iconBg: 'bg-indigo-100', + actionLabel: 'Create Export', + actionIcon: , + secondaryActionLabel: 'View Samples' + }; + case 'alerts': + return { + icon: , + defaultTitle: 'No Alerts Configured', + defaultMessage: 'Set up performance alerts to get notified when your campaigns need attention or are performing well.', + iconBg: 'bg-red-100', + actionLabel: 'Create Alert', + actionIcon: , + secondaryActionLabel: 'Learn About Alerts' + }; + case 'search': + return { + icon: , + defaultTitle: 'No Results Found', + defaultMessage: 'Try adjusting your search criteria or filters to find what you\'re looking for.', + iconBg: 'bg-gray-100', + actionLabel: 'Clear Filters', + actionIcon: null, + secondaryActionLabel: 'Reset Search' + }; + default: + return { + icon: , + defaultTitle: 'No Data Available', + defaultMessage: 'There\'s no data to display at the moment. Try refreshing or check back later.', + iconBg: 'bg-gray-100', + actionLabel: 'Refresh', + actionIcon: null, + secondaryActionLabel: 'Get Help' + }; + } + }; + + const config = getEmptyStateConfig(); + const displayTitle = title || config.defaultTitle; + const displayMessage = message || config.defaultMessage; + const displayActionLabel = actionLabel || config.actionLabel; + const displaySecondaryActionLabel = secondaryActionLabel || config.secondaryActionLabel; + + const sizeClasses = { + sm: 'p-4', + md: 'p-8', + lg: 'p-12' + }; + + const iconSizes = { + sm: 'h-8 w-8', + md: 'h-12 w-12', + lg: 'h-16 w-16' + }; + + const containerSizes = { + sm: 'w-12 h-12', + md: 'w-20 h-20', + lg: 'w-24 h-24' + }; + + const textSizes = { + sm: { + title: 'text-base', + message: 'text-sm' + }, + md: { + title: 'text-xl', + message: 'text-base' + }, + lg: { + title: 'text-2xl', + message: 'text-lg' + } + }; + + const Illustration: React.FC = () => { + if (!showIllustration) return null; + + return ( +
+
+ {React.cloneElement(config.icon, { + className: `${iconSizes[size]} ${config.icon.props.className}` + })} +
+ + {/* Optional decorative elements */} +
+
+
+
+
+
+ ); + }; + + return ( + + + + +

+ {displayTitle} +

+ +

+ {displayMessage} +

+ +
+ {onAction && ( + + )} + + {onSecondaryAction && ( + + )} +
+
+
+ ); +}; + +export default EmptyState; \ No newline at end of file diff --git a/Frontend/src/components/ui/error-state.tsx b/Frontend/src/components/ui/error-state.tsx new file mode 100644 index 0000000..02b04cc --- /dev/null +++ b/Frontend/src/components/ui/error-state.tsx @@ -0,0 +1,184 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { + AlertTriangle, + Wifi, + RefreshCw, + Settings, + ExternalLink, + AlertCircle, + XCircle +} from 'lucide-react'; + +export interface ErrorStateProps { + type?: 'network' | 'api' | 'auth' | 'permission' | 'not-found' | 'rate-limit' | 'generic'; + title?: string; + message?: string; + actionLabel?: string; + onAction?: () => void; + showRetry?: boolean; + onRetry?: () => void; + retryLoading?: boolean; + className?: string; + size?: 'sm' | 'md' | 'lg'; +} + +const ErrorState: React.FC = ({ + type = 'generic', + title, + message, + actionLabel, + onAction, + showRetry = true, + onRetry, + retryLoading = false, + className = '', + size = 'md' +}) => { + const getErrorConfig = () => { + switch (type) { + case 'network': + return { + icon: , + defaultTitle: 'Connection Error', + defaultMessage: 'Unable to connect to the server. Please check your internet connection and try again.', + iconBg: 'bg-red-100', + actionLabel: 'Check Connection', + actionIcon: + }; + case 'api': + return { + icon: , + defaultTitle: 'Service Error', + defaultMessage: 'We encountered an issue while processing your request. Please try again in a moment.', + iconBg: 'bg-orange-100', + actionLabel: 'Contact Support', + actionIcon: + }; + case 'auth': + return { + icon: , + defaultTitle: 'Authentication Required', + defaultMessage: 'Your session has expired. Please sign in again to continue.', + iconBg: 'bg-red-100', + actionLabel: 'Sign In', + actionIcon: + }; + case 'permission': + return { + icon: , + defaultTitle: 'Access Denied', + defaultMessage: 'You don\'t have permission to access this resource. Please contact your administrator.', + iconBg: 'bg-yellow-100', + actionLabel: 'Go Back', + actionIcon: null + }; + case 'not-found': + return { + icon: , + defaultTitle: 'Not Found', + defaultMessage: 'The requested resource could not be found.', + iconBg: 'bg-gray-100', + actionLabel: 'Go Home', + actionIcon: null + }; + case 'rate-limit': + return { + icon: , + defaultTitle: 'Rate Limit Exceeded', + defaultMessage: 'Too many requests. Please wait a moment before trying again.', + iconBg: 'bg-orange-100', + actionLabel: 'Learn More', + actionIcon: + }; + default: + return { + icon: , + defaultTitle: 'Something went wrong', + defaultMessage: 'An unexpected error occurred. Please try again.', + iconBg: 'bg-red-100', + actionLabel: 'Get Help', + actionIcon: + }; + } + }; + + const config = getErrorConfig(); + const displayTitle = title || config.defaultTitle; + const displayMessage = message || config.defaultMessage; + const displayActionLabel = actionLabel || config.actionLabel; + + const sizeClasses = { + sm: 'p-4', + md: 'p-6', + lg: 'p-8' + }; + + const iconSizes = { + sm: 'h-6 w-6', + md: 'h-8 w-8', + lg: 'h-10 w-10' + }; + + const textSizes = { + sm: { + title: 'text-base', + message: 'text-sm' + }, + md: { + title: 'text-lg', + message: 'text-sm' + }, + lg: { + title: 'text-xl', + message: 'text-base' + } + }; + + return ( + + +
+ {React.cloneElement(config.icon, { + className: `${iconSizes[size]} ${config.icon.props.className}` + })} +
+ +

+ {displayTitle} +

+ +

+ {displayMessage} +

+ +
+ {showRetry && onRetry && ( + + )} + + {onAction && ( + + )} +
+
+
+ ); +}; + +export default ErrorState; \ No newline at end of file diff --git a/Frontend/src/components/ui/progress.tsx b/Frontend/src/components/ui/progress.tsx new file mode 100644 index 0000000..e5ae975 --- /dev/null +++ b/Frontend/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } \ No newline at end of file diff --git a/Frontend/src/components/user-nav.tsx b/Frontend/src/components/user-nav.tsx index 9c4939f..b73dd7f 100644 --- a/Frontend/src/components/user-nav.tsx +++ b/Frontend/src/components/user-nav.tsx @@ -11,13 +11,22 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "./ui/dropdown-menu"; + import { useAuth } from "../context/AuthContext"; import { Link } from "react-router-dom"; -export function UserNav() { - const { user, isAuthenticated, logout } = useAuth(); +interface UserNavProps { + showDashboard?: boolean; +} + +export function UserNav({ showDashboard = true }: UserNavProps) { + const { user, isAuthenticated, logout, role } = useAuth(); const [avatarError, setAvatarError] = useState(false); + // Compute dashboardPath synchronously + const roleFromUser = (user as any)?.user_metadata?.role || (user as any)?.role; + const dashboardPath = (roleFromUser === "brand" || role === "brand") ? "/brand/dashboard" : "/dashboard"; + if (!isAuthenticated || !user) { return (
@@ -35,6 +44,7 @@ export function UserNav() { setAvatarError(true); }; + return ( @@ -60,9 +70,11 @@ export function UserNav() { - - Dashboard - + {showDashboard && ( + + Dashboard + + )} Profile Settings diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx index 8588c41..a18e695 100644 --- a/Frontend/src/context/AuthContext.tsx +++ b/Frontend/src/context/AuthContext.tsx @@ -14,8 +14,10 @@ interface AuthContextType { login: () => void; logout: () => void; checkUserOnboarding: (userToCheck?: User | null) => Promise<{ hasOnboarding: boolean; role: string | null }>; + role: string | null; } + const AuthContext = createContext(undefined); interface AuthProviderProps { @@ -64,6 +66,7 @@ async function ensureUserInTable(user: any) { export const AuthProvider = ({ children }: AuthProviderProps) => { const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); + const [role, setRole] = useState(null); const [loading, setLoading] = useState(true); const [lastRequest, setLastRequest] = useState(0); const navigate = useNavigate(); @@ -72,7 +75,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { const checkUserOnboarding = async (userToCheck?: User | null) => { const userToUse = userToCheck || user; if (!userToUse) return { hasOnboarding: false, role: null }; - + // Add rate limiting - only allow one request per 2 seconds const now = Date.now(); if (now - lastRequest < 2000) { @@ -80,29 +83,30 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { return { hasOnboarding: false, role: null }; } setLastRequest(now); - + // Check if user has completed onboarding by looking for social profiles or brand data const { data: socialProfiles } = await supabase .from("social_profiles") .select("id") .eq("user_id", userToUse.id) .limit(1); - + const { data: brandData } = await supabase .from("brands") .select("id") .eq("user_id", userToUse.id) .limit(1); - - const hasOnboarding = (socialProfiles && socialProfiles.length > 0) || (brandData && brandData.length > 0); - + + // Always return boolean, never null + const hasOnboarding = Boolean((socialProfiles && socialProfiles.length > 0) || (brandData && brandData.length > 0)); + // Get user role const { data: userData } = await supabase .from("users") .select("role") .eq("id", userToUse.id) .single(); - + return { hasOnboarding, role: userData?.role || null }; }; @@ -131,6 +135,17 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { setUser(data.session?.user || null); setIsAuthenticated(!!data.session?.user); + // Determine role once at startup + if (data.session?.user) { + try { + const res = await checkUserOnboarding(data.session.user); + if (res.role !== null && res.role !== undefined) { + setRole(res.role); + } + } catch (err) { + console.error("AuthContext: error determining role", err); + } + } if (data.session?.user) { console.log("AuthContext: Ensuring user in table"); try { @@ -160,13 +175,20 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { if (session?.user) { console.log("AuthContext: User authenticated"); try { + const userId = session.user.id; await ensureUserInTable(session.user); + const res = await checkUserOnboarding(session.user); + // Only update role if userId matches current session.user.id and role is defined + if (res.role !== undefined && session.user.id === userId) { + setRole(res.role); + } } catch (error) { console.error("AuthContext: Error ensuring user in table", error); } setLoading(false); } else { // User logged out + setRole(null); console.log("AuthContext: User logged out"); setLoading(false); } @@ -208,7 +230,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { } return ( - + {children} ); diff --git a/Frontend/src/index.css b/Frontend/src/index.css index f2a93bb..55feb25 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap'); @custom-variant dark (&:is(.dark *)); diff --git a/Frontend/src/pages/Brand/Dashboard.module.css b/Frontend/src/pages/Brand/Dashboard.module.css new file mode 100644 index 0000000..4c083d8 --- /dev/null +++ b/Frontend/src/pages/Brand/Dashboard.module.css @@ -0,0 +1,27 @@ +.brand-nav-btn { + @apply w-full border-none rounded-xl px-4 py-3 flex items-center gap-2 text-base font-medium cursor-pointer transition-colors duration-200 bg-transparent text-[var(--muted-text)] outline-none; +} +.brand-nav-btn:hover, +.brand-nav-btn:focus-visible { + @apply bg-[var(--sidebar-active)] text-[var(--text-default)] outline outline-2 outline-[var(--accent)]; +} +.brand-nav-btn.active { + @apply bg-[var(--sidebar-active)] text-[var(--text-default)] outline-none; +} +.brand-nav-btn.collapsed { + @apply justify-center px-2 py-2; +} +.brand-new-btn { + @apply w-full bg-[var(--primary)] border-none rounded-xl px-4 py-3 flex items-center gap-2 text-base font-medium cursor-pointer transition-colors duration-200 text-[var(--text-default)] outline-none; +} +.brand-new-btn:hover, +.brand-new-btn:focus-visible { + @apply bg-[var(--primary-hover)] outline outline-2 outline-[var(--accent)]; +} +.brand-profile-btn { + @apply w-full bg-transparent border-none rounded-lg px-4 py-3 flex items-center gap-3 text-base font-medium cursor-pointer transition-colors duration-200 text-[var(--muted-text)] outline-none; +} +.brand-profile-btn:hover, +.brand-profile-btn:focus-visible { + @apply bg-[var(--sidebar-active)] text-[var(--text-default)] outline outline-2 outline-[var(--accent)]; +} diff --git a/Frontend/src/pages/Brand/Dashboard.tsx b/Frontend/src/pages/Brand/Dashboard.tsx index 023c77b..e88fd92 100644 --- a/Frontend/src/pages/Brand/Dashboard.tsx +++ b/Frontend/src/pages/Brand/Dashboard.tsx @@ -1,380 +1,557 @@ -import Chat from "@/components/chat/chat"; -import { Button } from "../../components/ui/button"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "../../components/ui/card"; -import { Input } from "../../components/ui/input"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "../../components/ui/tabs"; -import { - BarChart3, - Users, - MessageSquareMore, - TrendingUp, - Search, - Bell, - UserCircle, - FileText, - Send, - Clock, - CheckCircle2, - XCircle, - BarChart, - ChevronRight, - FileSignature, - LineChart, - Activity, - Rocket, -} from "lucide-react"; -import { CreatorMatches } from "../../components/dashboard/creator-matches"; -import { useState } from "react"; +import React, { useState } from "react"; +import styles from "./Dashboard.module.css"; +import { Menu, Settings, Search, Plus, Home, BarChart3, MessageSquare, FileText, ChevronLeft, ChevronRight, User } from "lucide-react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { UserNav } from "../../components/user-nav"; -const Dashboard = () => { - // Mock sponsorships for selection (replace with real API call if needed) - const sponsorships = [ - { id: "1", title: "Summer Collection" }, - { id: "2", title: "Tech Launch" }, - { id: "3", title: "Fitness Drive" }, +const PRIMARY = "var(--primary)"; +const SECONDARY = "var(--secondary)"; +const ACCENT = "var(--accent)"; + +const TABS = [ + { label: "Discover", route: "/brand/dashboard", icon: Home }, + { label: "Contracts", route: "/brand/contracts", icon: FileText }, + { label: "Messages", route: "/brand/messages", icon: MessageSquare }, + { label: "Tracking", route: "/brand/tracking", icon: BarChart3 }, ]; - const [selectedSponsorship, setSelectedSponsorship] = useState(""); + +export default function BrandDashboard() { + const navigate = useNavigate(); + const location = useLocation(); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + + // Auto-collapse sidebar for viewports < 1024px + React.useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 1024) { + setSidebarCollapsed(true); + } else { + setSidebarCollapsed(false); + } + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); return ( - <> -
- {/* Navigation */} - - -
- {/* Header */} -
-

- Brand Dashboard -

-

- Discover and collaborate with creators that match your brand -

- {/* Search */} -
- - + {/* New Button */} +
+
- {/* Main Content */} - - - Discover - Contracts - Messages - Tracking - - - {/* Discover Tab */} - - {/* Stats */} -
- - - - Active Creators - - - - -
12,234
-

- +180 from last month -

-
-
- - - - Avg. Engagement - - - - -
4.5%
-

- +0.3% from last month -

-
-
- - - - Active Campaigns - - - - -
24
-

- 8 pending approval -

-
-
- - - - Messages - - - - -
12
-

- 3 unread messages -

-
-
+ {/* Navigation */} +
+ {TABS.map((tab) => { + const isActive = location.pathname === tab.route; + const Icon = tab.icon; + return ( + + ); + })}
- {/* Creator Recommendations */} -
-
-

- Matched Creators for Your Campaign -

-
-
- - -
- + {/* Bottom Section - Profile and Settings */} +
+ {/* Profile */} + - {/* Contracts Tab */} - -
-

- Active Contracts -

- + {/* Settings */} +
-
- {[1, 2, 3].map((i) => ( -
-
-
- Creator -
-

- Summer Collection Campaign -

-

- with Alex Rivera -

-
- - - Due in 12 days - + {/* Collapse Toggle */} + +
+ + {/* Main Content */} +
+ {/* Top Bar */} +
+
+ INPACT Brands
+
+ {/* Settings button removed from top bar since it's now in sidebar */} +
-
- - Active + + {/* Content Area */} +
+ {/* INPACT AI Title with animated gradient */} +

+ INPACT + + AI -

- $2,400 -

-

-
-
-
- - -
- -
-
- ))} -
- + - {/* Messages Tab */} - +
{ + e.currentTarget.style.borderColor = "#87CEEB"; + e.currentTarget.style.background = "rgba(26, 26, 26, 0.8)"; + e.currentTarget.style.backdropFilter = "blur(10px)"; + e.currentTarget.style.padding = "12px 16px"; + e.currentTarget.style.gap = "8px"; + e.currentTarget.style.width = "110%"; + e.currentTarget.style.transform = "translateX(-5%)"; + // Remove glass texture + const overlay = e.currentTarget.querySelector('[data-glass-overlay]'); + if (overlay) (overlay as HTMLElement).style.opacity = "0"; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = "rgba(255, 255, 255, 0.1)"; + e.currentTarget.style.background = "rgba(26, 26, 26, 0.6)"; + e.currentTarget.style.backdropFilter = "blur(20px)"; + e.currentTarget.style.padding = "16px 20px"; + e.currentTarget.style.gap = "12px"; + e.currentTarget.style.width = "100%"; + e.currentTarget.style.transform = "translateX(0)"; + // Restore glass texture + const overlay = e.currentTarget.querySelector('[data-glass-overlay]'); + if (overlay) (overlay as HTMLElement).style.opacity = "1"; + }} > - - - - {/* Tracking Tab */} - -
- - - - Total Reach - - - - -
2.4M
-

- Across all campaigns -

-
-
- - - - Engagement Rate - - - - -
5.2%
-

- Average across creators -

-
-
- - - ROI - - - -
3.8x
-

- Last 30 days -

-
-
- - - - Active Posts - - - - -
156
-

- Across platforms -

-
-
-
- -
-

- Campaign Performance -

-
- {[1, 2, 3].map((i) => ( -
-
-
- Creator -
-

Summer Collection

-

- with Sarah Parker -

+ {/* Glass texture overlay */} +
+ + +
-
-

458K Reach

-

- 6.2% Engagement -

-
-
-
-
- - 12 Posts Live -
-
- - 2 Pending -
-
-
+ + {/* Quick Actions */} +
+ {[ + { label: "Find Creators", icon: "👥", color: "#3b82f6" }, + { label: "Campaign Stats", icon: "📊", color: "#10b981" }, + { label: "Draft Contract", icon: "📄", color: "#f59e0b" }, + { label: "Analytics", icon: "📈", color: "#8b5cf6" }, + { label: "Messages", icon: "💬", color: "#ef4444" }, + ].map((action, index) => ( + ))}
- - -
- - ); -}; -export default Dashboard; + {/* CSS for gradient animation */} + {/* CSS for gradient animation and nav button styles */} + +
+ ); +}