Skip to content

Jules wip 8687616222093007272 #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
OPENAI_API_KEY=

# Update these with your pinecone details from your dashboard.
# PINECONE_INDEX_NAME is in the indexes tab under "index name" in blue
# PINECONE_ENVIRONMENT is in indexes tab under "Environment". Example: "us-east1-gcp"
PINECONE_API_KEY=
PINECONE_ENVIRONMENT=
PINECONE_INDEX_NAME=
# OpenAI API Key
OPENAI_API_KEY=your_openai_api_key

# Supabase Configuration
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key
# SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key # Only if used for specific admin actions from backend (use with extreme caution)

# Pinecone Configuration
PINECONE_API_KEY=your_pinecone_api_key
PINECONE_ENVIRONMENT=your_pinecone_environment_region # e.g., "us-east1-gcp"
PINECONE_INDEX_NAME=your_pinecone_index_name
PINECONE_NAMESPACE_PRODUCTS=your_pinecone_product_namespace # Default: "products-namespace"

# NextAuth.js Configuration
# For local development, usually http://localhost:3000
# For Vercel deployment, this MUST be your production Vercel URL (e.g., https://your-project-name.vercel.app)
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=generate_a_strong_random_string_for_production # Important for NextAuth.js security

# Google OAuth Credentials (for NextAuth.js Google Provider)
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
TELEGRAM_REPORT_CHAT_ID=your_telegram_chat_id_for_reports # Can be a user ID or a group/channel ID

# SendGrid Configuration
SENDGRID_API_KEY=your_sendgrid_api_key
[email protected] # Must be a verified sender in SendGrid

# Reporting Configuration
[email protected]
AUTOMATED_REPORT_SECRET=a_very_strong_and_unique_secret_for_cron_job_access

# Application Specific Settings (Defaults or examples, can be managed via UI if `app_settings` table is used)
# Example: DEFAULT_ITEMS_PER_PAGE=10

# Note: Ensure that for production deployment on Vercel, these variables are set
# in the Vercel project's Environment Variables settings.
# Do NOT commit actual secrets to your Git repository. This .env.example file is for guidance only.
# SUPABASE_SERVICE_ROLE_KEY should generally not be exposed to the frontend or client-side accessible parts of Next.js.
# If used, it should be strictly for server-side API functions that require elevated privileges.
# Consider Row Level Security (RLS) in Supabase as the primary data access control mechanism.
84 changes: 84 additions & 0 deletions components/Charts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import {
LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Label
} from 'recharts';

export const WeeklyLeadsChart = ({ data }) => {
if (!data || data.length === 0) {
return <p className="text-center text-gray-500">No data available for weekly leads chart.</p>;
}

// Ensure data is sorted by week_start_date for correct line chart rendering
const sortedData = [...data].sort((a, b) => new Date(a.week_start_date) - new Date(b.week_start_date));


return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={sortedData} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="week_start_date"
tickFormatter={(dateStr) => new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
/>
<YAxis allowDecimals={false}>
<Label value="Number of Leads" angle={-90} position="insideLeft" style={{ textAnchor: 'middle' }} />
</YAxis>
<Tooltip
labelFormatter={(label) => `Week of: ${new Date(label).toLocaleDateString()}`}
formatter={(value) => [value, 'Leads']}
/>
<Legend />
<Line type="monotone" dataKey="count" stroke="#8884d8" strokeWidth={2} name="Leads per Week" />
</LineChart>
</ResponsiveContainer>
);
};

export const MetaAdsPerformanceChart = ({ data }) => {
if (!data || data.length === 0) {
return <p className="text-center text-gray-500">No data available for Meta Ads performance chart.</p>;
}

// Aggregate data by campaign_name for spend and leads
const aggregatedData = data.reduce((acc, item) => {
const campaign = item.campaign_name || 'Unknown Campaign';
if (!acc[campaign]) {
acc[campaign] = { name: campaign, spend: 0, leads: 0 };
}
acc[campaign].spend += item.spend || 0;
acc[campaign].leads += item.leads || 0;
return acc;
}, {});

const chartData = Object.values(aggregatedData);

return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData} margin={{ top: 5, right: 20, left: 10, bottom: 50 /* Increased bottom margin for rotated labels */ }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="name"
angle={-45} // Rotate labels
textAnchor="end" // Anchor rotated labels at the end
height={70} // Increase height to accommodate rotated labels
interval={0} // Show all labels
/>
<YAxis yAxisId="left" orientation="left" stroke="#8884d8" allowDecimals={false}>
<Label value="Spend ($)" angle={-90} position="insideLeft" style={{ textAnchor: 'middle' }} />
</YAxis>
<YAxis yAxisId="right" orientation="right" stroke="#82ca9d" allowDecimals={false}>
<Label value="Leads" angle={-90} position="insideRight" style={{ textAnchor: 'middle' }} />
</YAxis>
<Tooltip
formatter={(value, name, props) => {
if (name === 'Spend') return [`$${parseFloat(value).toFixed(2)}`, 'Spend'];
return [value, 'Leads'];
}}
/>
<Legend />
<Bar yAxisId="left" dataKey="spend" fill="#8884d8" name="Spend" />
<Bar yAxisId="right" dataKey="leads" fill="#82ca9d" name="Leads" />
</BarChart>
</ResponsiveContainer>
);
};
55 changes: 55 additions & 0 deletions components/auth/withAdminAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import React, { ComponentType, useEffect } from 'react';
import Layout from '../layout'; // Assuming a general layout for error/loading states

interface WithAdminAuthProps {}

const withAdminAuth = <P extends object>(WrappedComponent: ComponentType<P>) => {
const AdminAuthComponent = (props: P & WithAdminAuthProps) => {
const { data: session, status } = useSession();
const router = useRouter();
const loading = status === 'loading';

useEffect(() => {
if (!loading && status === 'unauthenticated') {
// Not logged in, redirect to home or a login page
// Alternatively, could use signIn() here:
// signIn('google', { callbackUrl: router.pathname });
router.push('/');
} else if (!loading && status === 'authenticated' && (session?.user as any)?.role !== 'Admin') {
// Logged in, but not an admin
router.push('/unauthorized'); // Or some other page indicating lack of permission
}
}, [session, status, loading, router]);

if (loading) {
return <Layout><p>Loading session...</p></Layout>; // Or a dedicated loading component
}

if (status === 'unauthenticated' || (session && (session?.user as any)?.role !== 'Admin')) {
// Render null or a message while redirecting, or if redirect fails for some reason
// Or a more specific "Access Denied" component within the Layout
return <Layout><p>Access Denied. Redirecting...</p></Layout>;
}

// If authenticated and role is Admin, render the wrapped component
return <WrappedComponent {...props} />;
};

// Set a display name for easier debugging
AdminAuthComponent.displayName = `WithAdminAuth(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;

return AdminAuthComponent;
};

export default withAdminAuth;

// We should also create the /unauthorized page
// For now, if a non-admin tries to access an admin page, they will be redirected to /unauthorized
// If not logged in, they will be redirected to /
// This HOC will be used to wrap admin pages.
// Example usage:
// import withAdminAuth from '../components/auth/withAdminAuth';
// const AdminDashboardPage = () => <div>Admin Dashboard</div>;
// export default withAdminAuth(AdminDashboardPage);
49 changes: 44 additions & 5 deletions components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,56 @@
import { useSession, signIn, signOut } from 'next-auth/react';
import Link from 'next/link'; // Import Link for navigation

interface LayoutProps {
children?: React.ReactNode;
}

export default function Layout({ children }: LayoutProps) {
const { data: session, status } = useSession();
const loading = status === 'loading';

return (
<div className="mx-auto flex flex-col space-y-4">
<header className="container sticky top-0 z-40 bg-white">
<div className="h-16 border-b border-b-slate-200 py-4">
<nav className="ml-4 pl-6">
<a href="#" className="hover:text-slate-600 cursor-pointer">
Home
</a>
<div className="h-16 border-b border-b-slate-200 py-4 flex justify-between items-center">
<nav className="ml-4 pl-6 flex items-center space-x-4">
<Link href="/" legacyBehavior>
<a className="hover:text-slate-600 cursor-pointer">
Home
</a>
</Link>
{(session?.user as any)?.role === 'Admin' && (
<Link href="/admin/upload" legacyBehavior>
<a className="hover:text-slate-600 cursor-pointer">
Admin Upload
</a>
</Link>
)}
</nav>
<div className="mr-4 pr-6">
{loading && <p>Loading...</p>}
{!loading && !session && (
<button
onClick={() => signIn('google')}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Sign In with Google
</button>
)}
{!loading && session && (
<div className="flex items-center space-x-2">
<p>
{session.user?.name || session.user?.email} ({(session.user as any)?.role})
</p>
<button
onClick={() => signOut()}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Sign Out
</button>
</div>
)}
</div>
</div>
</header>
<div>
Expand Down
4 changes: 4 additions & 0 deletions components/ui/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Placeholder for a UI Button component
export default function Button({ children, ...props }) {
return <button {...props}>{children}</button>;
}
74 changes: 74 additions & 0 deletions components/ui/ProductCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import Image from 'next/image'; // Assuming Next.js Image component for optimization

// Basic styling - can be expanded with Tailwind or CSS Modules
const cardStyle = {
border: '1px solid #e0e0e0',
borderRadius: '8px',
padding: '16px',
margin: '16px 0',
maxWidth: '350px', // Or adjust as needed
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
};

const imageStyle = {
width: '100%',
height: '200px', // Fixed height, or use aspect ratio
objectFit: 'cover', // Or 'contain'
borderRadius: '4px',
marginBottom: '12px',
};

const ProductCard = ({ product }) => {
if (!product) {
return null;
}

// Destructure with defaults for safety
const { name, description, image_url, claims, nutrition_info, weight } = product;

return (
<div style={cardStyle}>
{image_url && (
<Image
src={image_url}
alt={name || 'Product Image'}
width={350} // Provide appropriate width
height={200} // Provide appropriate height
style={imageStyle}
// layout="responsive" // if you want responsive image sizing
/>
)}
<h3>{name || 'Unnamed Product'}</h3>
{description && <p style={{ fontSize: '0.9em', color: '#555' }}>{description}</p>}

{weight && <p style={{ fontSize: '0.8em', color: '#777' }}>Weight: {weight}</p>}

{claims && Array.isArray(claims) && claims.length > 0 && (
<div style={{ marginTop: '8px' }}>
<h4 style={{ fontSize: '0.85em', marginBottom: '4px' }}>Claims:</h4>
<ul style={{ fontSize: '0.8em', paddingLeft: '16px' }}>
{claims.map((claim, index) => (
<li key={index}>{typeof claim === 'string' ? claim : claim.text}</li>
))}
</ul>
</div>
)}

{nutrition_info && typeof nutrition_info === 'object' && Object.keys(nutrition_info).length > 0 && (
<div style={{ marginTop: '8px' }}>
<h4 style={{ fontSize: '0.85em', marginBottom: '4px' }}>Nutrition Highlights:</h4>
<ul style={{ fontSize: '0.8em', paddingLeft: '16px' }}>
{/* Example: Displaying a few key nutrition facts. Adjust as needed. */}
{nutrition_info.calories && <li>Calories: {nutrition_info.calories}</li>}
{nutrition_info.protein && <li>Protein: {nutrition_info.protein}g</li>}
{/* Add more nutrition details as desired */}
</ul>
</div>
)}
{/* Add more fields as needed, e.g., price, link to product page, etc. */}
</div>
);
};

export default ProductCard;
19 changes: 19 additions & 0 deletions components/ui/SummaryCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';

const SummaryCard = ({ title, value, children, valueClassName }) => {
return (
<div className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow duration-200 ease-in-out">
<h2 className="text-lg sm:text-xl font-semibold text-gray-700 mb-2 truncate" title={title}>
{title}
</h2>
{value !== undefined && value !== null && (
<p className={`text-2xl sm:text-3xl font-bold text-gray-900 ${valueClassName || ''}`}>
{value}
</p>
)}
{children && <div className="mt-2 text-sm text-gray-600">{children}</div>}
</div>
);
};

export default SummaryCard;
6 changes: 6 additions & 0 deletions config/appConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Placeholder for application level configurations
export const appConfig = {
appName: "My Next App",
version: "1.0.0",
// Add other configurations here
};
2 changes: 2 additions & 0 deletions declarations.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// declarations.d.ts
declare module 'qrcode-terminal';
Loading