diff --git a/.gitignore b/.gitignore index 5ef6a52..c926d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,48 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies +# Dependencies /node_modules /.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing +.pnp.js + +# Testing /coverage -# next.js +# Next.js /.next/ /out/ -# production +# Production /build -# misc +# Environment Variables +.env +.env.local +.env.production +.env.development +.env.test +.env*.local + +# Firebase +.firebase/ +firebase-debug.log +firebase-debug.log* + +# Vercel +.vercel/ + +# System & Editor .DS_Store +.vscode/ +.idea/ *.pem -# debug +# TypeScript +*.tsbuildinfo + +# Debug Logs npm-debug.log* yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* +pnpm-debug.log* -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts +# Local Security/Keys (Preserved from previous configuration) +certificates/ +serviceAccountKey.json diff --git a/README-PUSH-NOTIFICATIONS.md b/README-PUSH-NOTIFICATIONS.md new file mode 100644 index 0000000..42f9e23 --- /dev/null +++ b/README-PUSH-NOTIFICATIONS.md @@ -0,0 +1,48 @@ +# Web Push Implementation Guide + +## 1. Generate VAPID Keys +To secure your push notifications, you need VAPID keys. Run this command in your terminal: + +```bash +npx web-push generate-vapid-keys +``` + +This will output a **Public Key** and a **Private Key**. + +## 2. Environment Variables +Add the following to your `.env.local` file (create it if it doesn't exist): + +```properties +NEXT_PUBLIC_VAPID_PUBLIC_KEY="" +VAPID_PRIVATE_KEY="" +VAPID_SUBJECT="mailto:support@yourdomain.com" +``` + +## 3. Database Integration (Completed) +- **`app/api/notifications/subscribe/route.ts`**: Handles saving subscriptions to Firestore (`users/{userId}`). +- **`app/api/notifications/send-chat-push/route.ts`**: Handles retrieving subscriptions from Firestore. + +## 4. Frontend Integration +You must add the `` component to your **User Chat Page** or **Layout** so users can enable notifications. + +Example (`app/chat/layout.tsx` or `app/chat/page.tsx`): +```tsx +import PushNotificationSetup from '@/components/PushNotificationSetup'; +// ... inside your component + +``` + +## 4. Testing +1. Reload your application. +2. Click the new **"Enable Chat Notifications"** button (add `` to your chat page). +3. Check the console for "Subscribed!" +4. Test the push API manually (e.g., with Postman): + + **POST** `http://localhost:3000/api/notifications/send-chat-push` + ```json + { + "userId": "current-user-id", + "messageText": "Hello from the server!", + "senderName": "Test Bot" + } + ``` diff --git a/README.md b/README.md index e215bc4..55a1c39 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,72 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# TruthLens ๐Ÿ”๐Ÿฅ— -## Getting Started +**TruthLens** is an advanced AI-powered nutrition assistant that helps you "see the truth" behind the food you eat. -First, run the development server: +Built as a modern Progressive Web App (PWA), it combines the power of **Google Gemini** and **Groq (Llama 3)** to instantly analyze food products, decode complex ingredient lists, and provide personalized health insights based on your specific dietary needs. -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +## ๐ŸŒŸ Key Features -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### ๐Ÿฅ— Smart Food Analysis +* **Instant Grading**: Our proprietary **Smart Grade** algorithm evaluates products on a scale from **A (Excellent)** to **E (Avoid)** based on ingredients, processing levels, and nutritional value. +* **Ingredient Decoder**: Instantly identifies harmful additives, allergens, and hidden sugars in any product. +* **Barcode Scanning**: Quick access to millions of products via camera scan. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +### ๐Ÿค– Advanced AI Integration +* **Dual-AI Engine**: Leverages **Groq** for lightning-fast responses and **Gemini 1.5** for deep analytical reasoning. +* **Context-Aware Chat**: specialized `/ai-chat` for real-time nutrition advice and meal planning. +* **Dynamic Greetings**: A smart home screen that greets you with context-aware, witty messages depending on the time of day (e.g., "Scanning snacks at 3AM?"). -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +### ๐Ÿ’Ž Flexible & Lifetime Access +* **Fair Pricing**: We offer both Monthly subscriptions and a unique **Lifetime Deal** for permanent access. +* **Tiered Features**: + * **Explorer (Free)**: Basic scanning and manual entry. + * **Pro**: Unlimited scans, detailed AI analysis, and Priority Support. + * **Ultimate**: All Pro features + Exclusive "Global Search" and Beta access. -## Learn More +### ๐Ÿ› ๏ธ Powerful Admin Dashboard +* **Full Control**: A dedicated Admin Interface to manage Users, Subscription Tiers, and App Configurations in real-time. +* **Dynamic Logic**: Toggle features, enable maintenance mode, or adjust pricing instantly without redeploying. -To learn more about Next.js, take a look at the following resources: +## ๐Ÿš€ Technical Stack -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +Built with cutting-edge web technologies for performance and scale: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +* **Frontend**: Next.js 14 (App Router), TypeScript, Material UI (MUI). +* **Backend & Auth**: Firebase (Firestore, Authentication, Security Rules). +* **AI Services**: Groq SDK, Google Generative AI SDK (Gemini). +* **State Management**: React Context + Hooks. +* **Animations**: Framer Motion for buttery smooth UI transitions. +* **Styling**: Responsive Design supporting dark/light modes and mobile-first layouts. -## Deploy on Vercel +## ๐Ÿ› ๏ธ Getting Started -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +1. **Clone the Repo**: + ```bash + git clone https://github.com/yourusername/truthlens.git + cd truthlens + ``` -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +2. **Install Dependencies**: + ```bash + npm install + ``` + +3. **Environment Setup**: + Create a `.env.local` file with your keys: + ```env + NEXT_PUBLIC_FIREBASE_API_KEY=... + NEXT_PUBLIC_GEMINI_API_KEY=... + NEXT_PUBLIC_GROQ_API_KEY=... + ``` + +4. **Run Development Server**: + ```bash + npm run dev + ``` + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- +*Empowering healthier choices, one scan at a time.* diff --git a/actions/ai-actions.ts b/actions/ai-actions.ts new file mode 100644 index 0000000..c5805dd --- /dev/null +++ b/actions/ai-actions.ts @@ -0,0 +1,65 @@ +'use server'; + +import { adminDb } from '@/lib/firebaseAdmin'; +import Groq from 'groq-sdk'; +import { GoogleGenerativeAI } from '@google/generative-ai'; + +// Helper to fetch keys securely from Firestore (Admin-only collection) +async function getSecureKeys() { + try { + if (!adminDb) { + console.warn('Admin SDK not initialized. Using env fallback.'); + return { + groq: process.env.GROQ_API_KEY, + gemini: process.env.GEMINI_API_KEY + }; + } + + const doc = await adminDb.collection('_system_secrets').doc('ai_config').get(); + if (!doc.exists) { + console.warn('System secrets not found. Using fallback env vars.'); + return { + groq: process.env.GROQ_API_KEY, + gemini: process.env.GEMINI_API_KEY + }; + } + const data = doc.data(); + return { + groq: data?.groq || process.env.GROQ_API_KEY, + gemini: data?.gemini || process.env.GEMINI_API_KEY + }; + } catch (error) { + console.error('Failed to fetch secure keys:', error); + return { groq: process.env.GROQ_API_KEY, gemini: process.env.GEMINI_API_KEY }; + } +} + +export async function generateAIResponse(prompt: string, provider: 'groq' | 'gemini' = 'groq') { + const keys = await getSecureKeys(); + + try { + if (provider === 'groq') { + if (!keys.groq) throw new Error('Groq API Key missing in _system_secrets'); + const groq = new Groq({ apiKey: keys.groq }); + + const completion = await groq.chat.completions.create({ + messages: [{ role: 'user', content: prompt }], + model: 'llama-3.3-70b-versatile', + temperature: 0.7, + }); + return completion.choices[0]?.message?.content || 'No response'; + } + else if (provider === 'gemini') { + if (!keys.gemini) throw new Error('Gemini API Key missing in _system_secrets'); + const genAI = new GoogleGenerativeAI(keys.gemini); + const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' }); + + const result = await model.generateContent(prompt); + return result.response.text(); + } + } catch (error: any) { + console.error('AI Generation Error:', error); + return `Error: ${error.message}`; + } + return ''; +} diff --git a/app/(admin)/admin/cancellations/page.tsx b/app/(admin)/admin/cancellations/page.tsx new file mode 100644 index 0000000..09525d8 --- /dev/null +++ b/app/(admin)/admin/cancellations/page.tsx @@ -0,0 +1,190 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + Box, Typography, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, + Chip, IconButton, Tooltip, CircularProgress, Button, Dialog, DialogTitle, DialogContent, DialogActions, DialogContentText +} from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CancelIcon from '@mui/icons-material/Cancel'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { getCancellationRequests, updateUserTier } from '@/services/subscriptionService'; +import { doc, updateDoc, deleteDoc } from 'firebase/firestore'; +import { db } from '@/lib/firebase'; + +interface CancellationRequest { + id: string; + userId: string; + email: string; + reason: string; + status: 'pending' | 'approved' | 'rejected' | 'completed'; + createdAt: any; +} + +export default function CancellationRequestsPage() { + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + + useEffect(() => { + fetchRequests(); + }, []); + + const fetchRequests = async () => { + setLoading(true); + const data = await getCancellationRequests(); + setRequests(data as CancellationRequest[]); + setLoading(false); + }; + + const handleApprove = async (request: CancellationRequest) => { + if (!confirm('Are you sure you want to approve this cancellation? This will downgrade the user to FREE tier immediately.')) return; + + setActionLoading(request.id); + try { + // 1. Update User Tier to Free + await updateUserTier(request.userId, 'free'); + + // 2. Update Request Status + const requestRef = doc(db, 'cancellationRequests', request.id); + await updateDoc(requestRef, { status: 'completed' }); + + // 3. Refresh List + await fetchRequests(); + } catch (error) { + console.error('Error approving cancellation:', error); + alert('Failed to approve cancellation.'); + } finally { + setActionLoading(null); + } + }; + + const handleReject = async (request: CancellationRequest) => { + if (!confirm('Are you sure you want to reject this cancellation?')) return; + + setActionLoading(request.id); + try { + const requestRef = doc(db, 'cancellationRequests', request.id); + await updateDoc(requestRef, { status: 'rejected' }); + await fetchRequests(); + } catch (error) { + console.error('Error rejecting cancellation:', error); + alert('Failed to reject cancellation.'); + } finally { + setActionLoading(null); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to delete this record?')) return; + + try { + await deleteDoc(doc(db, 'cancellationRequests', id)); + setRequests(prev => prev.filter(r => r.id !== id)); + } catch (error) { + console.error('Error deleting request:', error); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'pending': return 'warning'; + case 'approved': + case 'completed': return 'success'; + case 'rejected': return 'error'; + default: return 'default'; + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + Membership Cancellations + + + + + + + + User Email + Reason + Date + Status + Actions + + + + {requests.length === 0 ? ( + + + No cancellation requests found. + + + ) : ( + requests.map((request) => ( + + {request.email} + + + {request.reason} + + + + {request.createdAt?.toDate ? request.createdAt.toDate().toLocaleDateString() : 'N/A'} + + + + + + {request.status === 'pending' && ( + + + handleReject(request)} + disabled={actionLoading === request.id} + > + + + + )} + {request.status !== 'pending' && ( + handleDelete(request.id)} color="error" size="small"> + + + )} + + + )) + )} + +
+
+
+
+ ); +} diff --git a/app/(admin)/admin/chat/page.tsx b/app/(admin)/admin/chat/page.tsx new file mode 100644 index 0000000..3e016d5 --- /dev/null +++ b/app/(admin)/admin/chat/page.tsx @@ -0,0 +1,205 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { listenForAdminChatList, clearAllChats, deleteChat } from '@/services/supportService'; +import AdminChatWindow from '@/components/admin/AdminChatWindow'; +import UserAvatarWithStatus from '@/components/admin/UserAvatarWithStatus'; +import { useAuth } from '@/context/AuthContext'; +import { Chat } from '@/types/chat'; +import { Trash2, Search, Filter, MessageSquare, X } from 'lucide-react'; +import { AnimatePresence, motion } from 'framer-motion'; + +export default function AdminChatPage() { + const { user } = useAuth(); + const [chats, setChats] = useState([]); + const [selectedChatId, setSelectedChatId] = useState(null); + const [isClearing, setIsClearing] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + useEffect(() => { + const unsubscribe = listenForAdminChatList((newChats) => { + setChats(newChats); + }); + return () => unsubscribe(); + }, []); + + const handleDeleteChat = async (chatId: string, e: React.MouseEvent) => { + e.stopPropagation(); + if (!confirm("Are you sure you want to delete this conversation?")) return; + + try { + if (selectedChatId === chatId) setSelectedChatId(null); + await deleteChat(chatId); + } catch (error) { + console.error("Failed to delete chat:", error); + alert("Failed to delete chat."); + } + }; + + const handleClearAll = async () => { + if (!confirm("Are you sure you want to DELETE ALL chats and messages? This cannot be undone.")) return; + + setIsClearing(true); + try { + await clearAllChats(); + setSelectedChatId(null); + } catch (error) { + console.error("Failed to clear chats:", error); + alert("Failed to clear chats."); + } finally { + setIsClearing(false); + } + }; + + const selectedChat = chats.find(c => c.id === selectedChatId); + + if (!user) return null; + + // View A: The Chat List + const ChatListView = ( +
+ {/* Header */} +
+
+

Messages

+ +
+ + {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full bg-white/20 backdrop-blur-md text-white placeholder-blue-100 border border-white/30 rounded-xl py-3 pl-10 pr-4 focus:outline-none focus:bg-white/30 transition-all font-medium" + /> +
+
+ + {/* List */} +
+ {chats.filter(c => + (c.userName?.toLowerCase().includes(searchQuery.toLowerCase())) || + (c.userEmail?.toLowerCase().includes(searchQuery.toLowerCase())) || + c.id.includes(searchQuery) + ).length === 0 ? ( +
+

No conversations found

+
+ ) : ( + chats + .filter(c => + (c.userName?.toLowerCase().includes(searchQuery.toLowerCase())) || + (c.userEmail?.toLowerCase().includes(searchQuery.toLowerCase())) || + c.id.includes(searchQuery) + ) + .map(chat => ( + setSelectedChatId(chat.id)} + className={`w-full text-left p-4 rounded-2xl shadow-sm border transition-all flex items-center gap-4 group ${selectedChatId === chat.id + ? 'bg-white border-blue-400 shadow-blue-100' + : 'bg-white border-transparent hover:border-gray-200' + }`} + > + + + {/* Delete Button - Visible on Group Hover */} + + +
+
+ + {chat.userName || chat.userEmail || "Anonymous User"} + + + {chat.updatedAt?.toDate?.().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + +
+

+ {chat.lastMessage?.text || "Started a new conversation"} +

+
+
+ )) + )} +
+
+ ); + + return ( +
+ {/* Desktop: Sidebar (List) */} +
+ {ChatListView} +
+ + {/* Desktop & Mobile: Main Content (Conversation) */} +
+ + {selectedChatId ? ( + + setSelectedChatId(null)} + /> + + ) : ( +
+
+
+ +
+
+

Select a Conversation

+

+ Choose a chat from the sidebar to start messaging your users. +

+
+ )} +
+
+
+ ); +} + diff --git a/app/(admin)/admin/layout.tsx b/app/(admin)/admin/layout.tsx new file mode 100644 index 0000000..ceaf87a --- /dev/null +++ b/app/(admin)/admin/layout.tsx @@ -0,0 +1,135 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Box, AppBar, Toolbar, Typography, IconButton, useTheme, useMediaQuery, alpha } from '@mui/material'; +import { usePathname } from 'next/navigation'; +import { AnimatePresence } from 'framer-motion'; +import PageTransition from '@/components/animation/PageTransition'; +import AdminSidebar from '@/components/admin/AdminSidebar'; +import Footer from '@/components/layout/Footer'; +import MenuIcon from '@mui/icons-material/MenuRounded'; +import CloseIcon from '@mui/icons-material/CloseRounded'; +import NotificationsIcon from '@mui/icons-material/NotificationsNoneRounded'; + +import { Badge } from '@mui/material'; +import { listenForAdminUnreadCount } from '@/services/supportService'; + +const SIDEBAR_WIDTH = 280; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const [mobileOpen, setMobileOpen] = useState(false); + const [unreadCount, setUnreadCount] = useState(0); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + useEffect(() => { + const unsubscribe = listenForAdminUnreadCount((count) => { + setUnreadCount(count); + }); + return () => unsubscribe(); + }, []); + + const handleDrawerToggle = () => { + setMobileOpen(!mobileOpen); + }; + + const getPageTitle = (path: string) => { + const parts = path.split('/').filter(Boolean); + if (parts.length === 1 && parts[0] === 'admin') return 'Dashboard'; + const lastPart = parts[parts.length - 1]; + return lastPart.charAt(0).toUpperCase() + lastPart.slice(1); + }; + + return ( + + {/* Sidebar Navigation */} + + + {/* Main Content Area */} + + {/* Glassmorphic AppBar */} + + + {/* Hamburger Menu (Mobile Only) */} + + {mobileOpen ? : } + + + + {getPageTitle(pathname)} + + + {/* Right Side Actions */} + + + + + + + + + {/* Page Content Container */} + + + + {children} + + + + + {/* Footer */} + +