Skip to content
Draft
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
3 changes: 0 additions & 3 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Gemini AI Configuration
GEMINI_API_KEY=your_gemini_api_key_here

# Cloudinary Configuration
# Get these from https://cloudinary.com/console
VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name
Expand Down
124 changes: 1 addition & 123 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { ICONS, PRODUCTS as INITIAL_PRODUCTS, CATEGORIES, WHATSAPP_NUMBER as INITIAL_WA, ADMIN_PASSCODE, HERO_SLIDES } from './constants';
import { Product, Message, GroundingSource, Category } from './types';
import { gemini } from './services/geminiService';
import { Product, Category } from './types';
import { uploadImage } from './services/cloudinaryService';

type ViewType = 'home' | 'products' | 'detail' | 'admin' | 'about';
Expand Down Expand Up @@ -537,15 +536,6 @@ const App: React.FC = () => {
// --- ADMIN STATE (Only auth needed here, rest is in AdminView) ---
const [isAdmin, setIsAdmin] = useState(false);

// --- AI CHAT STATE ---
const [chatOpen, setChatOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{ role: 'assistant', content: 'Hello! I am the Nexlyn Grid Expert. I can assist with MikroTik® hardware selection and technical planning. How can I help your business today?' }
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);

// --- THEME SYNC ---
useEffect(() => {
const root = window.document.documentElement;
Expand All @@ -572,10 +562,6 @@ const App: React.FC = () => {
return () => window.removeEventListener('scroll', handleScroll);
}, []);

useEffect(() => {
if (chatOpen) chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, chatOpen]);

// Slide transition for Hero
useEffect(() => {
if (view === 'home') {
Expand Down Expand Up @@ -607,54 +593,6 @@ const App: React.FC = () => {
}, [selectedCat, searchQuery, products]);

// --- ACTIONS ---
const handleChat = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!input.trim() || isLoading) return;

const userMsg: Message = { role: 'user', content: input };
setMessages(prev => [...prev, userMsg]);
setInput('');
setIsLoading(true);

const assistantMsg: Message = { role: 'assistant', content: '' };
setMessages(prev => [...prev, assistantMsg]);

try {
let accumulatedText = "";
let accumulatedSources: GroundingSource[] = [];

const stream = gemini.streamTech(input);
for await (const chunk of stream) {
accumulatedText += chunk.text;
if (chunk.sources.length > 0) {
accumulatedSources = [...new Set([...accumulatedSources, ...chunk.sources])];
}

setMessages(prev => {
const updated = [...prev];
const lastIdx = updated.length - 1;
updated[lastIdx] = {
...updated[lastIdx],
content: accumulatedText,
sources: accumulatedSources
};
return updated;
});
}
} catch (err) {
setMessages(prev => {
const updated = [...prev];
updated[updated.length - 1] = {
role: 'assistant',
content: "System interference detected. Please re-initiate the request or contact technical support."
};
return updated;
});
} finally {
setIsLoading(false);
}
};

const toggleTheme = () => setTheme(prev => prev === 'dark' ? 'light' : 'dark');

const openWhatsApp = (context?: 'product' | 'reseller' | 'general' | 'category', data?: any) => {
Expand Down Expand Up @@ -1015,67 +953,7 @@ const App: React.FC = () => {
)}
</main>

{/* AI SIDE PANEL */}
<div className={`fixed inset-y-0 right-0 w-full md:w-[480px] z-[200] transition-transform duration-[800ms] cubic-bezier(0.16, 1, 0.3, 1) ${chatOpen ? 'translate-x-0' : 'translate-x-full'}`}>
<div className="h-full glass-panel border-l border-black/10 dark:border-white/10 flex flex-col shadow-2xl backdrop-blur-3xl">
<div className="p-10 border-b border-black/10 dark:border-white/10 flex justify-between items-center bg-black/[0.04] dark:bg-white/[0.04]">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-nexlyn rounded-2xl flex items-center justify-center text-white shadow-2xl shadow-nexlyn/40 group overflow-hidden relative">
<div className="absolute inset-0 bg-white/20 animate-pulse" />
<ICONS.Bolt className="w-8 h-8 relative z-10" />
</div>
<div>
<h3 className="font-black text-2xl italic uppercase text-slate-900 dark:text-white tracking-tighter">Grid Expert</h3>
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.8)]" />
<span className="text-[10px] font-black uppercase text-nexlyn tracking-widest">NEX-AI Active</span>
</div>
</div>
</div>
<button onClick={() => setChatOpen(false)} aria-label="Close Chat" className="w-10 h-10 flex items-center justify-center text-slate-500 hover:text-nexlyn text-3xl font-light transition-colors focus:outline-none focus:text-nexlyn">&times;</button>
</div>
<div className="flex-1 overflow-y-auto p-10 space-y-10 no-scrollbar">
{messages.map((m, i) => (
<div key={i} className={`flex flex-col ${m.role === 'user' ? 'items-end' : 'items-start'} animate-in slide-in-from-bottom-2 duration-500`}>
<div className={`max-w-[95%] p-7 rounded-3xl text-sm leading-relaxed font-medium ${m.role === 'user' ? 'bg-nexlyn text-white rounded-tr-none shadow-xl shadow-nexlyn/20' : 'glass-panel text-slate-700 dark:text-slate-300 rounded-tl-none border border-black/5 dark:border-white/10'}`}>
{m.content || (isLoading && i === messages.length - 1 ? <span className="flex gap-1 items-center">Generating <span className="animate-pulse">...</span></span> : m.content)}
{m.sources && m.sources.length > 0 && (
<div className="mt-6 pt-6 border-t border-black/10 dark:border-white/10 space-y-3">
<div className="text-[10px] font-black uppercase text-nexlyn tracking-widest">Verified Intelligence Sources:</div>
<div className="flex flex-wrap gap-2">
{m.sources.map((s, idx) => (
<a key={idx} href={s.uri} target="_blank" className="px-4 py-1.5 glass-panel border border-black/5 dark:border-white/5 rounded-full text-[10px] font-bold text-slate-500 hover:text-nexlyn hover:border-nexlyn transition-all truncate max-w-[150px]">{s.title}</a>
))}
</div>
</div>
)}
</div>
</div>
))}
<div ref={chatEndRef} />
</div>
<form onSubmit={handleChat} className="p-10 border-t border-black/10 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-md">
<div className="relative">
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Query hardware metrics or network design..."
className="w-full glass-panel py-6 px-8 rounded-2xl border border-black/10 dark:border-white/10 focus:outline-none focus:border-nexlyn text-sm font-bold text-slate-900 dark:text-white shadow-inner"
/>
<button type="submit" disabled={isLoading || !input.trim()} className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-nexlyn text-white rounded-xl flex items-center justify-center shadow-lg hover:scale-110 disabled:opacity-50 disabled:scale-100 transition-all focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-nexlyn">
<ICONS.ChevronRight className="w-5 h-5" />
</button>
</div>
</form>
</div>
</div>

<div className="fixed bottom-12 right-12 z-[150] flex flex-col items-end gap-6">
<button onClick={() => setChatOpen(true)} aria-label="Open AI Assistant" className="w-20 h-20 glass-panel border border-black/10 dark:border-white/10 text-slate-900 dark:text-white rounded-[2rem] flex items-center justify-center shadow-2xl hover:scale-110 active:scale-95 transition-all group overflow-hidden relative focus:outline-none focus:ring-2 focus:ring-nexlyn">
<div className="absolute inset-0 bg-nexlyn opacity-0 group-hover:opacity-10 transition-opacity" />
<ICONS.Bolt className="w-10 h-10 relative z-10 text-nexlyn" />
</button>

<div className="relative group">
<div className="absolute inset-0 bg-[#25D366] rounded-[2rem] animate-sonar pointer-events-none" />
<button
Expand Down
143 changes: 100 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,85 +2,142 @@
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>

# Run and deploy your AI Studio app
# NEXLYN - MikroTik® Distribution Platform

This contains everything you need to run your app locally.
A lightweight, modern web application for NEXLYN Distributions - your professional MikroTik® hardware distributor. Built with React and optimized for fast performance and easy deployment.

View your app in AI Studio: https://ai.studio/apps/drive/1TooJrvvYNEPtXmyX5sfuyYKZ-ofUdW0j
## ✨ Features

## Run Locally
- 🛍️ **Product Catalog** - Browse MikroTik® routers, switches, wireless equipment, and more
- 🔐 **Admin Panel** - Secure dashboard for managing products, settings, and content
- 📱 **WhatsApp Integration** - Direct B2B quote requests via WhatsApp
- 🎨 **Dark/Light Mode** - Beautiful themes with smooth transitions
- ☁️ **Cloudinary Integration** - Secure image hosting for product photos
- 📊 **Category Management** - Organized by Routing, Switching, Wireless, 5G/LTE, IoT, and Accessories
- 📱 **Responsive Design** - Optimized for desktop, tablet, and mobile

**Prerequisites:** Node.js
## 🚀 Quick Start

**Prerequisites:** Node.js 16+

1. Install dependencies:
1. **Install dependencies:**
```bash
npm install
```

2. Configure environment variables in `.env.local`:
```env
# Gemini AI API Key
GEMINI_API_KEY=your_gemini_api_key_here
2. **Configure environment variables** (optional for admin panel image uploads):

# Cloudinary Configuration (for admin image uploads)
Copy `.env.local.example` to `.env.local` and configure:
```env
# Cloudinary Configuration (optional - for admin image uploads)
VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name
VITE_CLOUDINARY_UPLOAD_PRESET=your_upload_preset
```

3. Run the app:
3. **Run the app:**
```bash
npm run dev
```

## Cloudinary Setup for Image Uploads
4. **Build for production:**
```bash
npm run build
```

The admin panel uses Cloudinary for secure image hosting. Follow these steps:
## 🔧 Cloudinary Setup (Optional)

The admin panel supports direct image upload to Cloudinary. To enable this feature:

### 1. Create a Free Cloudinary Account
- Go to https://cloudinary.com/users/register/free
- Sign up for a free account (25GB storage, 25GB bandwidth/month)
- Visit: https://cloudinary.com/users/register/free
- Free tier includes: 25GB storage, 25GB bandwidth/month

### 2. Get Your Cloud Name
- After logging in, go to Dashboard
- Copy your **Cloud Name** (e.g., `dxxxxxxxxxxxxx`)
### 2. Get Your Credentials
- Go to your Cloudinary Dashboard
- Copy your **Cloud Name**

### 3. Create an Upload Preset
- Go to Settings → Upload → Upload Presets
- Navigate to: Settings → Upload → Upload Presets
- Click "Add upload preset"
- Set **Signing Mode** to "Unsigned"
- Set **Folder** to "nexlyn-products" (optional)
- Copy the **Upload preset name** (e.g., `nexlyn_unsigned`)
- Optional: Set **Folder** to "nexlyn-products"
- Copy the **Upload preset name**

### 4. Update .env.local
```env
VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name_here
VITE_CLOUDINARY_UPLOAD_PRESET=your_preset_name_here
```

## Features
**Note:** You can also add products with direct image URLs without Cloudinary configuration.

## 🎯 Tech Stack

- **React 19** - Modern UI library with TypeScript
- **Vite** - Lightning-fast build tool and dev server
- **Native CSS** - Custom styling with Tailwind-inspired utilities
- **Cloudinary API** - Optional image hosting via native fetch

## 📦 Deployment

This app is optimized for deployment on any static hosting platform:

- **Vercel** (Recommended)
- **Netlify**
- **GitHub Pages**
- **Cloudflare Pages**

### Vercel Deployment

1. Push your code to GitHub
2. Import project in Vercel
3. Add environment variables (if using Cloudinary)
4. Deploy!

### Environment Variables for Production

If using Cloudinary for image uploads, set these in your deployment platform:
```
VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name
VITE_CLOUDINARY_UPLOAD_PRESET=your_upload_preset
```

## 🔐 Admin Access

The admin panel is protected by a passcode (defined in `constants.tsx`). Default features:

- Product management (add, edit, delete)
- WhatsApp number configuration
- About content editing
- Address and location settings
- Product statistics dashboard

## 📊 Bundle Size

- **Dependencies:** 68 packages (65% lighter than previous version)
- **Build time:** ~80ms
- **Optimized for:** Fast loading and minimal bandwidth

## 🛠️ Project Structure

```
NEXLYN---v2/
├── App.tsx # Main application component
├── constants.tsx # Product data, categories, settings
├── types.ts # TypeScript type definitions
├── services/
│ └── cloudinaryService.ts # Image upload service
├── index.tsx # Application entry point
├── index.html # HTML template
├── vite.config.ts # Vite configuration
└── package.json # Dependencies and scripts
```

### ✅ Retained from Original Design
- **WhatsApp Integration** - Existing ICONS.WhatsApp component
- **AI Chat** - "Grid Expert" and "NEX-AI Active" branding
- **Admin Panel Structure** - Security authorization, stats dashboard
## 📝 License

### 🆕 New Enhancements
- **File Upload** - Direct image upload to Cloudinary with preview
- **Image Management** - Upload progress, file validation, preview
- **Dual Input** - Support both file upload and manual URL entry
© NEXLYN Distributions. All rights reserved.

## Tech Stack
- **React 18** with TypeScript
- **Vite** for fast development
- **Tailwind CSS** for styling
- **Google Gemini AI** for chat intelligence
- **Cloudinary** for image hosting
## 🤝 Contributing

## Deployment
This app is optimized for deployment on:
- GitHub Pages
- Vercel
- Netlify
This is a proprietary business application. For issues or feature requests, please contact the development team.

Make sure to set environment variables in your deployment platform's settings.
11 changes: 0 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,6 @@
animation: heroOut 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@^19.2.3",
"react/": "https://esm.sh/react@^19.2.3/",
"@google/genai": "https://esm.sh/@google/genai@^1.37.0",
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"recharts": "https://esm.sh/recharts@^3.6.0"
}
}
</script>
</head>
<body>
<div class="bg-grid"></div>
Expand Down
Loading