Add POZO with dynamic SEO, BAT helper scripts, and deployment guides
|
|
@ -0,0 +1,15 @@
|
||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Backup folders
|
||||||
|
dist_backup/
|
||||||
|
dist_backup_new/
|
||||||
|
dist_working_backup/
|
||||||
|
|
||||||
|
# Zip files
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
# 🚀 POZO - BAT Files Usage Guide
|
||||||
|
|
||||||
|
## 📁 Available BAT Files
|
||||||
|
|
||||||
|
### 🔨 BUILD-PROJECT.bat (RECOMMENDED)
|
||||||
|
**What it does:**
|
||||||
|
- Stops all running servers
|
||||||
|
- Installs npm dependencies if needed
|
||||||
|
- Runs complete build with `npm run convert-jsx-to-html`
|
||||||
|
- Copies server files to dist
|
||||||
|
- Generates SEO files
|
||||||
|
- Verifies all output files
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- First time setup
|
||||||
|
- After making code changes
|
||||||
|
- When you need a fresh, complete build
|
||||||
|
|
||||||
|
**How to use:**
|
||||||
|
1. Double-click `BUILD-PROJECT.bat`
|
||||||
|
2. Wait for completion (1-2 minutes)
|
||||||
|
3. Check for success message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚡ SIMPLE-BUILD.bat (ALTERNATIVE)
|
||||||
|
**What it does:**
|
||||||
|
- Same as BUILD-PROJECT but with better error handling
|
||||||
|
- Skips react-snap (faster)
|
||||||
|
- Still generates SEO files
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- If BUILD-PROJECT.bat fails
|
||||||
|
- When you want faster builds
|
||||||
|
- When react-snap causes issues
|
||||||
|
|
||||||
|
**How to use:**
|
||||||
|
1. Double-click `SIMPLE-BUILD.bat`
|
||||||
|
2. Wait for completion
|
||||||
|
3. Check output messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚀 QUICK-START.bat (FASTEST)
|
||||||
|
**What it does:**
|
||||||
|
- Just starts the server (no build)
|
||||||
|
- Uses existing dist folder
|
||||||
|
- Installs dependencies if missing
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- When you already have a build
|
||||||
|
- Just want to start/restart server
|
||||||
|
- Testing without rebuilding
|
||||||
|
|
||||||
|
**How to use:**
|
||||||
|
1. Make sure you've built at least once
|
||||||
|
2. Double-click `QUICK-START.bat`
|
||||||
|
3. Server starts immediately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ▶️ START-SERVER.bat
|
||||||
|
**What it does:**
|
||||||
|
- Starts the Node.js server from dist folder
|
||||||
|
- Shows all test URLs
|
||||||
|
- Checks if dist/server.js exists
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- After building
|
||||||
|
- When QUICK-START seems too minimal
|
||||||
|
|
||||||
|
**How to use:**
|
||||||
|
1. Ensure build is complete
|
||||||
|
2. Double-click `START-SERVER.bat`
|
||||||
|
3. Browser-ல URLs open பண்ணு
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⏹️ STOP-SERVER.bat
|
||||||
|
**What it does:**
|
||||||
|
- Kills all Node.js processes
|
||||||
|
- Confirms servers stopped
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- When you're done testing
|
||||||
|
- Before starting a new build
|
||||||
|
- When server is stuck
|
||||||
|
|
||||||
|
**How to use:**
|
||||||
|
1. Double-click `STOP-SERVER.bat`
|
||||||
|
2. Wait for confirmation
|
||||||
|
3. Done!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Recommended Workflow
|
||||||
|
|
||||||
|
### First Time / Fresh Setup:
|
||||||
|
```
|
||||||
|
1. BUILD-PROJECT.bat (Build everything)
|
||||||
|
2. START-SERVER.bat (Start server)
|
||||||
|
3. Test in browser (Verify SEO)
|
||||||
|
4. STOP-SERVER.bat (Stop when done)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Regular Development:
|
||||||
|
```
|
||||||
|
1. Make code changes
|
||||||
|
2. STOP-SERVER.bat (Stop old server)
|
||||||
|
3. BUILD-PROJECT.bat (Rebuild)
|
||||||
|
4. START-SERVER.bat (Start new server)
|
||||||
|
5. Test in browser
|
||||||
|
6. STOP-SERVER.bat (Stop when done)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Testing (No Changes):
|
||||||
|
```
|
||||||
|
1. QUICK-START.bat (Just start server)
|
||||||
|
2. Test in browser
|
||||||
|
3. Press Ctrl+C in window (Stop server)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Issue: BUILD-PROJECT.bat fails
|
||||||
|
|
||||||
|
**Solution 1:** Try SIMPLE-BUILD.bat instead
|
||||||
|
```
|
||||||
|
Double-click: SIMPLE-BUILD.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2:** Manual build
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
node scripts/jsx-to-html-converter.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 3:** Check for errors
|
||||||
|
- Missing dependencies → Run `npm install`
|
||||||
|
- Syntax errors in code → Check error messages
|
||||||
|
- Port already in use → Run STOP-SERVER.bat first
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue: START-SERVER.bat shows "dist/server.js not found"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```
|
||||||
|
1. Run BUILD-PROJECT.bat first
|
||||||
|
2. Check if dist folder exists
|
||||||
|
3. Verify dist/server.js is present
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue: Server starts but shows blank page
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```
|
||||||
|
1. Check browser console (F12)
|
||||||
|
2. Verify URL is http://localhost:3000/home/ (with /home/)
|
||||||
|
3. Clear browser cache (Ctrl + Shift + Delete)
|
||||||
|
4. Rebuild: BUILD-PROJECT.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue: Port 3000 already in use
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```
|
||||||
|
1. Run STOP-SERVER.bat
|
||||||
|
2. Or manually kill process:
|
||||||
|
Get-Process -Name node | Stop-Process -Force
|
||||||
|
3. Try starting again
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Issue: SEO tags not showing
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```
|
||||||
|
1. Verify you're viewing page source (Ctrl + U)
|
||||||
|
2. Check if scripts/generate-seo.js ran successfully
|
||||||
|
3. Rebuild with BUILD-PROJECT.bat
|
||||||
|
4. Check dist/home/pricing-pozoapp/index.html manually
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Quick Command Reference
|
||||||
|
|
||||||
|
### To Build:
|
||||||
|
- **Full build:** `BUILD-PROJECT.bat`
|
||||||
|
- **Simple build:** `SIMPLE-BUILD.bat`
|
||||||
|
|
||||||
|
### To Start:
|
||||||
|
- **Normal start:** `START-SERVER.bat`
|
||||||
|
- **Quick start:** `QUICK-START.bat`
|
||||||
|
|
||||||
|
### To Stop:
|
||||||
|
- **Clean stop:** `STOP-SERVER.bat`
|
||||||
|
- **Force stop:** Press `Ctrl+C` in server window
|
||||||
|
|
||||||
|
### To Test:
|
||||||
|
1. Start server (any method above)
|
||||||
|
2. Open: http://localhost:3000/home/
|
||||||
|
3. View source: Press `Ctrl + U`
|
||||||
|
4. Check `<title>` and `<meta>` tags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Success Checklist
|
||||||
|
|
||||||
|
After running BUILD-PROJECT.bat, verify:
|
||||||
|
- [ ] No error messages
|
||||||
|
- [ ] dist/server.js exists
|
||||||
|
- [ ] dist/server/seo-middleware.js exists
|
||||||
|
- [ ] dist/web.config exists
|
||||||
|
- [ ] dist/home/pricing-pozoapp/index.html exists
|
||||||
|
- [ ] dist/home/blog/index.html exists
|
||||||
|
|
||||||
|
After running START-SERVER.bat, verify:
|
||||||
|
- [ ] "Server running on port 3000" message shows
|
||||||
|
- [ ] "Dynamic SEO enabled!" message shows
|
||||||
|
- [ ] http://localhost:3000/home/ opens in browser
|
||||||
|
- [ ] Page shows correct content
|
||||||
|
- [ ] View Source shows SEO tags
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Pro Tips
|
||||||
|
|
||||||
|
1. **Always stop server before building:**
|
||||||
|
- Run STOP-SERVER.bat before BUILD-PROJECT.bat
|
||||||
|
|
||||||
|
2. **Check for success messages:**
|
||||||
|
- Green text = success
|
||||||
|
- Red text = error
|
||||||
|
- Yellow text = warning (usually okay)
|
||||||
|
|
||||||
|
3. **Save time:**
|
||||||
|
- Use QUICK-START.bat if no code changes
|
||||||
|
- Use SIMPLE-BUILD.bat for faster builds
|
||||||
|
|
||||||
|
4. **Verify SEO:**
|
||||||
|
- Always check with Ctrl+U (View Source)
|
||||||
|
- Don't rely on browser inspector (F12)
|
||||||
|
|
||||||
|
5. **Keep terminal open:**
|
||||||
|
- Don't close the server window
|
||||||
|
- It shows useful logs and errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Still Having Issues?
|
||||||
|
|
||||||
|
### Check logs:
|
||||||
|
1. Look at terminal output carefully
|
||||||
|
2. Red error messages show the problem
|
||||||
|
3. Note the step where it fails
|
||||||
|
|
||||||
|
### Common fixes:
|
||||||
|
```powershell
|
||||||
|
# Reinstall dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Clear node_modules and reinstall
|
||||||
|
Remove-Item node_modules -Recurse -Force
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Clear dist and rebuild
|
||||||
|
Remove-Item dist -Recurse -Force
|
||||||
|
BUILD-PROJECT.bat
|
||||||
|
|
||||||
|
# Kill all node processes
|
||||||
|
Get-Process -Name node | Stop-Process -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Created:** 2025-11-04
|
||||||
|
**Version:** 1.0
|
||||||
|
**Project:** POZO - Retail ERP & POS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**படி படியா follow பண்ணா எல்லாம் work ஆகும்! 💪🚀**
|
||||||
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
@echo off
|
||||||
|
REM ======================================
|
||||||
|
REM POZO - Build Project with SEO
|
||||||
|
REM ======================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Building POZO Project...
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Stop any running servers first
|
||||||
|
echo [STEP 1/4] Stopping any running servers...
|
||||||
|
powershell -Command "Get-Process -Name node -ErrorAction SilentlyContinue | Stop-Process -Force"
|
||||||
|
echo Done!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Install dependencies if needed
|
||||||
|
echo [STEP 2/4] Checking dependencies...
|
||||||
|
if not exist "node_modules" (
|
||||||
|
echo Installing dependencies...
|
||||||
|
call npm install
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] npm install failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo Dependencies already installed.
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Build the project
|
||||||
|
echo [STEP 3/4] Building project...
|
||||||
|
echo.
|
||||||
|
echo This will:
|
||||||
|
echo - Build React app with Vite
|
||||||
|
echo - Copy server files to dist
|
||||||
|
echo - Generate SEO files
|
||||||
|
echo.
|
||||||
|
echo Please wait, this may take 1-2 minutes...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
call npm run convert-jsx-to-html
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo [ERROR] Build failed!
|
||||||
|
echo ========================================
|
||||||
|
echo Check the error messages above.
|
||||||
|
echo Common issues:
|
||||||
|
echo - Missing dependencies (run: npm install)
|
||||||
|
echo - Syntax errors in code
|
||||||
|
echo - Out of memory
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
echo [SUCCESS] Build completed!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Verify build output
|
||||||
|
echo [STEP 4/4] Verifying build output...
|
||||||
|
powershell -Command "if (Test-Path 'dist\server.js') { Write-Host ' [OK] dist\server.js' -ForegroundColor Green } else { Write-Host ' [MISSING] dist\server.js' -ForegroundColor Red }"
|
||||||
|
powershell -Command "if (Test-Path 'dist\server\seo-middleware.js') { Write-Host ' [OK] dist\server\seo-middleware.js' -ForegroundColor Green } else { Write-Host ' [MISSING] dist\server\seo-middleware.js' -ForegroundColor Red }"
|
||||||
|
powershell -Command "if (Test-Path 'dist\web.config') { Write-Host ' [OK] dist\web.config' -ForegroundColor Green } else { Write-Host ' [MISSING] dist\web.config' -ForegroundColor Red }"
|
||||||
|
powershell -Command "if (Test-Path 'dist\home\pricing-pozoapp\index.html') { Write-Host ' [OK] dist\home\pricing-pozoapp\index.html' -ForegroundColor Green } else { Write-Host ' [MISSING] dist\home\pricing-pozoapp\index.html' -ForegroundColor Red }"
|
||||||
|
powershell -Command "if (Test-Path 'dist\home\blog\index.html') { Write-Host ' [OK] dist\home\blog\index.html' -ForegroundColor Green } else { Write-Host ' [MISSING] dist\home\blog\index.html' -ForegroundColor Red }"
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Build Complete!
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Next steps:
|
||||||
|
echo 1. Double-click START-SERVER.bat to test locally
|
||||||
|
echo 2. Open http://localhost:3000/home/ in browser
|
||||||
|
echo 3. Verify SEO tags with Ctrl+U (View Source)
|
||||||
|
echo 4. If all looks good, deploy dist folder to server
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
||||||
|
|
@ -0,0 +1,450 @@
|
||||||
|
# 🚀 POZO - Complete Deployment Guide with Dynamic SEO
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Follow EXACTLY in this order. Don't skip any step!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prerequisites Check
|
||||||
|
|
||||||
|
Before starting, make sure you have:
|
||||||
|
- ✅ Node.js installed (check: `node --version`)
|
||||||
|
- ✅ npm installed (check: `npm --version`)
|
||||||
|
- ✅ Access to your IIS server
|
||||||
|
- ✅ iisnode installed on IIS server
|
||||||
|
- ✅ URL Rewrite module installed on IIS server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 PART 1: Build the Project Locally
|
||||||
|
|
||||||
|
### Step 1: Stop any running servers
|
||||||
|
```powershell
|
||||||
|
# Kill all node processes
|
||||||
|
Get-Process -Name node -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Clean previous build (Optional but recommended)
|
||||||
|
```powershell
|
||||||
|
# Remove old dist folder
|
||||||
|
Remove-Item -Path "dist" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Install dependencies
|
||||||
|
```powershell
|
||||||
|
# Make sure all packages are installed
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Build the project with SEO generation
|
||||||
|
```powershell
|
||||||
|
# This runs Vite build + React Snap + SEO generation
|
||||||
|
npm run convert-jsx-to-html
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output:**
|
||||||
|
- ✅ Vite build completes
|
||||||
|
- ✅ React Snap attempts to prerender (may show warnings - ignore them)
|
||||||
|
- ✅ SEO files generated for all pages
|
||||||
|
- ✅ `dist/` folder created with all files
|
||||||
|
- ✅ `dist/server.js` created
|
||||||
|
- ✅ `dist/server/seo-middleware.js` created
|
||||||
|
- ✅ `dist/web.config` created
|
||||||
|
|
||||||
|
### Step 5: Verify build output
|
||||||
|
```powershell
|
||||||
|
# Check if critical files exist
|
||||||
|
Test-Path "dist/server.js"
|
||||||
|
Test-Path "dist/server/seo-middleware.js"
|
||||||
|
Test-Path "dist/web.config"
|
||||||
|
Test-Path "dist/home/pricing-pozoapp/index.html"
|
||||||
|
Test-Path "dist/home/blog/index.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
**All should return:** `True`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 PART 2: Test Locally (MANDATORY!)
|
||||||
|
|
||||||
|
### Step 6: Start local server
|
||||||
|
```powershell
|
||||||
|
# Navigate to dist folder and start server
|
||||||
|
cd dist
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output:**
|
||||||
|
```
|
||||||
|
Server running on port 3000
|
||||||
|
Dynamic SEO enabled!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Test in browser
|
||||||
|
|
||||||
|
Open these URLs in your browser:
|
||||||
|
|
||||||
|
1. **Home:** http://localhost:3000/home/
|
||||||
|
2. **Pricing:** http://localhost:3000/home/pricing-pozoapp
|
||||||
|
3. **Blog:** http://localhost:3000/home/blog
|
||||||
|
4. **Contact:** http://localhost:3000/home/contact-us
|
||||||
|
5. **Sign In:** http://localhost:3000/home/signin
|
||||||
|
|
||||||
|
### Step 8: Verify SEO for each page
|
||||||
|
|
||||||
|
For EACH page above:
|
||||||
|
1. Open the URL in browser
|
||||||
|
2. Press `Ctrl + U` to view page source
|
||||||
|
3. Check `<head>` section contains:
|
||||||
|
- ✅ `<title>` tag with correct page title
|
||||||
|
- ✅ `<meta name="description"` with correct description
|
||||||
|
- ✅ `<meta property="og:title"` - Open Graph title
|
||||||
|
- ✅ `<meta property="og:description"` - OG description
|
||||||
|
- ✅ `<meta property="og:image"` - OG image URL
|
||||||
|
- ✅ `<meta property="og:url"` - Page URL
|
||||||
|
- ✅ `<meta property="twitter:card"` - Twitter card
|
||||||
|
- ✅ `<link rel="canonical"` - Canonical URL
|
||||||
|
|
||||||
|
**IMPORTANT:** If ANY page shows wrong SEO or missing tags, **STOP** and fix before deploying!
|
||||||
|
|
||||||
|
### Step 9: Stop local server
|
||||||
|
```powershell
|
||||||
|
# Press Ctrl+C in the terminal where server is running
|
||||||
|
# OR run this in new terminal:
|
||||||
|
Get-Process -Name node -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 PART 3: Prepare Files for Deployment
|
||||||
|
|
||||||
|
### Step 10: Check dist folder contents
|
||||||
|
|
||||||
|
Your `dist/` folder should have:
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── assets/ (all JS, CSS, images)
|
||||||
|
├── home/
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── blog/
|
||||||
|
│ │ └── index.html
|
||||||
|
│ ├── pricing-pozoapp/
|
||||||
|
│ │ └── index.html
|
||||||
|
│ ├── contact-us/
|
||||||
|
│ │ └── index.html
|
||||||
|
│ └── signin/
|
||||||
|
│ └── index.html
|
||||||
|
├── og/ (Open Graph images)
|
||||||
|
├── server/
|
||||||
|
│ └── seo-middleware.js
|
||||||
|
├── index.html
|
||||||
|
├── server.js (CRITICAL!)
|
||||||
|
├── web.config (CRITICAL!)
|
||||||
|
├── robots.txt
|
||||||
|
├── sitemap.xml
|
||||||
|
└── ... (other files)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 11: Create deployment package
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Go back to project root
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Create a zip file of dist folder (optional, makes upload easier)
|
||||||
|
Compress-Archive -Path "dist\*" -DestinationPath "pozo-deployment.zip" -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 PART 4: Deploy to IIS Server
|
||||||
|
|
||||||
|
### Step 12: Upload files to server
|
||||||
|
|
||||||
|
**Option A: If you have FTP/SFTP access:**
|
||||||
|
1. Connect to your server via FTP/SFTP
|
||||||
|
2. Navigate to your website root folder (e.g., `C:\inetpub\wwwroot\pozo.dev\`)
|
||||||
|
3. **BACKUP existing files first!**
|
||||||
|
4. Delete old files in the folder
|
||||||
|
5. Upload ALL files from `dist/` folder
|
||||||
|
|
||||||
|
**Option B: If you have RDP access:**
|
||||||
|
1. Connect via Remote Desktop
|
||||||
|
2. Copy `dist/` folder or `pozo-deployment.zip` to server
|
||||||
|
3. Extract/move files to website root (e.g., `C:\inetpub\wwwroot\pozo.dev\`)
|
||||||
|
|
||||||
|
### Step 13: Install Node.js dependencies on server
|
||||||
|
|
||||||
|
**On your IIS server (via RDP or SSH):**
|
||||||
|
```powershell
|
||||||
|
# Navigate to your website root
|
||||||
|
cd C:\inetpub\wwwroot\pozo.dev
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install express axios
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 14: Verify critical files on server
|
||||||
|
|
||||||
|
**On server, check these files exist:**
|
||||||
|
```powershell
|
||||||
|
Test-Path "server.js"
|
||||||
|
Test-Path "server\seo-middleware.js"
|
||||||
|
Test-Path "web.config"
|
||||||
|
Test-Path "node_modules\express"
|
||||||
|
Test-Path "node_modules\axios"
|
||||||
|
```
|
||||||
|
|
||||||
|
**All should return:** `True`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ PART 5: Configure IIS
|
||||||
|
|
||||||
|
### Step 15: Verify iisnode is installed
|
||||||
|
|
||||||
|
1. Open IIS Manager
|
||||||
|
2. Select your site
|
||||||
|
3. Check if "iisnode" icon is visible in Features View
|
||||||
|
4. If NOT visible, install iisnode from: https://github.com/Azure/iisnode/releases
|
||||||
|
|
||||||
|
### Step 16: Verify URL Rewrite module
|
||||||
|
|
||||||
|
1. In IIS Manager, select your site
|
||||||
|
2. Check if "URL Rewrite" icon is visible
|
||||||
|
3. If NOT visible, install from: https://www.iis.net/downloads/microsoft/url-rewrite
|
||||||
|
|
||||||
|
### Step 17: Check web.config
|
||||||
|
|
||||||
|
Your `web.config` should look like this:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<system.webServer>
|
||||||
|
<handlers>
|
||||||
|
<add name="iisnode" path="server.js" verb="*" modules="iisnode" resourceType="File" />
|
||||||
|
</handlers>
|
||||||
|
<rewrite>
|
||||||
|
<rules>
|
||||||
|
<!-- Route all requests to Node.js server -->
|
||||||
|
<rule name="pozo.dev" stopProcessing="true">
|
||||||
|
<match url="/*" />
|
||||||
|
<action type="Rewrite" url="server.js" />
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</rewrite>
|
||||||
|
<iisnode node_env="production" />
|
||||||
|
</system.webServer>
|
||||||
|
</configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
**If different, replace with above content.**
|
||||||
|
|
||||||
|
### Step 18: Set proper permissions
|
||||||
|
|
||||||
|
**On server:**
|
||||||
|
1. Right-click on website folder → Properties → Security
|
||||||
|
2. Add `IIS_IUSRS` with Read & Execute permissions
|
||||||
|
3. Add `IUSR` with Read & Execute permissions
|
||||||
|
|
||||||
|
### Step 19: Configure Application Pool
|
||||||
|
|
||||||
|
1. Open IIS Manager
|
||||||
|
2. Go to Application Pools
|
||||||
|
3. Find your site's app pool
|
||||||
|
4. Right-click → Advanced Settings
|
||||||
|
5. Set:
|
||||||
|
- `.NET CLR Version`: No Managed Code
|
||||||
|
- `Enable 32-Bit Applications`: False
|
||||||
|
- `Pipeline Mode`: Integrated
|
||||||
|
|
||||||
|
### Step 20: Restart Application Pool
|
||||||
|
|
||||||
|
1. In IIS Manager → Application Pools
|
||||||
|
2. Right-click your app pool → Stop
|
||||||
|
3. Wait 5 seconds
|
||||||
|
4. Right-click → Start
|
||||||
|
|
||||||
|
### Step 21: Restart IIS (optional but recommended)
|
||||||
|
|
||||||
|
**On server:**
|
||||||
|
```powershell
|
||||||
|
iisreset
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ PART 6: Test Production Site
|
||||||
|
|
||||||
|
### Step 22: Test your live site
|
||||||
|
|
||||||
|
Open these URLs (replace `pozo.dev` with your domain):
|
||||||
|
|
||||||
|
1. **Home:** https://www.pozo.dev/home/
|
||||||
|
2. **Pricing:** https://www.pozo.dev/home/pricing-pozoapp
|
||||||
|
3. **Blog:** https://www.pozo.dev/home/blog
|
||||||
|
4. **Contact:** https://www.pozo.dev/home/contact-us
|
||||||
|
5. **Sign In:** https://www.pozo.dev/home/signin
|
||||||
|
|
||||||
|
### Step 23: Verify SEO on production
|
||||||
|
|
||||||
|
For EACH page:
|
||||||
|
1. Open URL
|
||||||
|
2. Press `Ctrl + U` (View Source)
|
||||||
|
3. Verify ALL SEO tags are present:
|
||||||
|
- ✅ Title tag
|
||||||
|
- ✅ Meta description
|
||||||
|
- ✅ OG tags
|
||||||
|
- ✅ Twitter tags
|
||||||
|
- ✅ Canonical URL
|
||||||
|
|
||||||
|
### Step 24: Test with SEO tools
|
||||||
|
|
||||||
|
1. **Google Rich Results Test:**
|
||||||
|
- Go to: https://search.google.com/test/rich-results
|
||||||
|
- Enter your page URL
|
||||||
|
- Check for errors
|
||||||
|
|
||||||
|
2. **Meta Tags Preview:**
|
||||||
|
- Go to: https://metatags.io/
|
||||||
|
- Enter your page URL
|
||||||
|
- See how it looks on social media
|
||||||
|
|
||||||
|
3. **PageSpeed Insights:**
|
||||||
|
- Go to: https://pagespeed.web.dev/
|
||||||
|
- Enter your page URL
|
||||||
|
- Check performance and SEO score
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 TROUBLESHOOTING
|
||||||
|
|
||||||
|
### Issue 1: Site shows blank page or 404
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```powershell
|
||||||
|
# On server, check iisnode logs
|
||||||
|
type C:\inetpub\wwwroot\pozo.dev\iisnode\*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for errors, usually:
|
||||||
|
- Missing `node_modules` → Run `npm install`
|
||||||
|
- Wrong `web.config` → Replace with correct version
|
||||||
|
|
||||||
|
### Issue 2: SEO tags not showing
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Clear browser cache (Ctrl + Shift + Delete)
|
||||||
|
2. Check if server is actually running Node.js:
|
||||||
|
```powershell
|
||||||
|
Get-Process -Name node
|
||||||
|
```
|
||||||
|
3. Check iisnode is processing requests:
|
||||||
|
- Check `iisnode\*.log` files for activity
|
||||||
|
|
||||||
|
### Issue 3: Assets (CSS/JS) not loading
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check browser console (F12)
|
||||||
|
2. Verify paths in HTML match actual file locations
|
||||||
|
3. Check IIS MIME types for `.js`, `.css`, `.woff`, `.woff2`
|
||||||
|
|
||||||
|
### Issue 4: Server.js not running
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check `web.config` has correct handler
|
||||||
|
2. Verify iisnode is installed
|
||||||
|
3. Check Node.js is installed on server: `node --version`
|
||||||
|
4. Restart App Pool
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 QUICK REFERENCE COMMANDS
|
||||||
|
|
||||||
|
### Local Development:
|
||||||
|
```powershell
|
||||||
|
# Build project
|
||||||
|
npm run convert-jsx-to-html
|
||||||
|
|
||||||
|
# Test locally
|
||||||
|
cd dist
|
||||||
|
node server.js
|
||||||
|
|
||||||
|
# Stop server
|
||||||
|
Get-Process -Name node | Stop-Process -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
### On Server:
|
||||||
|
```powershell
|
||||||
|
# Install dependencies
|
||||||
|
npm install express axios
|
||||||
|
|
||||||
|
# Restart IIS
|
||||||
|
iisreset
|
||||||
|
|
||||||
|
# Check node processes
|
||||||
|
Get-Process -Name node
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
type C:\inetpub\wwwroot\pozo.dev\iisnode\*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 CHECKLIST BEFORE DEPLOYMENT
|
||||||
|
|
||||||
|
- [ ] Local build completed without errors
|
||||||
|
- [ ] Local server tested and SEO verified
|
||||||
|
- [ ] All pages show correct titles and meta tags locally
|
||||||
|
- [ ] `dist/server.js` exists
|
||||||
|
- [ ] `dist/server/seo-middleware.js` exists
|
||||||
|
- [ ] `dist/web.config` exists
|
||||||
|
- [ ] Backed up old production files
|
||||||
|
- [ ] iisnode installed on server
|
||||||
|
- [ ] URL Rewrite module installed on server
|
||||||
|
- [ ] Node.js installed on server
|
||||||
|
- [ ] `npm install` run on server
|
||||||
|
- [ ] Permissions set correctly
|
||||||
|
- [ ] Application pool restarted
|
||||||
|
- [ ] Production site tested
|
||||||
|
- [ ] SEO tags verified on production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ SUCCESS CRITERIA
|
||||||
|
|
||||||
|
Your deployment is successful when:
|
||||||
|
|
||||||
|
1. ✅ All pages load without errors
|
||||||
|
2. ✅ Every page has unique, correct title in browser tab
|
||||||
|
3. ✅ Page source (`Ctrl+U`) shows all SEO meta tags
|
||||||
|
4. ✅ Google Rich Results Test shows no errors
|
||||||
|
5. ✅ Social media preview tools show correct title, description, and image
|
||||||
|
6. ✅ Assets (images, CSS, JS) load properly
|
||||||
|
7. ✅ No console errors in browser (F12)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 SUPPORT
|
||||||
|
|
||||||
|
If you face any issues:
|
||||||
|
|
||||||
|
1. Check the TROUBLESHOOTING section above
|
||||||
|
2. Check server logs: `C:\inetpub\wwwroot\pozo.dev\iisnode\*.log`
|
||||||
|
3. Check browser console (F12) for errors
|
||||||
|
4. Verify all files uploaded correctly
|
||||||
|
5. Confirm Node.js and dependencies installed on server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Created:** 2025-11-04
|
||||||
|
**Version:** 1.0
|
||||||
|
**Project:** POZO - Retail ERP & POS
|
||||||
|
**Tech Stack:** React + Vite + Node.js + Express + IIS + iisnode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**நீ இந்த guide-ஐ படி படியா follow பண்ணா, 100% guarantee SEO work ஆகும்! 🚀**
|
||||||
|
|
||||||
|
**Oru step-um miss பண்ணாதே! எல்லாம் சரியா வரும்! 💪**
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
@echo off
|
||||||
|
REM ======================================
|
||||||
|
REM POZO - Quick Start (Use Existing Build)
|
||||||
|
REM ======================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Quick Start Server
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Check if dist exists
|
||||||
|
if not exist "dist\server.js" (
|
||||||
|
echo [ERROR] No build found!
|
||||||
|
echo Please run BUILD-PROJECT.bat first.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if dependencies are installed
|
||||||
|
if not exist "node_modules\express" (
|
||||||
|
echo [INFO] Installing server dependencies...
|
||||||
|
call npm install express axios
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Failed to install dependencies!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Starting server...
|
||||||
|
echo.
|
||||||
|
echo Server URLs:
|
||||||
|
echo http://localhost:3000/home/
|
||||||
|
echo http://localhost:3000/home/pricing-pozoapp
|
||||||
|
echo http://localhost:3000/home/blog
|
||||||
|
echo.
|
||||||
|
echo Press Ctrl+C to stop
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
node server.js
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Server stopped.
|
||||||
|
pause
|
||||||
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
@echo off
|
||||||
|
REM ======================================
|
||||||
|
REM POZO - Simple Build (No React-Snap)
|
||||||
|
REM ======================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Simple Build Process
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Stop any running servers
|
||||||
|
echo [1/4] Stopping servers...
|
||||||
|
powershell -Command "Get-Process -Name node -ErrorAction SilentlyContinue | Stop-Process -Force"
|
||||||
|
echo Done!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Install dependencies
|
||||||
|
echo [2/4] Installing dependencies...
|
||||||
|
call npm install
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] npm install failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Build with Vite only
|
||||||
|
echo [3/4] Building with Vite...
|
||||||
|
call npm run build
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Vite build failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Copy server files and generate SEO
|
||||||
|
echo [4/4] Setting up server and SEO...
|
||||||
|
node scripts/jsx-to-html-converter.js
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [WARNING] Script had issues, but continuing...
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Verify
|
||||||
|
echo Checking files...
|
||||||
|
if exist "dist\server.js" (
|
||||||
|
echo [OK] dist\server.js
|
||||||
|
) else (
|
||||||
|
echo [MISSING] dist\server.js
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "dist\server\seo-middleware.js" (
|
||||||
|
echo [OK] dist\server\seo-middleware.js
|
||||||
|
) else (
|
||||||
|
echo [MISSING] dist\server\seo-middleware.js
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "dist\web.config" (
|
||||||
|
echo [OK] dist\web.config
|
||||||
|
) else (
|
||||||
|
echo [MISSING] dist\web.config
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Build Complete!
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo To test: Double-click START-SERVER.bat
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
@echo off
|
||||||
|
REM ======================================
|
||||||
|
REM POZO - Start Local Development Server
|
||||||
|
REM ======================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Starting POZO Server...
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Check if dist folder exists
|
||||||
|
if not exist "dist" (
|
||||||
|
echo [ERROR] dist folder not found!
|
||||||
|
echo Please run BUILD-PROJECT.bat first to create the dist folder.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if server.js exists
|
||||||
|
if not exist "dist\server.js" (
|
||||||
|
echo [ERROR] dist\server.js not found!
|
||||||
|
echo Please run BUILD-PROJECT.bat first.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Navigate to dist folder
|
||||||
|
cd dist
|
||||||
|
|
||||||
|
echo Starting Node.js server on port 3000...
|
||||||
|
echo.
|
||||||
|
echo Server will be available at:
|
||||||
|
echo http://localhost:3000/home/
|
||||||
|
echo http://localhost:3000/home/pricing-pozoapp
|
||||||
|
echo http://localhost:3000/home/blog
|
||||||
|
echo.
|
||||||
|
echo Press Ctrl+C to stop the server
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Start the server
|
||||||
|
node server.js
|
||||||
|
|
||||||
|
REM If server stops unexpectedly
|
||||||
|
echo.
|
||||||
|
echo [INFO] Server stopped.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
@echo off
|
||||||
|
REM ======================================
|
||||||
|
REM POZO - Stop Local Development Server
|
||||||
|
REM ======================================
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Stopping POZO Server...
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Kill all node processes
|
||||||
|
powershell -Command "Get-Process -Name node -ErrorAction SilentlyContinue | Stop-Process -Force"
|
||||||
|
|
||||||
|
REM Check if any node processes are still running
|
||||||
|
powershell -Command "$processes = Get-Process -Name node -ErrorAction SilentlyContinue; if ($processes) { Write-Host '[WARNING] Some node processes are still running!' -ForegroundColor Yellow } else { Write-Host '[SUCCESS] All servers stopped successfully!' -ForegroundColor Green }"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Done!
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// AuthContext.jsx
|
||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const AuthCtx = createContext(null);
|
||||||
|
export const useAuth = () => useContext(AuthCtx);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }) {
|
||||||
|
const [hasAuthorizedSession, setHasAuthorizedSession] = useState(false);
|
||||||
|
const [isAuthResolved, setIsAuthResolved] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// simulate or implement real check (localStorage/cookie/ping)
|
||||||
|
const token = localStorage.getItem("pozo_token");
|
||||||
|
setHasAuthorizedSession(!!token);
|
||||||
|
} finally {
|
||||||
|
setIsAuthResolved(true);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthCtx.Provider value={{ hasAuthorizedSession, isAuthResolved }}>
|
||||||
|
{children}
|
||||||
|
</AuthCtx.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🚀 Starting SEO-optimized build..."
|
||||||
|
|
||||||
|
# Clean previous build
|
||||||
|
rm -rf dist/
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Prerender pages and apply SEO meta
|
||||||
|
npm run convert-jsx-to-html
|
||||||
|
|
||||||
|
echo "✅ Build completed with SEO optimization!"
|
||||||
|
echo "📁 Deploy the 'dist' folder to your server"
|
||||||
|
echo "🔍 Don't forget to:"
|
||||||
|
echo " 1. Replace G-XXXXXXXXXX with your Google Analytics ID"
|
||||||
|
echo " 2. Replace YOUR_GSC_VERIFICATION_CODE with your Search Console code"
|
||||||
|
echo " 3. Submit sitemap to Google Search Console"
|
||||||
|
echo " 4. Test with PageSpeed Insights"
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.cdnfonts.com/css/wasted-vindey" rel="stylesheet">
|
||||||
|
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,400;0,500;0,600;0,700;1,200&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Manrope:wght@200;300;400;500;600;700;800&family=Montserrat:wght@400;500;600;700&family=Poppins:ital,wght@1,200&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
|
<!-- <link href="https://vjs.zencdn.net/8.3.0/video-js.css" rel="stylesheet" /> -->
|
||||||
|
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="./src/Images/PozoAppFavicon.png" type="image/x-icon">
|
||||||
|
<!-- <title>PozoApp - Complete Business Management Solution | POS & Inventory</title> -->
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<meta name="description" content="PozoApp delivers AI-powered POS and SaaS solutions for MSMEs and enterprises. Complete business management with billing, inventory, and analytics.">
|
||||||
|
<meta name="keywords" content="POS software, retail ERP, billing software, inventory management">
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://www.pozo.app/">
|
||||||
|
<meta property="og:title" content="PozoApp - Complete Business Management Solution | POS & Inventory">
|
||||||
|
<meta property="og:description" content="PozoApp delivers AI-powered POS and SaaS solutions for MSMEs and enterprises.">
|
||||||
|
<meta property="og:image" content="https://www.pozo.app/og/home.jpg">
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image">
|
||||||
|
<meta property="twitter:url" content="https://www.pozo.app/">
|
||||||
|
<meta property="twitter:title" content="PozoApp - Complete Business Management Solution | POS & Inventory">
|
||||||
|
<meta property="twitter:description" content="PozoApp delivers AI-powered POS and SaaS solutions for MSMEs and enterprises.">
|
||||||
|
<meta property="twitter:image" content="https://www.pozo.app/og/home.jpg">
|
||||||
|
|
||||||
|
<link rel="canonical" href="https://www.pozo.app/">
|
||||||
|
|
||||||
|
<!-- mohan -->
|
||||||
|
<!-- <script>
|
||||||
|
// Disable right-click menu and common hotkeys
|
||||||
|
document.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (
|
||||||
|
(event.ctrlKey && event.shiftKey && ["I", "J", "C"].includes(event.key.toUpperCase())) ||
|
||||||
|
(event.ctrlKey && ["U", "S"].includes(event.key.toUpperCase())) ||
|
||||||
|
event.key === "F12"
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
<!-- <script src="https://vjs.zencdn.net/8.3.0/video.min.js"></script> -->
|
||||||
|
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||||
|
<!-- Start of LiveChat (www.livechat.com) code -->
|
||||||
|
<!-- <script>
|
||||||
|
window.__lc = window.__lc || {};
|
||||||
|
window.__lc.license = 18861501;
|
||||||
|
window.__lc.integration_name = "manual_onboarding";
|
||||||
|
window.__lc.product_name = "livechat";
|
||||||
|
;(function(n,t,c){function i(n){return e._h?e._h.apply(null,n):e._q.push(n)}var e={_q:[],_h:null,_v:"2.0",on:function(){i(["on",c.call(arguments)])},once:function(){i(["once",c.call(arguments)])},off:function(){i(["off",c.call(arguments)])},get:function(){if(!e._h)throw new Error("[LiveChatWidget] You can't use getters before load.");return i(["get",c.call(arguments)])},call:function(){i(["call",c.call(arguments)])},init:function(){var n=t.createElement("script");n.async=!0,n.type="text/javascript",n.src="https://cdn.livechatinc.com/tracking.js",t.head.appendChild(n)}};!n.__lc.asyncInit&&e.init(),n.LiveChatWidget=n.LiveChatWidget||e}(window,document,[].slice))
|
||||||
|
</script> -->
|
||||||
|
<noscript><a href="https://www.livechat.com/chat-with/18861501/" rel="nofollow">Chat with us</a>, powered by <a
|
||||||
|
href="https://www.livechat.com/?welcome" rel="noopener nofollow" target="_blank">LiveChat</a></noscript>
|
||||||
|
<!-- End of LiveChat code -->
|
||||||
|
|
||||||
|
</body>
|
||||||
|
<iframe id="ifmcontentstoprint" style="
|
||||||
|
height: 0px;
|
||||||
|
width: 0px;
|
||||||
|
position: absolute;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
"></iframe>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"name": "pozo",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"server": "node server/server.js",
|
||||||
|
"start": "npm run build && npm run server",
|
||||||
|
"deploy": "bash deploy.sh",
|
||||||
|
"seo-test": "echo 'Test your site at: https://pagespeed.web.dev/ and https://search.google.com/test/rich-results'",
|
||||||
|
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"convert-jsx-to-html": "node scripts/jsx-to-html-converter.js"
|
||||||
|
},
|
||||||
|
"reactSnap": {
|
||||||
|
"source": "dist",
|
||||||
|
"inlineCss": false,
|
||||||
|
"puppeteerArgs": [
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-setuid-sandbox",
|
||||||
|
"--disable-dev-shm-usage"
|
||||||
|
],
|
||||||
|
"skipThirdPartyRequests": true,
|
||||||
|
"waitFor": 3000,
|
||||||
|
"include": ["/home/", "/home/blog", "/home/signin", "/home/contact-us", "/home/pricing-pozoapp"],
|
||||||
|
"minifyHtml": {
|
||||||
|
"collapseWhitespace": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"removeEmptyAttributes": true
|
||||||
|
},
|
||||||
|
"crawl": true,
|
||||||
|
"userAgent": "ReactSnap",
|
||||||
|
"routes": [
|
||||||
|
"/home/",
|
||||||
|
"/home/signin",
|
||||||
|
"/home/blog",
|
||||||
|
"/home/contact-us",
|
||||||
|
"/home/pricing-pozoapp",
|
||||||
|
"/home/live-Session"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@editorjs/code": "^2.9.3",
|
||||||
|
"@editorjs/editorjs": "^2.31.0",
|
||||||
|
"@editorjs/embed": "^2.7.6",
|
||||||
|
"@editorjs/header": "^2.8.8",
|
||||||
|
"@editorjs/image": "^2.10.3",
|
||||||
|
"@editorjs/link": "^2.6.2",
|
||||||
|
"@editorjs/list": "^2.0.8",
|
||||||
|
"@editorjs/paragraph": "^2.11.7",
|
||||||
|
"@editorjs/quote": "^2.7.6",
|
||||||
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
|
"@studio-freight/lenis": "^1.0.42",
|
||||||
|
"@weekwood/editorjs-video": "^1.0.2",
|
||||||
|
"antd": "^5.4.4",
|
||||||
|
"aos": "^2.3.4",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"bowser": "^2.11.0",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"devtools-detect": "^4.0.2",
|
||||||
|
"exceljs": "^4.3.0",
|
||||||
|
"framer-motion": "^12.17.0",
|
||||||
|
"google-maps-react": "^2.0.6",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"html2pdf.js": "^0.10.3",
|
||||||
|
"informed": "^4.44.1",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"jspdf": "^2.5.1",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-color": "^2.19.3",
|
||||||
|
"react-device-detect": "^2.2.3",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-excel-renderer": "^1.1.0",
|
||||||
|
"react-helmet-async": "^2.0.5",
|
||||||
|
"react-icons": "^4.8.0",
|
||||||
|
"react-qr-code": "^2.0.15",
|
||||||
|
"react-redux": "^8.0.5",
|
||||||
|
"react-router-dom": "^6.11.0",
|
||||||
|
"react-slick": "^0.29.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"sass": "^1.62.0",
|
||||||
|
"slick-carousel": "^1.8.1",
|
||||||
|
"split-type": "^0.3.4",
|
||||||
|
"swiper": "^10.2.0",
|
||||||
|
"universal-cookie": "^4.0.4",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
|
"webfontloader": "^1.6.28",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0-beta.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"react-snap": "^1.23.0",
|
||||||
|
"vite": "^4.3.0",
|
||||||
|
"vite-plugin-prerender": "^1.0.8",
|
||||||
|
"vite-plugin-sitemap": "^0.8.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Content-Type-Options nosniff
|
||||||
|
Header always set X-Frame-Options DENY
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Cache control
|
||||||
|
<filesMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg)$">
|
||||||
|
Header set Cache-Control "max-age=31536000, public"
|
||||||
|
</filesMatch>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<users>
|
||||||
|
<user>YOUR_BING_VERIFICATION_CODE</user>
|
||||||
|
</users>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Ads.txt file for PozoApp
|
||||||
|
# This file is used to authorize digital ad sellers
|
||||||
|
# Add your ad network entries here when you start monetizing
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/src/Images/PozoAppFavicon.png"/>
|
||||||
|
<TileColor>#1677ff</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Google Analytics 4 Configuration
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
// Replace with your actual GA4 Measurement ID
|
||||||
|
gtag('config', 'G-MQBWL7JVJ0', {
|
||||||
|
page_title: document.title,
|
||||||
|
page_location: window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track custom events
|
||||||
|
export const trackEvent = (eventName, parameters = {}) => {
|
||||||
|
gtag('event', eventName, parameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track page views
|
||||||
|
export const trackPageView = (page_path) => {
|
||||||
|
gtag('config', 'G-MQBWL7JVJ0', {
|
||||||
|
page_path: page_path
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="google-site-verification" content="YOUR_VERIFICATION_CODE_HERE" />
|
||||||
|
<title>Google Site Verification</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Google site verification page</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "PozoApp - Business Management Solution",
|
||||||
|
"short_name": "PozoApp",
|
||||||
|
"description": "AI-powered POS and SaaS solutions for businesses",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#1677ff",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/src/Images/PozoAppFavicon.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 676 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
|
@ -0,0 +1,33 @@
|
||||||
|
User-agent: *
|
||||||
|
|
||||||
|
# Allow top-level site content and important pages
|
||||||
|
Allow: /
|
||||||
|
Allow: /features
|
||||||
|
Allow: /pricing
|
||||||
|
Allow: /contact-us
|
||||||
|
Allow: /blog
|
||||||
|
|
||||||
|
# Allow common public asset folders
|
||||||
|
Allow: /assets/
|
||||||
|
Allow: /images/
|
||||||
|
Allow: /css/
|
||||||
|
Allow: /js/
|
||||||
|
|
||||||
|
# Disallow admin, build and development files
|
||||||
|
Disallow: /admin
|
||||||
|
Disallow: /AdminPanel
|
||||||
|
Disallow: /*.env
|
||||||
|
Disallow: /src/
|
||||||
|
Disallow: /dist/
|
||||||
|
Disallow: /node_modules/
|
||||||
|
|
||||||
|
# Sitemap location (replace if your production domain differs)
|
||||||
|
Sitemap: https://www.pozo.app/sitemap.xml
|
||||||
|
|
||||||
|
# Crawl delay (optional)
|
||||||
|
Crawl-delay: 1
|
||||||
|
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# - This file is served from /public when using Vite. Ensure production deploy serves this file at https://your-domain/robots.txt
|
||||||
|
# - Robots rules are advisory; protect sensitive routes server-side as well.
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/signin</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/pricing-pozoapp</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/contact-us</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/blog</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/live-Session</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/features</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://www.pozo.app/home/about</loc>
|
||||||
|
<lastmod>2024-10-28</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
|
After Width: | Height: | Size: 5.2 KiB |
|
|
@ -0,0 +1,21 @@
|
||||||
|
const CACHE_NAME = 'pozoapp-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/static/css/main.css',
|
||||||
|
'/static/js/main.js',
|
||||||
|
'/og/home.jpg'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(cache => cache.addAll(urlsToCache))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => response || fetch(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<system.webServer>
|
||||||
|
<handlers>
|
||||||
|
<add name="iisnode" path="server.js" verb="*" modules="iisnode" resourceType="File" />
|
||||||
|
</handlers>
|
||||||
|
<rewrite>
|
||||||
|
<rules>
|
||||||
|
<!-- Route all requests to Node.js server -->
|
||||||
|
<rule name="pozo.dev" stopProcessing="true">
|
||||||
|
<match url="/*" />
|
||||||
|
<action type="Rewrite" url="server.js" />
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</rewrite>
|
||||||
|
<iisnode node_env="production" />
|
||||||
|
</system.webServer>
|
||||||
|
</configuration>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
module.exports = {
|
||||||
|
routes: [
|
||||||
|
"/home/",
|
||||||
|
"/home/signin",
|
||||||
|
"/home/signup",
|
||||||
|
"/home/dashboard",
|
||||||
|
"/home/features",
|
||||||
|
"/home/pricing",
|
||||||
|
"/home/contact-us",
|
||||||
|
"/home/blog"
|
||||||
|
],
|
||||||
|
inlineCss: true,
|
||||||
|
puppeteerArgs: ["--no-sandbox"],
|
||||||
|
skipThirdPartyRequests: true,
|
||||||
|
minifyHtml: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeComments: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import axios from "axios";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
// For production deployment, always use the production URL
|
||||||
|
// When running convert-jsx-to-html, always use production URLs for SEO
|
||||||
|
const mainDirectory = (import.meta?.env?.ENV_MAIN_BASE_URL)
|
||||||
|
|| process.env.ENV_MAIN_BASE_URL
|
||||||
|
|| 'https://www.pozo.app/home';
|
||||||
|
const rawApiUrl = (import.meta?.env?.ENV_API_URL) || process.env.ENV_API_URL || '';
|
||||||
|
|
||||||
|
function normalizeApiBase(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
// If full http/https, use as-is
|
||||||
|
if (/^https?:\/\//i.test(url)) return url;
|
||||||
|
// If protocol-relative (//domain), prefix https:
|
||||||
|
if (/^\/\//.test(url)) return `https:${url}`;
|
||||||
|
// Otherwise, assume domain/path and prefix https://
|
||||||
|
return `https://${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommonApiUrl = normalizeApiBase(rawApiUrl).replace(/\/$/, '');
|
||||||
|
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: "home/index.html", pageId: 1, url: `${mainDirectory}/`},
|
||||||
|
{
|
||||||
|
path: "home/signin/index.html",
|
||||||
|
pageId: 2,
|
||||||
|
url: `${mainDirectory}/signin`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "home/pricing-pozoapp/index.html",
|
||||||
|
pageId: 3,
|
||||||
|
url: `${mainDirectory}/pricing-pozoapp`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "home/contact-us/index.html",
|
||||||
|
pageId: 4,
|
||||||
|
url: `${mainDirectory}/contact-us`,
|
||||||
|
},
|
||||||
|
{ path: "home/blog/index.html", pageId: 5, url: `${mainDirectory}/blog` },
|
||||||
|
];
|
||||||
|
|
||||||
|
const fallbackData = {
|
||||||
|
1: {
|
||||||
|
title: "Retail ERP & POS for Indian MSMEs | POZO",
|
||||||
|
description: "Fast billing, smart inventory, GST-ready POS. POZO helps kirana, mini-supermarkets & retail chains speed checkout, connect weighing scales, and manage multi-store ops.",
|
||||||
|
image: `${mainDirectory}/og/home.jpg`,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
title: "Sign In to PozoApp",
|
||||||
|
description: "Access your business dashboard",
|
||||||
|
image: `${mainDirectory}/og/Signin-og.jpg`,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
title: "Pricing - Retail ERP & POS Plans | POZO",
|
||||||
|
description: "Simple plans for MSMEs. Fast billing, inventory, GST e-invoice, weighing-scale integration, WhatsApp e-bills & multi-store controls. Book a demo.",
|
||||||
|
image: `${mainDirectory}/og/pricing-og.jpg`,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
title: "Contact PozoApp",
|
||||||
|
description: "Get support and sales information",
|
||||||
|
image: `${mainDirectory}/og/contact-og.jpg`,
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
title: "POZO Blog — Retail ERP, POS & Grocery Billing Guides",
|
||||||
|
description: "Practical guides on POS billing, weighing-scale integration, GST e-invoices, multi-store ERP & inventory control for Indian retailers.",
|
||||||
|
image: `${mainDirectory}/og/blog-og.jpg`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchSEO(pageId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CommonApiUrl}/Seo`,
|
||||||
|
{ params: { pageId } }
|
||||||
|
);
|
||||||
|
if (response.data?.statusCode === 1 && response.data?.data?.length > 0) {
|
||||||
|
return response.data.data[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Using fallback for PageId ${pageId}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateSEOFiles() {
|
||||||
|
for (const route of routes) {
|
||||||
|
const dbData = await fetchSEO(route.pageId);
|
||||||
|
const fallback = fallbackData[route.pageId];
|
||||||
|
|
||||||
|
const seoData = {
|
||||||
|
title: dbData?.MetaTitle || fallback.title,
|
||||||
|
description: dbData?.MetaDesc || fallback.description,
|
||||||
|
image: dbData?.ImageUrl || fallback.image,
|
||||||
|
url: route.url,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the prerendered HTML file for this specific route
|
||||||
|
const filePath = path.join(__dirname, "../dist", route.path);
|
||||||
|
let html;
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
// If prerendered file exists, read it
|
||||||
|
html = fs.readFileSync(filePath, "utf8");
|
||||||
|
} else {
|
||||||
|
// Fallback to base index.html if prerendered file doesn't exist
|
||||||
|
html = fs.readFileSync(
|
||||||
|
path.join(__dirname, "../dist/index.html"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace SEO tags in the HTML
|
||||||
|
html = html
|
||||||
|
// First uncomment commented title tags
|
||||||
|
.replace(/<!--\s*<title>[^<]*<\/title>\s*-->/, `<title>${seoData.title}</title>`)
|
||||||
|
// Then replace any existing title tags
|
||||||
|
.replace(/<title>[^<]*<\/title>/, `<title>${seoData.title}</title>`)
|
||||||
|
.replace(
|
||||||
|
/<meta property="og:title" content="[^"]*">/,
|
||||||
|
`<meta property="og:title" content="${seoData.title}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="og:description" content="[^"]*">/,
|
||||||
|
`<meta property="og:description" content="${seoData.description}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="og:image" content="[^"]*">/,
|
||||||
|
`<meta property="og:image" content="${seoData.image}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="og:url" content="[^"]*">/,
|
||||||
|
`<meta property="og:url" content="${seoData.url}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta name="description" content="[^"]*">/,
|
||||||
|
`<meta name="description" content="${seoData.description}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="twitter:title" content="[^"]*">/,
|
||||||
|
`<meta property="twitter:title" content="${seoData.title}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="twitter:description" content="[^"]*">/,
|
||||||
|
`<meta property="twitter:description" content="${seoData.description}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="twitter:image" content="[^"]*">/,
|
||||||
|
`<meta property="twitter:image" content="${seoData.image}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<meta property="twitter:url" content="[^"]*">/,
|
||||||
|
`<meta property="twitter:url" content="${seoData.url}">`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<link rel="canonical" href="[^"]*">/,
|
||||||
|
`<link rel="canonical" href="${seoData.url}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the updated HTML back to the file
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, html);
|
||||||
|
console.log(`✓ Generated ${route.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✓ All SEO files generated successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSEOFiles().catch(console.error);
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync, spawn } from 'child_process';
|
||||||
|
import { setTimeout } from 'timers/promises';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const isWin = process.platform === 'win32';
|
||||||
|
const npmCmd = isWin ? 'npm.cmd' : 'npm';
|
||||||
|
const npxCmd = isWin ? 'npx.cmd' : 'npx';
|
||||||
|
|
||||||
|
if (!process.env.SKIP_BUILD) {
|
||||||
|
console.log('Building the React app...');
|
||||||
|
try {
|
||||||
|
execSync(`${npmCmd} run build`, { stdio: 'inherit', shell: isWin });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Build failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Skipping build (SKIP_BUILD=1)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy server.js and server folder to dist for iisnode
|
||||||
|
console.log('Copying server files to dist...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create dist-specific server.js for iisnode (serves from __dirname, not dist subfolder)
|
||||||
|
const distServerJs = `import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import seoMiddleware from './server/seo-middleware.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Serve static files - __dirname is already dist/
|
||||||
|
app.use(express.static(__dirname));
|
||||||
|
// Also mount under /home because Vite base is /home/
|
||||||
|
app.use('/home', express.static(__dirname));
|
||||||
|
|
||||||
|
// Apply SEO middleware for HTML routes
|
||||||
|
app.use(seoMiddleware);
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(\`Server running on port \${PORT}\`);
|
||||||
|
console.log('Dynamic SEO enabled!');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join('dist', 'server.js'), distServerJs, 'utf8');
|
||||||
|
|
||||||
|
// Copy server folder to dist
|
||||||
|
if (!fs.existsSync(path.join('dist', 'server'))) {
|
||||||
|
fs.mkdirSync(path.join('dist', 'server'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy server.js from server folder (if it exists)
|
||||||
|
if (fs.existsSync(path.join('server', 'server.js'))) {
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join('server', 'server.js'),
|
||||||
|
path.join('dist', 'server', 'server.js')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create dist-specific seo-middleware.js with correct paths
|
||||||
|
const seoMiddlewareJs = fs.readFileSync(path.join('server', 'seo-middleware.js'), 'utf8');
|
||||||
|
// Replace the path from ../dist to .. for dist deployment (handle all variations)
|
||||||
|
const distSeoMiddlewareJs = seoMiddlewareJs
|
||||||
|
.replace(/path\.join\(__dirname, '\.\.\/dist'/g, "path.join(__dirname, '..'")
|
||||||
|
.replace(/path\.join\(__dirname, '\.\.', 'dist'/g, "path.join(__dirname, '..'")
|
||||||
|
.replace(/path\.join\(__dirname, '\.\.\/dist',/g, "path.join(__dirname, '..',")
|
||||||
|
.replace(/path\.join\(__dirname, '\.\.\/dist"/g, "path.join(__dirname, '..\"")
|
||||||
|
.replace(/path\.join\(__dirname, '\.\.\/dist',\s*req\.path/g, "path.join(__dirname, '..', req.path");
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join('dist', 'server', 'seo-middleware.js'), distSeoMiddlewareJs, 'utf8');
|
||||||
|
|
||||||
|
console.log('Server files copied successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy server files:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.PRERENDER === '1') {
|
||||||
|
let server;
|
||||||
|
try {
|
||||||
|
console.log('Starting development server for prerender...');
|
||||||
|
server = spawn(npmCmd, ['run', 'dev'], { stdio: 'inherit', shell: isWin });
|
||||||
|
// Wait for server to start
|
||||||
|
await setTimeout(5000);
|
||||||
|
|
||||||
|
console.log('Prerendering pages with react-snap...');
|
||||||
|
execSync(`${npxCmd} react-snap`, { stdio: 'inherit', shell: isWin });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Prerendering skipped due to error (continuing to SEO):', error?.message || error);
|
||||||
|
} finally {
|
||||||
|
if (server) {
|
||||||
|
console.log('Stopping development server...');
|
||||||
|
server.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Skipping react-snap prerender (set PRERENDER=1 to enable)');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Applying SEO optimizations...');
|
||||||
|
try {
|
||||||
|
execSync('node scripts/generate-seo.js', { stdio: 'inherit', shell: isWin });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SEO application failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('JSX to HTML conversion completed successfully!');
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Function to update index.html with dynamic SEO data
|
||||||
|
async function updateIndexHTML(seoData) {
|
||||||
|
const indexPath = path.join(__dirname, '../dist/index.html');
|
||||||
|
let html = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
|
||||||
|
// Replace meta tags with dynamic data
|
||||||
|
html = html.replace(
|
||||||
|
/<meta name="description" content="[^"]*">/,
|
||||||
|
`<meta name="description" content="${seoData.metaDescription}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
html = html.replace(
|
||||||
|
/<meta property="og:title" content="[^"]*">/,
|
||||||
|
`<meta property="og:title" content="${seoData.metaTitle}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
html = html.replace(
|
||||||
|
/<meta property="og:description" content="[^"]*">/,
|
||||||
|
`<meta property="og:description" content="${seoData.metaDescription}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
html = html.replace(
|
||||||
|
/<meta name="keywords" content="[^"]*">/,
|
||||||
|
`<meta name="keywords" content="${seoData.keywords}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
html = html.replace(
|
||||||
|
/<title>[^<]*<\/title>/,
|
||||||
|
`<title>${seoData.metaTitle}</title>`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write updated HTML
|
||||||
|
fs.writeFileSync(indexPath, html);
|
||||||
|
console.log('SEO updated successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { updateIndexHTML };
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import seoMiddleware from './server/seo-middleware.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Serve static files from dist folder
|
||||||
|
app.use(express.static(path.join(__dirname, 'dist')));
|
||||||
|
|
||||||
|
// Apply SEO middleware for HTML routes
|
||||||
|
app.use(seoMiddleware);
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
console.log('Dynamic SEO enabled!');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
// Express middleware to inject dynamic SEO meta tags
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const seoMiddleware = async (req, res, next) => {
|
||||||
|
// Skip non-HTML requests and static assets (only skip if it has a file extension that's not .html)
|
||||||
|
if (req.path.includes('.') && !req.path.endsWith('.html') && !req.path.endsWith('/')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if it's an API route or static asset
|
||||||
|
if (req.path.startsWith('/api/') || req.path.startsWith('/assets/') || req.path.startsWith('/home/assets/')) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('SEO Middleware triggered for:', req.path);
|
||||||
|
|
||||||
|
const normalizedPath = normalizePath(mapToHomeNamespace(req.path));
|
||||||
|
|
||||||
|
// Attempt dynamic SEO fetch by path/slug first, fallback to pageId map
|
||||||
|
const routeToPageId = {
|
||||||
|
'/home/': 1,
|
||||||
|
'/home/blog': 5,
|
||||||
|
'/home/blog/': 5,
|
||||||
|
'/home/pricing-pozoapp': 3,
|
||||||
|
'/home/pricing-pozoapp/': 3,
|
||||||
|
'/home/contact-us': 4,
|
||||||
|
'/home/contact-us/': 4,
|
||||||
|
'/home/signin': 2,
|
||||||
|
'/home/signin/': 2
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageId = routeToPageId[normalizedPath] || 1;
|
||||||
|
|
||||||
|
const fullUrl = buildFullUrl(req);
|
||||||
|
|
||||||
|
const dynamicSeo = await fetchSEOByPathOrSlug(normalizedPath);
|
||||||
|
let dbSeoData = dynamicSeo || (await fetchSEOFromDB(pageId));
|
||||||
|
|
||||||
|
const fallbackData = {
|
||||||
|
1: { title: 'Retail ERP & POS for Indian MSMEs | POZO', description: 'Fast billing, smart inventory, GST-ready POS. POZO helps kirana, mini-supermarkets & retail chains speed checkout, connect weighing scales, and manage multi-store ops.', image: 'https://www.pozo.app/og/home.jpg', url: fullUrl },
|
||||||
|
2: { title: 'Sign In to PozoApp', description: 'Access your business dashboard', image: 'https://www.pozo.app/og/Signin-og.jpg', url: fullUrl },
|
||||||
|
3: { title: 'Pricing - Retail ERP & POS Plans | POZO', description: 'Simple plans for MSMEs. Fast billing, inventory, GST e-invoice, weighing-scale integration, WhatsApp e-bills & multi-store controls. Book a demo.', image: 'https://www.pozo.app/og/pricing-og.jpg', url: fullUrl },
|
||||||
|
4: { title: 'Contact PozoApp', description: 'Get support and sales information', image: 'https://www.pozo.app/og/contact-og.jpg', url: fullUrl },
|
||||||
|
5: { title: 'POZO Blog — Retail ERP, POS & Grocery Billing Guides', description: 'Practical guides on POS billing, weighing-scale integration, GST e-invoices, multi-store ERP & inventory control for Indian retailers.', image: 'https://www.pozo.app/og/blog-og.jpg', url: fullUrl }
|
||||||
|
};
|
||||||
|
|
||||||
|
const chosenFallback = fallbackData[routeToPageId[normalizedPath]] || fallbackData[1];
|
||||||
|
const seoData = dbSeoData ? {
|
||||||
|
title: dbSeoData.MetaTitle || dbSeoData.title || chosenFallback.title,
|
||||||
|
description: dbSeoData.MetaDesc || dbSeoData.description || chosenFallback.description,
|
||||||
|
image: dbSeoData.ImageUrl || dbSeoData.image || chosenFallback.image,
|
||||||
|
url: dbSeoData.CanonicalUrl || dbSeoData.url || fullUrl
|
||||||
|
} : chosenFallback;
|
||||||
|
|
||||||
|
// Read HTML for route or fallback to index.html
|
||||||
|
// For dist deployment: __dirname is dist/server/, so go up to dist/
|
||||||
|
let htmlPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
normalizedPath.endsWith('/') ? normalizedPath.slice(0, -1) : normalizedPath,
|
||||||
|
'index.html'
|
||||||
|
);
|
||||||
|
if (!fs.existsSync(htmlPath)) {
|
||||||
|
htmlPath = path.join(__dirname, '..', 'index.html');
|
||||||
|
}
|
||||||
|
let html = fs.readFileSync(htmlPath, 'utf8');
|
||||||
|
|
||||||
|
// Ensure <head> exists
|
||||||
|
if (!/<head[\s\S]*<\/head>/i.test(html)) {
|
||||||
|
html = html.replace(/<html[^>]*>/i, match => `${match}\n<head></head>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
|
html = replaceOrInsert(html, /<title>[^<]*<\/title>/i, `<title>${escapeHtml(seoData.title)}</title>`);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
html = replaceOrInsert(
|
||||||
|
html,
|
||||||
|
/<meta\s+name="description"[^>]*>/i,
|
||||||
|
`<meta name="description" content="${escapeHtml(seoData.description)}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Canonical
|
||||||
|
html = replaceOrInsert(
|
||||||
|
html,
|
||||||
|
/<link\s+rel="canonical"[^>]*>/i,
|
||||||
|
`<link rel="canonical" href="${escapeAttribute(seoData.url)}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open Graph
|
||||||
|
html = replaceOrInsert(html, /<meta\s+property="og:title"[^>]*>/i, `<meta property="og:title" content="${escapeAttribute(seoData.title)}">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+property="og:description"[^>]*>/i, `<meta property="og:description" content="${escapeAttribute(seoData.description)}">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+property="og:image"[^>]*>/i, `<meta property="og:image" content="${escapeAttribute(seoData.image)}">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+property="og:url"[^>]*>/i, `<meta property="og:url" content="${escapeAttribute(seoData.url)}">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+property="og:type"[^>]*>/i, `<meta property="og:type" content="article">`);
|
||||||
|
|
||||||
|
// Twitter Cards
|
||||||
|
html = replaceOrInsert(html, /<meta\s+name="twitter:card"[^>]*>/i, `<meta name="twitter:card" content="summary_large_image">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+name="twitter:title"[^>]*>/i, `<meta name="twitter:title" content="${escapeAttribute(seoData.title)}">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+name="twitter:description"[^>]*>/i, `<meta name="twitter:description" content="${escapeAttribute(seoData.description)}">`);
|
||||||
|
html = replaceOrInsert(html, /<meta\s+name="twitter:image"[^>]*>/i, `<meta name="twitter:image" content="${escapeAttribute(seoData.image)}">`);
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SEO middleware error:', error);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to fetch SEO data from your database
|
||||||
|
async function fetchSEOFromDB(pageId) {
|
||||||
|
try {
|
||||||
|
const API_URL = process.env.API_URL || 'https://api.pozo.app';
|
||||||
|
const response = await axios.get(`${API_URL}/Seo?PageId=${pageId}`);
|
||||||
|
if (response.data?.statusCode === 1 && response.data?.data?.length > 0) {
|
||||||
|
return response.data.data[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch SEO data from database:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch SEO by exact path or blog slug, with graceful fallbacks
|
||||||
|
async function fetchSEOByPathOrSlug(normalizedPath) {
|
||||||
|
try {
|
||||||
|
const API_URL = process.env.API_URL || 'https://api.pozo.app';
|
||||||
|
|
||||||
|
// 1) Try path-based SEO: /Seo?Path=/home/blog/my-post
|
||||||
|
const byPath = await safeGet(`${API_URL}/Seo`, { Path: normalizedPath });
|
||||||
|
if (byPath) return byPath;
|
||||||
|
|
||||||
|
// 2) If looks like blog detail: /home/blog/slug
|
||||||
|
if (/^\/home\/blog\//.test(normalizedPath)) {
|
||||||
|
const slug = normalizedPath.replace(/^\/home\/blog\//, '').replace(/\/$/, '');
|
||||||
|
if (slug) {
|
||||||
|
// Try common blog SEO endpoints
|
||||||
|
const blogSeo = await safeGet(`${API_URL}/Seo/Blog`, { slug })
|
||||||
|
|| await safeGet(`${API_URL}/Blog/Seo`, { slug })
|
||||||
|
|| await safeGet(`${API_URL}/Blog`, { slug });
|
||||||
|
if (blogSeo) return blogSeo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeGet(baseUrl, queryObj) {
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams(queryObj).toString();
|
||||||
|
const url = `${baseUrl}?${qs}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
const data = response.data;
|
||||||
|
if (data?.statusCode === 1 && Array.isArray(data?.data) && data.data.length > 0) {
|
||||||
|
return data.data[0];
|
||||||
|
}
|
||||||
|
// Some APIs return object directly
|
||||||
|
if (data && typeof data === 'object' && !Array.isArray(data)) return data;
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(p) {
|
||||||
|
if (!p) return '/';
|
||||||
|
try {
|
||||||
|
// Remove query/hash, ensure leading slash, collapse duplicate slashes
|
||||||
|
const onlyPath = p.split('?')[0].split('#')[0] || '/';
|
||||||
|
let normalized = onlyPath.replace(/\\+/g, '/');
|
||||||
|
if (!normalized.startsWith('/')) normalized = `/${normalized}`;
|
||||||
|
// Remove trailing slash except for root
|
||||||
|
if (normalized.length > 1 && normalized.endsWith('/')) {
|
||||||
|
normalized = normalized.slice(0, -1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
} catch {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map routes without the "/home" prefix into the SPA's "/home" namespace
|
||||||
|
function mapToHomeNamespace(p) {
|
||||||
|
if (!p) return '/home/';
|
||||||
|
const raw = p.split('?')[0].split('#')[0] || '/';
|
||||||
|
// Already in /home namespace
|
||||||
|
if (raw === '/' || raw.startsWith('/home/')) return raw;
|
||||||
|
|
||||||
|
// Known top-level marketing routes to map into /home/*
|
||||||
|
const known = ['pricing-pozoapp', 'contact-us', 'signin', 'blog'];
|
||||||
|
const parts = raw.replace(/^\/+/, '').split('/');
|
||||||
|
const head = parts[0];
|
||||||
|
|
||||||
|
if (known.includes(head)) {
|
||||||
|
// blog detail: /blog/slug -> /home/blog/slug
|
||||||
|
if (head === 'blog' && parts.length > 1) {
|
||||||
|
return `/home/${parts.join('/')}`;
|
||||||
|
}
|
||||||
|
// simple mapping: /pricing-pozoapp -> /home/pricing-pozoapp
|
||||||
|
return `/home/${raw.replace(/^\/+/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default: keep as-is
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFullUrl(req) {
|
||||||
|
const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'https').split(',')[0];
|
||||||
|
const host = req.headers['x-forwarded-host'] || req.headers.host || 'www.pozo.app';
|
||||||
|
const pathOnly = normalizePath(req.path);
|
||||||
|
return `${proto}://${host}${pathOnly}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceOrInsert(html, regex, newTagHtml) {
|
||||||
|
if (regex.test(html)) {
|
||||||
|
return html.replace(regex, newTagHtml);
|
||||||
|
}
|
||||||
|
// Insert before </head>
|
||||||
|
if (/<\/head>/i.test(html)) {
|
||||||
|
return html.replace(/<\/head>/i, `${newTagHtml}\n</head>`);
|
||||||
|
}
|
||||||
|
// As a last resort, prepend to document
|
||||||
|
return `${newTagHtml}\n${html}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttribute(str) {
|
||||||
|
return String(str || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default seoMiddleware;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import seoMiddleware from './seo-middleware.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Serve static files first (CSS, JS, images)
|
||||||
|
app.use(express.static(path.join(__dirname, '../dist')));
|
||||||
|
// Also mount under /home because Vite base is /home/
|
||||||
|
app.use('/home', express.static(path.join(__dirname, '../dist')));
|
||||||
|
|
||||||
|
// Apply SEO middleware for HTML routes
|
||||||
|
app.use(seoMiddleware);
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
console.log('Dynamic SEO enabled!');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
ENV_BASE_URL='/'
|
||||||
|
ENV_API_URL='http://192.168.1.37:8013'
|
||||||
|
ENV_API_URL_TOKEN='http://192.168.1.37:8001'
|
||||||
|
# ENV_API_URL_TOKEN='https://www.pozo.dev/JwtToken'
|
||||||
|
# ENV_API_URL='https://www.pozo.dev/pozo-common-api'
|
||||||
|
# ENV_IMAGE_UPLOAD_API_URL="https://api.pozo.app/pozo-common-image-api/"
|
||||||
|
ENV_API_URL_RETAIL='http://192.168.1.37:8012/'
|
||||||
|
# ENV_API_URL_TOKEN='https://api.pozo.app/JwtToken'
|
||||||
|
# ENV_API_URL='https://api.pozo.app/pozo-common-api'
|
||||||
|
# ENV_API_URL_RETAIL='https://api.pozo.app/pozo-retail-api'
|
||||||
|
ENV_IMAGE_UPLOAD_API_URL="http://192.168.1.38/"
|
||||||
|
ENV_SECRET_KEY="3a939a2eed2cca4b64cd47f51b23b135b2b9b765273f695d47682ea803c8429d"
|
||||||
|
ENV_URL_SECRET_KEY="f2ef79b8b2d4fc1cc569726a5a753552257b4ff2872d6a71cf3119bce3f07c1b"
|
||||||
|
ENV_MAIN_BASE_URL='http://192.168.1.16:3000'
|
||||||
|
ENV_MAIN_REDIRECT_URL='/apps/retail/app-page/home'
|
||||||
|
ENV_EMAIL_API='http://192.168.1.37:8014'
|
||||||
|
ENV_IMAGE_UPLOAD_API_URL="http://192.168.1.38/"
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#221 server
|
||||||
|
|
||||||
|
# ENV_BASE_URL='/home/'
|
||||||
|
# ENV_API_URL='http://api.pozo.co.in/Pozocommonapi_UnderTest'
|
||||||
|
# # ENV_API_URL='http://api.pozo.co.in/Pozocommonapi'
|
||||||
|
# ENV_IMAGE_UPLOAD_API_URL="https://api.pozo.app/pozo-common-image-api"
|
||||||
|
# ENV_SECRET_KEY="3a939a2eed2cca4b64cd47f51b23b135b2b9b765273f695d47682ea803c8429d"
|
||||||
|
# ENV_URL_SECRET_KEY="f2ef79b8b2d4fc1cc569726a5a753552257b4ff2872d6a71cf3119bce3f07c1b"
|
||||||
|
# ENV_MAIN_BASE_URL='http://pozo.co.in/'
|
||||||
|
# ENV_MAIN_REDIRECT_URL='/apps/retail/app-page/home'
|
||||||
|
|
||||||
|
# live server(#173 server)
|
||||||
|
# ENV_BASE_URL='/home/'
|
||||||
|
# ENV_API_URL_TOKEN='https://api.pozo.app/JwtToken'
|
||||||
|
# ENV_API_URL='https://api.pozo.app/pozo-common-api'
|
||||||
|
# ENV_IMAGE_UPLOAD_API_URL="https://api.pozo.app/pozo-common-image-api"
|
||||||
|
# ENV_SECRET_KEY="3a939a2eed2cca4b64cd47f51b23b135b2b9b765273f695d47682ea803c8429d"
|
||||||
|
# ENV_URL_SECRET_KEY="f2ef79b8b2d4fc1cc569726a5a753552257b4ff2872d6a71cf3119bce3f07c1b"
|
||||||
|
# ENV_MAIN_BASE_URL='https://www.pozo.app'
|
||||||
|
# ENV_MAIN_REDIRECT_URL='/apps/retail/app-page/home'
|
||||||
|
# ENV_PAYMENT_URL='https://pozo.app/paymentgateway/PozoPaymentGateway'
|
||||||
|
# ENV_CUSTOM_PAYMENT_URL='https://pozo.app/CustomPaymentGateway/CustomPaymentGateway'
|
||||||
|
# ENV_EMAIL_API='https://api.pozo.app/pozo-sms-email-template-api'
|
||||||
|
# ENV_API_URL_RETAIL='https://api.pozo.app/pozo-retail-api'
|
||||||
|
|
||||||
|
#172 server
|
||||||
|
|
||||||
|
ENV_BASE_URL='/home/'
|
||||||
|
ENV_API_URL_TOKEN='https://www.pozo.dev/JwtToken'
|
||||||
|
ENV_API_URL='https://www.pozo.dev/pozo-common-api'
|
||||||
|
ENV_API_URL_RETAIL='https://www.pozo.dev/pozo-retail-api'
|
||||||
|
ENV_IMAGE_UPLOAD_API_URL="https://www.pozo.dev/pozo-common-image-api"
|
||||||
|
ENV_SECRET_KEY="3a939a2eed2cca4b64cd47f51b23b135b2b9b765273f695d47682ea803c8429d"
|
||||||
|
ENV_URL_SECRET_KEY="f2ef79b8b2d4fc1cc569726a5a753552257b4ff2872d6a71cf3119bce3f07c1b"
|
||||||
|
ENV_MAIN_BASE_URL='https://www.pozo.dev'
|
||||||
|
ENV_MAIN_REDIRECT_URL='/apps/retail/app-page/home'
|
||||||
|
ENV_PAYMENT_URL='https://pozo.app/paymentgateway/PozoPaymentGateway'
|
||||||
|
ENV_CUSTOM_PAYMENT_URL='https://pozo.app/CustomPaymentGateway/CustomPaymentGateway'
|
||||||
|
ENV_EMAIL_API='https://www.pozo.dev/pozo-sms-email-template-api'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "./AdminPanel.scss";
|
||||||
|
import {
|
||||||
|
FaTh,
|
||||||
|
FaUser,
|
||||||
|
FaUsers,
|
||||||
|
FaCalendarAlt,
|
||||||
|
FaBullseye,
|
||||||
|
FaVideo,
|
||||||
|
FaChevronRight,
|
||||||
|
FaPlus,
|
||||||
|
FaList,
|
||||||
|
FaSortDown,
|
||||||
|
FaArrowLeft,
|
||||||
|
FaRedo,
|
||||||
|
FaSignOutAlt,
|
||||||
|
FaCog,
|
||||||
|
FaGift,
|
||||||
|
FaGlobe,
|
||||||
|
FaImage,
|
||||||
|
FaMobile,
|
||||||
|
FaQuestionCircle,
|
||||||
|
FaBullhorn,
|
||||||
|
FaFootballBall,
|
||||||
|
FaBars,
|
||||||
|
FaTimes,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { BsPostcardFill } from "react-icons/bs";
|
||||||
|
import { HiOutlineHome } from "react-icons/hi2";
|
||||||
|
|
||||||
|
import { useAdminPanel } from "./AdminPanelContext";
|
||||||
|
import BannerSectionForm from "./AdminPanelForms/BannerSectionForm";
|
||||||
|
import CTASectionForm from "./AdminPanelForms/CTASectionForm";
|
||||||
|
import EcosystemForm from "./AdminPanelForms/EcosystemForm";
|
||||||
|
import FaqSectionForm from "./AdminPanelForms/FaqSectionForm";
|
||||||
|
import FooterForm from "./AdminPanelForms/FooterForm";
|
||||||
|
import HeroSectionForm from "./AdminPanelForms/HeroSectionForm";
|
||||||
|
import OfferingsForm from "./AdminPanelForms/OfferingsForm";
|
||||||
|
import TrendingAppsForm from "./AdminPanelForms/TrendingAppsForm";
|
||||||
|
import AppDemo from "./AdminPanelForms/AppDemo";
|
||||||
|
import BlogForm from "./AdminPanelForms/BlogForm";
|
||||||
|
import SeoForm from "./AdminPanelForms/SeoForm";
|
||||||
|
import SEO from "../Components/SEO/SEO";
|
||||||
|
import { clearSession, getSession } from "../Services/others";
|
||||||
|
import { GenerateLogout } from "../features/signInPage/signInPage";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
const subDirectory = import.meta.env.BASE_URL;
|
||||||
|
|
||||||
|
const AdminPanel = () => {
|
||||||
|
// Default to first option (HeroSectionForm)
|
||||||
|
const [selectedId, setSelectedId] = useState(0);
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const UserType = getSession("UserType");
|
||||||
|
console.log(UserType, "UserTypeUserType")
|
||||||
|
|
||||||
|
const sidebarOptions = [
|
||||||
|
// { id: 1, title: "Dashboard", subtitle: "Overview", icon: <FaTh /> },
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: "Blog",
|
||||||
|
subtitle: "Post Content Control",
|
||||||
|
icon: <BsPostcardFill size={25} />,
|
||||||
|
component: BlogForm,
|
||||||
|
key: "BlogSection",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: "SEO",
|
||||||
|
subtitle: "Content Management",
|
||||||
|
icon: <FaUser />,
|
||||||
|
component: SeoForm,
|
||||||
|
key: "SEOForm",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: 1,
|
||||||
|
// title: "HeroSection",
|
||||||
|
// subtitle: "Hero Section Management",
|
||||||
|
// icon: <FaUser />,
|
||||||
|
// component: HeroSectionForm,
|
||||||
|
// key: "HeroSection",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 2,
|
||||||
|
// title: "Offerings",
|
||||||
|
// subtitle: "Offering Section Management",
|
||||||
|
// icon: <FaGift />,
|
||||||
|
// component: OfferingsForm,
|
||||||
|
// key: "OfferingSection",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 3,
|
||||||
|
// title: "Industries",
|
||||||
|
// subtitle: "Ecosystem Management",
|
||||||
|
// icon: <FaGlobe />,
|
||||||
|
// component: EcosystemForm,
|
||||||
|
// key: "Industries",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 4,
|
||||||
|
// title: "AppDemo",
|
||||||
|
// subtitle: "Video Section Management",
|
||||||
|
// icon: <FaVideo />,
|
||||||
|
// component: AppDemo,
|
||||||
|
// key: "VideoDemo",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 5,
|
||||||
|
// title: "BannerSection",
|
||||||
|
// subtitle: "Banner Section Management",
|
||||||
|
// icon: <FaImage />,
|
||||||
|
// component: BannerSectionForm,
|
||||||
|
// key: "BannerSection",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 6,
|
||||||
|
// title: "TrendingApps",
|
||||||
|
// subtitle: "Trending Apps Management",
|
||||||
|
// icon: <FaMobile />,
|
||||||
|
// component: TrendingAppsForm,
|
||||||
|
// key: "TrendingApps",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 7,
|
||||||
|
// title: "FaqSection",
|
||||||
|
// subtitle: "FAQ Management",
|
||||||
|
// icon: <FaQuestionCircle />,
|
||||||
|
// component: FaqSectionForm,
|
||||||
|
// key: "FAQSection",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 8,
|
||||||
|
// title: "CTASection",
|
||||||
|
// subtitle: "Call-to-Action Management",
|
||||||
|
// icon: <FaBullhorn />,
|
||||||
|
// component: CTASectionForm,
|
||||||
|
// key: "CTASection",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 9,
|
||||||
|
// title: "Footer",
|
||||||
|
// subtitle: "Footer Management",
|
||||||
|
// icon: <FaFootballBall />,
|
||||||
|
// component: FooterForm,
|
||||||
|
// key: "FooterSection",
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentSection = sidebarOptions.find(
|
||||||
|
(option) => option.id === selectedId
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSideBarSelect = (option) => {
|
||||||
|
setSelectedId(option?.id);
|
||||||
|
setIsMobileMenuOpen(false); // Close mobile menu on selection
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
const UserId = getSession("UserId");
|
||||||
|
const status = "N";
|
||||||
|
const res = await dispatch(GenerateLogout({ UserId, status })).unwrap();
|
||||||
|
if (res?.data?.statusCode === 1) {
|
||||||
|
clearSession();
|
||||||
|
navigate(`${subDirectory}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO
|
||||||
|
title="PozoApp Admin Panel - Manage Your Business Settings"
|
||||||
|
description="Access PozoApp's comprehensive admin panel to configure your business settings, manage users, and customize your experience."
|
||||||
|
keywords="admin panel, business settings, user management, configuration"
|
||||||
|
url={`${subDirectory}admin-panel`}
|
||||||
|
image="/og/admin-og.jpg"
|
||||||
|
type="website"
|
||||||
|
noindex={true}
|
||||||
|
/>
|
||||||
|
<div className="admin-panel-container">
|
||||||
|
{/* Mobile Menu Toggle */}
|
||||||
|
<div className="admin-panel-mobile-header">
|
||||||
|
<div className="admin-panel-mobile-logo">
|
||||||
|
<FaCog /> Admin Panel
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="admin-panel-mobile-toggle"
|
||||||
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
|
>
|
||||||
|
{isMobileMenuOpen ? <FaTimes /> : <FaBars />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar Navigation */}
|
||||||
|
<div
|
||||||
|
className={`admin-panel-sidebar ${isMobileMenuOpen ? "mobile-open" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="admin-panel-sidebar-header">
|
||||||
|
<div className="admin-panel-logo">
|
||||||
|
<FaCog />
|
||||||
|
<h1>Admin Panel</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="admin-panel-sidebar-nav">
|
||||||
|
{sidebarOptions.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
className={`admin-panel-nav-item ${selectedId === option.id ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSideBarSelect(option)}
|
||||||
|
>
|
||||||
|
<div className="admin-panel-nav-icon">{option.icon}</div>
|
||||||
|
<div className="admin-panel-nav-content">
|
||||||
|
<span className="admin-panel-nav-title">{option.title}</span>
|
||||||
|
<span className="admin-panel-nav-subtitle">
|
||||||
|
{option.subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{selectedId === option.id && (
|
||||||
|
<div className="admin-panel-active-indicator"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="admin-panel-sidebar-footer">
|
||||||
|
<div className="admin-panel-user-profile">
|
||||||
|
<div className="admin-panel-user-avatar">SA</div>
|
||||||
|
<div className="admin-panel-user-info">
|
||||||
|
<span className="admin-panel-user-name">
|
||||||
|
Marketing Administrator
|
||||||
|
</span>
|
||||||
|
<span className="admin-panel-user-role">Super Admin</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="admin-panel-signout-btn"
|
||||||
|
onClick={handleSignOut}
|
||||||
|
>
|
||||||
|
<FaSignOutAlt /> Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Overlay */}
|
||||||
|
{isMobileMenuOpen && (
|
||||||
|
<div
|
||||||
|
className="admin-panel-mobile-overlay"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="admin-panel-main-content">
|
||||||
|
{/* Top Header with Breadcrumbs */}
|
||||||
|
<div className="admin-panel-top-header">
|
||||||
|
<div className="admin-panel-breadcrumbs">
|
||||||
|
<span>Dashboard</span>
|
||||||
|
{currentSection && (
|
||||||
|
<>
|
||||||
|
<FaChevronRight className="admin-panel-breadcrumb-arrow" />
|
||||||
|
<span>{currentSection.title} Management</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="backtohomefromAdminpanel"
|
||||||
|
onClick={() => UserType === "Super Admin" ? navigate(`${subDirectory}landing-page/home`) : navigate(`${subDirectory}`)}
|
||||||
|
>
|
||||||
|
Back to Home <HiOutlineHome size={18} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<div className="admin-panel-content-area">
|
||||||
|
{currentSection?.component ? (
|
||||||
|
<currentSection.component sectionKey={currentSection.key} />
|
||||||
|
) : (
|
||||||
|
<div className="admin-panel-placeholder-content">
|
||||||
|
<div className="admin-panel-placeholder-icon">
|
||||||
|
<FaTh />
|
||||||
|
</div>
|
||||||
|
<h2>Welcome to Admin Panel</h2>
|
||||||
|
<p>Select a section from the sidebar to get started</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPanel;
|
||||||
|
|
@ -0,0 +1,976 @@
|
||||||
|
// Modern Admin Panel with Trending UI/UX
|
||||||
|
.admin-panel {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%);
|
||||||
|
color: #1a202c;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
|
||||||
|
// Sidebar Styles - Modern Dark Theme
|
||||||
|
.sidebar {
|
||||||
|
width: 290px;
|
||||||
|
background: linear-gradient(180deg, #0f172a 0%, #1e1b4b 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 8px 0 40px rgba(0, 0, 0, 0.15);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #5255c8, #8b5cf6, #ec4899);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 2.5rem 1.75rem 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.875rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #818cf8;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(129, 140, 248, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #5255c8, #8b5cf6);
|
||||||
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
.nav-title {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.nav-subtitle {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-right: 1rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-indicator {
|
||||||
|
width: 4px;
|
||||||
|
height: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6, #ec4899);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.signout-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Content Styles
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// Top Header
|
||||||
|
.top-header {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #64748b;
|
||||||
|
|
||||||
|
.breadcrumb-arrow {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
color: #374151;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Header
|
||||||
|
.main-header {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25rem 0 0 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
background: linear-gradient(135deg, #1e40af, #1d4ed8);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(30, 64, 175, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.tab-navigation {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.sort-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
appearance: none;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle {
|
||||||
|
display: flex;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
.view-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: white;
|
||||||
|
color: #1e293b;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content Area
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
// Remove existing form styling and make it clean
|
||||||
|
.hero-section-form,
|
||||||
|
.offerings-form,
|
||||||
|
.ecosystem-form,
|
||||||
|
.trending-apps-form,
|
||||||
|
.faq-form,
|
||||||
|
.CTASectionFormMaster,
|
||||||
|
.FooterFormMaster,
|
||||||
|
.banner-section-form {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.hero-section-header,
|
||||||
|
.offerings-header,
|
||||||
|
.ecosystem-header,
|
||||||
|
.trending-apps-header,
|
||||||
|
.faq-header,
|
||||||
|
.cta-header,
|
||||||
|
.footer-header,
|
||||||
|
.banner-section-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container,
|
||||||
|
.offeringsContent,
|
||||||
|
.ecosystemContent,
|
||||||
|
.form-inputs,
|
||||||
|
.faq-inputs,
|
||||||
|
.cta-form,
|
||||||
|
.footer-form,
|
||||||
|
.banner-content {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: linear-gradient(135deg, #e2e8f0, #cbd5e1);
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Overlay
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(90vh - 80px);
|
||||||
|
|
||||||
|
// Style the form components inside modal
|
||||||
|
.hero-section-form,
|
||||||
|
.offerings-form,
|
||||||
|
.ecosystem-form,
|
||||||
|
.trending-apps-form,
|
||||||
|
.faq-form,
|
||||||
|
.CTASectionFormMaster,
|
||||||
|
.FooterFormMaster,
|
||||||
|
.banner-section-form {
|
||||||
|
.form-container,
|
||||||
|
.offeringsContent,
|
||||||
|
.ecosystemContent,
|
||||||
|
.form-inputs,
|
||||||
|
.faq-inputs,
|
||||||
|
.cta-form,
|
||||||
|
.footer-form,
|
||||||
|
.banner-content {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide data display sections in modal
|
||||||
|
.hero-data-display,
|
||||||
|
.apps-display,
|
||||||
|
.added-titles,
|
||||||
|
.saved-posters,
|
||||||
|
.added-banners,
|
||||||
|
.added-faqs,
|
||||||
|
.apps-display,
|
||||||
|
.admin-cards-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
.main-header,
|
||||||
|
.tab-navigation,
|
||||||
|
.top-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
min-width: 150px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
.main-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-navigation {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.tab-controls {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-modal {
|
||||||
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.create-modal-form {
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.save-btn {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #3b82f6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card-based layout for data display
|
||||||
|
.admin-cards-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f1f5f9;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 1.25rem;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
|
||||||
|
.priority-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.high {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.low {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
border-color: #ef4444;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
import React, { createContext, useContext, useState } from 'react'
|
||||||
|
|
||||||
|
const AdminPanelContext = createContext()
|
||||||
|
|
||||||
|
export const useAdminPanel = () => {
|
||||||
|
const context = useContext(AdminPanelContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAdminPanel must be used within AdminPanelProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminPanelProvider = ({ children }) => {
|
||||||
|
const [sectionData, setSectionData] = useState({
|
||||||
|
BannerSectionForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Summer Sale Banner',
|
||||||
|
description: 'Promotional banner for summer sale campaign',
|
||||||
|
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 22, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
CTASectionForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Get Started CTA',
|
||||||
|
description: 'Primary call-to-action for new users',
|
||||||
|
image: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 21, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
EcosystemForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Smart City Solutions',
|
||||||
|
description: 'Comprehensive urban development platform integrating IoT, data analytics, and citizen engagement.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 21, 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Healthcare Innovation Hub',
|
||||||
|
description: 'Connected healthcare ecosystem with telemedicine, AI diagnostics, and patient management.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'medium',
|
||||||
|
date: 'Dec 20, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
FaqSectionForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'How to Get Started',
|
||||||
|
description: 'Step-by-step guide for new users to begin using our platform',
|
||||||
|
image: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 20, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
FooterForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Main Footer',
|
||||||
|
description: 'Primary footer with contact information and links',
|
||||||
|
image: 'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'medium',
|
||||||
|
date: 'Dec 19, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
HeroSectionForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Support the Environment',
|
||||||
|
description: 'Environmental conservation through waste management, and sustainable practices for a better future.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1542601906990-b4d3fb778b09?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'medium',
|
||||||
|
date: 'Dec 19, 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Water, Sanitation & Hygiene',
|
||||||
|
description: 'Providing clean water access, sanitation facilities, hygiene education to improve community health and wellbeing.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1559027615-cd4628902d4a?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 18, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
OfferingsForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Digital Transformation',
|
||||||
|
description: 'Comprehensive digital solutions to modernize your business operations and enhance customer experience.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 18, 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Cloud Infrastructure',
|
||||||
|
description: 'Scalable cloud solutions for secure data storage, processing, and seamless business operations.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'medium',
|
||||||
|
date: 'Dec 17, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
TrendingAppsForm: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'PozoPay',
|
||||||
|
description: 'Secure mobile payment solution with advanced encryption and fraud protection.',
|
||||||
|
icon: 'https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=100&h=100&fit=crop',
|
||||||
|
category: 'Finance',
|
||||||
|
rating: 4.8,
|
||||||
|
downloads: '1.2M',
|
||||||
|
status: 'active',
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 22, 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'SmartInventory',
|
||||||
|
description: 'AI-powered inventory management system with real-time tracking and analytics.',
|
||||||
|
icon: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=100&h=100&fit=crop',
|
||||||
|
category: 'Business',
|
||||||
|
rating: 4.6,
|
||||||
|
downloads: '850K',
|
||||||
|
status: 'active',
|
||||||
|
priority: 'medium',
|
||||||
|
date: 'Dec 21, 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'HealthTracker',
|
||||||
|
description: 'Comprehensive health monitoring app with wearable device integration.',
|
||||||
|
icon: 'https://images.unsplash.com/photo-1559757175-0eb30cd8c063?w=100&h=100&fit=crop',
|
||||||
|
category: 'Health',
|
||||||
|
rating: 4.9,
|
||||||
|
downloads: '2.1M',
|
||||||
|
status: 'active',
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 20, 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
AppDemo: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Product Demo Video',
|
||||||
|
description: 'Comprehensive demonstration of our latest product features and capabilities',
|
||||||
|
image: 'https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
priority: 'high',
|
||||||
|
date: 'Dec 19, 2024'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const addItem = (sectionKey, item) => {
|
||||||
|
setSectionData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: [...prev[sectionKey], { ...item, id: Date.now() }]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItem = (sectionKey, itemId, updatedItem) => {
|
||||||
|
setSectionData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: prev[sectionKey].map(item =>
|
||||||
|
item.id === itemId ? { ...item, ...updatedItem } : item
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteItem = (sectionKey, itemId) => {
|
||||||
|
setSectionData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: prev[sectionKey].filter(item => item.id !== itemId)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleItemStatus = (sectionKey, itemId) => {
|
||||||
|
setSectionData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: prev[sectionKey].map(item =>
|
||||||
|
item.id === itemId ? { ...item, active: !item.active } : item
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSectionData = (sectionKey, newData) => {
|
||||||
|
setSectionData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: newData
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
sectionData,
|
||||||
|
addItem,
|
||||||
|
updateItem,
|
||||||
|
deleteItem,
|
||||||
|
toggleItemStatus,
|
||||||
|
updateSectionData
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminPanelContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AdminPanelContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { useAdminPanel } from '../AdminPanelContext'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/AppDemo.scss"
|
||||||
|
|
||||||
|
const AppDemo = () => {
|
||||||
|
const { sectionData, updateSectionData } = useAdminPanel()
|
||||||
|
const videoData = sectionData.AppDemo || []
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [file, setFile] = useState(null)
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
const [fileError, setFileError] = useState('')
|
||||||
|
const [previewUrls, setPreviewUrls] = useState(new Map())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
previewUrls.forEach(url => URL.revokeObjectURL(url))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
// Clean up preview URL
|
||||||
|
const prevUrl = previewUrls.get('preview')
|
||||||
|
if (prevUrl) {
|
||||||
|
URL.revokeObjectURL(prevUrl)
|
||||||
|
setPreviewUrls(new Map(previewUrls.delete('preview')))
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setTitle('')
|
||||||
|
setFile(null)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setFileError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setFile(null)
|
||||||
|
setFileError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const selectedFile = e.target.files[0]
|
||||||
|
if (selectedFile) {
|
||||||
|
// Clean up previous preview URL
|
||||||
|
const prevUrl = previewUrls.get('preview')
|
||||||
|
if (prevUrl) URL.revokeObjectURL(prevUrl)
|
||||||
|
|
||||||
|
// Create new preview URL
|
||||||
|
const newUrl = URL.createObjectURL(selectedFile)
|
||||||
|
setPreviewUrls(new Map(previewUrls.set('preview', newUrl)))
|
||||||
|
|
||||||
|
setFile(selectedFile)
|
||||||
|
if (fileError) setFileError('')
|
||||||
|
if (!title) {
|
||||||
|
setTitle(selectedFile.name.replace(/\.[^/.]+$/, ''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!file) {
|
||||||
|
setFileError('Video file is required')
|
||||||
|
message.error('Please select a video file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setFileError('')
|
||||||
|
|
||||||
|
const newVideo = {
|
||||||
|
id: editingIndex >= 0 ? videoData[editingIndex].id : Date.now(),
|
||||||
|
title: title.trim() || file.name,
|
||||||
|
file: file,
|
||||||
|
active: true,
|
||||||
|
createdAt: editingIndex >= 0 ? videoData[editingIndex].createdAt : new Date().toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex >= 0) {
|
||||||
|
const updatedVideos = videoData.map((video, index) => index === editingIndex ? newVideo : video)
|
||||||
|
updateSectionData('AppDemo', updatedVideos)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
message.success('Video updated!')
|
||||||
|
} else {
|
||||||
|
updateSectionData('AppDemo', [...videoData, newVideo])
|
||||||
|
message.success('Video added!')
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedVideos = videoData.filter((_, i) => i !== index)
|
||||||
|
updateSectionData('AppDemo', updatedVideos)
|
||||||
|
message.success('Video deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
const video = videoData[index]
|
||||||
|
setTitle(video.title)
|
||||||
|
setFile(video.file)
|
||||||
|
setEditingIndex(index)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...videoData]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
updateSectionData('AppDemo', updated)
|
||||||
|
message.success('Status updated')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='video-section-form-master'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='video-section-form-header'>
|
||||||
|
<div className='video-section-header-left'>
|
||||||
|
{/* <div className='video-section-header-icon'>
|
||||||
|
<span>🎥</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='video-section-header-content'>
|
||||||
|
<h2>Video Management</h2>
|
||||||
|
<p>Manage and organize your video content.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='video-section-create-btn'>
|
||||||
|
<span>+</span> Upload Video
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='video-section-tab-navigation'>
|
||||||
|
<div className='video-section-tab-buttons'>
|
||||||
|
<button className='video-section-tab-btn video-section-active'>
|
||||||
|
Videos ({videoData.length})
|
||||||
|
</button>
|
||||||
|
<button className='video-section-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='video-section-tab-controls'>
|
||||||
|
<div className='video-section-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='video-section-view-toggle'>
|
||||||
|
<button className='video-section-view-btn video-section-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='video-section-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{videoData.length > 0 ? (
|
||||||
|
<div className='video-section-display'>
|
||||||
|
<div className='video-section-grid'>
|
||||||
|
{videoData.map((video, index) => (
|
||||||
|
<div key={video.id} className={`video-section-card ${!video.active ? 'video-section-inactive' : ''}`}>
|
||||||
|
<div className='video-section-card-header'>
|
||||||
|
<h3>{video.title || video.file?.name || 'Untitled Video'}</h3>
|
||||||
|
<div className='video-section-card-actions'>
|
||||||
|
<button className='video-section-edit-btn' onClick={() => handleEdit(index)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Video"
|
||||||
|
description="Are you sure you want to delete this video?"
|
||||||
|
onConfirm={() => handleDelete(index)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='video-section-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`video-section-status-btn ${video.active ? 'video-section-deactivate' : 'video-section-activate'}`}
|
||||||
|
onClick={() => handleToggleActive(index)}
|
||||||
|
>
|
||||||
|
{video.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='video-section-card-content'>
|
||||||
|
<div className='video-section-video-preview'>
|
||||||
|
{video.file instanceof File ? (
|
||||||
|
<video width='100%' controls>
|
||||||
|
<source src={previewUrls.get(video.id) || URL.createObjectURL(video.file)} type={video.file.type} />
|
||||||
|
</video>
|
||||||
|
) : (
|
||||||
|
<div className='video-placeholder'>Video not available</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className='video-section-created-date'>Created: {video.date || video.createdAt}</p>
|
||||||
|
<p className={`video-section-status ${video.active ? 'video-section-active' : 'video-section-inactive'}`}>
|
||||||
|
Status: {video.active ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='video-section-placeholder-content'>
|
||||||
|
<div className='video-section-placeholder-icon'>
|
||||||
|
<span>🎥</span>
|
||||||
|
</div>
|
||||||
|
<h2>No Videos Found</h2>
|
||||||
|
<p>Get started by uploading your first video</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingIndex >= 0 ? 'Edit Video' : 'Upload New Video'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingIndex >= 0 ? 'Update Video' : 'Add Video'}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='video-section-form'>
|
||||||
|
<div className='video-section-form-group'>
|
||||||
|
<label htmlFor='videoTitle'>Video Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='videoTitle'
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder='Enter video title'
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='video-section-form-group'>
|
||||||
|
<label htmlFor='videoFile'>Video Upload</label>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
id='videoFile'
|
||||||
|
accept='video/*'
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className={`video-section-file-input ${fileError ? 'error' : ''}`}
|
||||||
|
/>
|
||||||
|
{fileError && <span className='error-message'>{fileError}</span>}
|
||||||
|
{file && file instanceof File && (
|
||||||
|
<div className='video-section-video-preview'>
|
||||||
|
<p>Selected: {file.name}</p>
|
||||||
|
<video width='100%' controls>
|
||||||
|
<source src={previewUrls.get('preview') || URL.createObjectURL(file)} type={file.type} />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='video-section-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='video-section-save-btn'>
|
||||||
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='video-section-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='video-section-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppDemo
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { useAdminPanel } from '../AdminPanelContext'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/BannerSectionForm.scss"
|
||||||
|
|
||||||
|
const BannerSectionForm = () => {
|
||||||
|
const { sectionData, updateSectionData } = useAdminPanel()
|
||||||
|
const bannerData = sectionData.BannerSectionForm || []
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
const [titleError, setTitleError] = useState('')
|
||||||
|
const [descriptionError, setDescriptionError] = useState('')
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setTitle('')
|
||||||
|
setDescription('')
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setTitleError('')
|
||||||
|
setDescriptionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setDescription('')
|
||||||
|
setTitleError('')
|
||||||
|
setDescriptionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
setTitleError('Title is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description.trim()) {
|
||||||
|
setDescriptionError('Description is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setDescriptionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
message.error('Please fix the highlighted fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBanner = {
|
||||||
|
id: editingIndex >= 0 ? bannerData[editingIndex].id : Date.now(),
|
||||||
|
title: title.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
active: true,
|
||||||
|
createdAt: editingIndex >= 0 ? bannerData[editingIndex].createdAt : new Date().toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex >= 0) {
|
||||||
|
const updatedBanners = bannerData.map((banner, index) => index === editingIndex ? newBanner : banner)
|
||||||
|
updateSectionData('BannerSectionForm', updatedBanners)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
message.success('Banner updated!')
|
||||||
|
} else {
|
||||||
|
updateSectionData('BannerSectionForm', [...bannerData, newBanner])
|
||||||
|
message.success('Banner added!')
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedBanners = bannerData.filter((_, i) => i !== index)
|
||||||
|
updateSectionData('BannerSectionForm', updatedBanners)
|
||||||
|
message.success('Banner deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
const banner = bannerData[index]
|
||||||
|
setTitle(banner.title)
|
||||||
|
setDescription(banner.description)
|
||||||
|
setEditingIndex(index)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...bannerData]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
updateSectionData('BannerSectionForm', updated)
|
||||||
|
message.success('Status updated')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='banner-section-form-master'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='banner-section-form-header'>
|
||||||
|
<div className='banner-section-header-left'>
|
||||||
|
{/* <div className='banner-section-header-icon'>
|
||||||
|
<span>🏷️</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='banner-section-header-content'>
|
||||||
|
<h2>Banner Management</h2>
|
||||||
|
<p>Manage and organize your banner content and settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='banner-section-create-btn'>
|
||||||
|
<span>+</span> Create Banner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='banner-section-tab-navigation'>
|
||||||
|
<div className='banner-section-tab-buttons'>
|
||||||
|
<button className='banner-section-tab-btn banner-section-active'>
|
||||||
|
Banners ({bannerData.length})
|
||||||
|
</button>
|
||||||
|
<button className='banner-section-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='banner-section-tab-controls'>
|
||||||
|
<div className='banner-section-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='banner-section-view-toggle'>
|
||||||
|
<button className='banner-section-view-btn banner-section-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='banner-section-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{bannerData.length > 0 ? (
|
||||||
|
<div className='banner-section-display'>
|
||||||
|
<div className='banner-section-grid'>
|
||||||
|
{bannerData.map((banner, index) => (
|
||||||
|
<div key={banner.id} className={`banner-section-card ${!banner.active ? 'banner-section-inactive' : ''}`}>
|
||||||
|
<div className='banner-section-card-header'>
|
||||||
|
<h3>{banner.title}</h3>
|
||||||
|
<div className='banner-section-card-actions'>
|
||||||
|
<button className='banner-section-edit-btn' onClick={() => handleEdit(index)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Banner"
|
||||||
|
description="Are you sure you want to delete this banner?"
|
||||||
|
onConfirm={() => handleDelete(index)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='banner-section-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`banner-section-status-btn ${banner.active ? 'banner-section-deactivate' : 'banner-section-activate'}`}
|
||||||
|
onClick={() => handleToggleActive(index)}
|
||||||
|
>
|
||||||
|
{banner.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='banner-section-card-content'>
|
||||||
|
<p><strong>Description:</strong> {banner.description || 'No description'}</p>
|
||||||
|
<p className='banner-section-created-date'>Created: {banner.date || banner.createdAt}</p>
|
||||||
|
<p className={`banner-section-status ${banner.active ? 'banner-section-active' : 'banner-section-inactive'}`}>
|
||||||
|
Status: {banner.active ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='banner-section-placeholder-content'>
|
||||||
|
<div className='banner-section-placeholder-icon'>
|
||||||
|
<span>🏷️</span>
|
||||||
|
</div>
|
||||||
|
<h2>No Banners Found</h2>
|
||||||
|
<p>Get started by creating your first banner</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingIndex >= 0 ? 'Edit Banner' : 'Create New Banner'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingIndex >= 0 ? 'Update Banner' : 'Add Banner'}
|
||||||
|
width={700}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='banner-section-form'>
|
||||||
|
<div className='banner-section-form-group'>
|
||||||
|
<label htmlFor='bannerTitle'>Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='bannerTitle'
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
if (titleError) setTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter banner title'
|
||||||
|
className={titleError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='banner-section-form-group'>
|
||||||
|
<label htmlFor='bannerDescription'>Description</label>
|
||||||
|
<textarea
|
||||||
|
id='bannerDescription'
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => {
|
||||||
|
setDescription(e.target.value)
|
||||||
|
if (descriptionError) setDescriptionError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter banner description'
|
||||||
|
rows='4'
|
||||||
|
className={descriptionError ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{descriptionError && <span className='error-message'>{descriptionError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='banner-section-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='banner-section-save-btn'>
|
||||||
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='banner-section-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='banner-section-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BannerSectionForm
|
||||||
|
|
@ -0,0 +1,905 @@
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import "../Styles/BlogForm.scss"
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import { Messages } from '../../Components/Notifications/Messages.jsx'
|
||||||
|
import { getSession } from "../../Services/others.js"
|
||||||
|
import { deleteBlog, getBlog, postBlog, putBlog } from '../../features/Blog/Blog.js'
|
||||||
|
import { Await, useNavigate } from 'react-router-dom'
|
||||||
|
import { uploadImage } from '../../features/applications/bannerImage'
|
||||||
|
import { RiImageLine, RiVideoLine, RiArticleLine, RiPlayFill, RiUserLine, RiSearchLine } from 'react-icons/ri'
|
||||||
|
import { getConfigNames } from '../../features/configmasterPage/configmasterPage.js'
|
||||||
|
import { AiOutlineDelete, AiOutlineEdit } from "react-icons/ai";
|
||||||
|
|
||||||
|
|
||||||
|
const subDirectory = import.meta.env.ENV_BASE_URL
|
||||||
|
const BlogForm = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
const [blogTitle, setBlogTitle] = useState('')
|
||||||
|
const [blogSubtitle, setBlogSubtitle] = useState('')
|
||||||
|
const [posterImage, setPosterImage] = useState(null)
|
||||||
|
const [publishDate, setPublishDate] = useState('')
|
||||||
|
const [publishTime, setPublishTime] = useState('')
|
||||||
|
const [detailSections, setDetailSections] = useState([{
|
||||||
|
DtlTitle: '',
|
||||||
|
DtlDescription: [''],
|
||||||
|
DtlImages: [null],
|
||||||
|
DtlVideos: [null]
|
||||||
|
}])
|
||||||
|
console.log(detailSections, 'detailSections')
|
||||||
|
const userId = getSession("UserId")
|
||||||
|
// const [moreDetails, setMoreDetails] = useState(false);
|
||||||
|
|
||||||
|
const [savedBlogs, setSavedBlogs] = useState([])
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
const [moreDetails, setMoreDetails] = useState(false)
|
||||||
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
|
const [modalContent, setModalContent] = useState(null)
|
||||||
|
const [modalType, setModalType] = useState('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [filterStatus, setFilterStatus] = useState('all')
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
|
||||||
|
|
||||||
|
const addDescription = (sectionIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlDescription.push('')
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDescription = (sectionIndex, descIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlDescription.splice(descIndex, 1)
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addImage = (sectionIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlImages.push(null)
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeImage = (sectionIndex, imgIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlImages.splice(imgIndex, 1)
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addVideo = (sectionIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlVideos.push(null)
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeVideo = (sectionIndex, vidIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlVideos.splice(vidIndex, 1)
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addDetailSection = () => {
|
||||||
|
setDetailSections([...detailSections, {
|
||||||
|
DtlTitle: '',
|
||||||
|
DtlDescription: [''],
|
||||||
|
DtlImages: [null],
|
||||||
|
DtlVideos: [null]
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDetailSection = (sectionIndex) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated.splice(sectionIndex, 1)
|
||||||
|
setDetailSections(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkScheduledBlogs()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const checkScheduledBlogs = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
let Res = await dispatch(getBlog()).unwrap();
|
||||||
|
if (Res?.data?.statusCode == 1) {
|
||||||
|
const blogs = Res?.data?.data
|
||||||
|
setSavedBlogs(blogs)
|
||||||
|
} else {
|
||||||
|
setSavedBlogs([])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error checking scheduled blogs:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!blogTitle.trim()) {
|
||||||
|
message.error('Blog title is required')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!blogSubtitle.trim()) {
|
||||||
|
message.error('Blog subtitle is required')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// debugger
|
||||||
|
try {
|
||||||
|
// First, ensure all images/videos are uploaded
|
||||||
|
const updatedDetails = detailSections.map((section) => {
|
||||||
|
return {
|
||||||
|
...(editingIndex > -1 ? { BlogId: section?.BlogId } : {}),
|
||||||
|
...(editingIndex > -1 ? { UniqueId: section?.UniqueId } : {}),
|
||||||
|
DtlTitle: section?.DtlTitle ?? "",
|
||||||
|
DtlDescription: section?.DtlDescription ?? [],
|
||||||
|
DtlImages: section.DtlImages ?? [],
|
||||||
|
DtlVideos: section.DtlVideos ?? [],
|
||||||
|
ActiveStatus: "A",
|
||||||
|
...(editingIndex > -1 ? { UpdatedBy: userId || 1 } : { CreatedBy: userId || 1 }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare blog data
|
||||||
|
const PostBlogData = {
|
||||||
|
BlogTitle: blogTitle,
|
||||||
|
BlogSubtitle: blogSubtitle,
|
||||||
|
HeaderImage: posterImage,
|
||||||
|
PublishDate: publishDate || '',
|
||||||
|
PublishTime: publishTime || '',
|
||||||
|
ActiveStatus: savedBlogs[editingIndex]?.ActiveStatus ? savedBlogs[editingIndex]?.ActiveStatus : "A",
|
||||||
|
...(editingIndex > -1
|
||||||
|
? { BlogId: savedBlogs[editingIndex].BlogId, UpdatedBy: userId || 1 }
|
||||||
|
: { CreatedBy: userId || 1 }),
|
||||||
|
BlogDetails: updatedDetails,
|
||||||
|
IsPublished: savedBlogs[editingIndex]?.IsPublished ? savedBlogs[editingIndex]?.IsPublished : "N"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send API request
|
||||||
|
const Response = editingIndex > -1
|
||||||
|
? await dispatch(putBlog(PostBlogData)).unwrap()
|
||||||
|
: await dispatch(postBlog(PostBlogData)).unwrap();
|
||||||
|
|
||||||
|
if (Response) {
|
||||||
|
message.success('Blog post saved successfully!');
|
||||||
|
await checkScheduledBlogs();
|
||||||
|
handleClear();
|
||||||
|
setShowForm(false);
|
||||||
|
} else {
|
||||||
|
message.error('No response from server');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
message.error(`Failed to save: ${error?.message || 'Please check your connection and try again'}`);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setEditingIndex(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
const blog = savedBlogs[index];
|
||||||
|
navigate(`${subDirectory}PostEditorPage`, {
|
||||||
|
state: { blog, isEdit: true }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (blogId, activeStatus) => {
|
||||||
|
// if (confirm('Are you sure you want to delete this blog post?')) {
|
||||||
|
// const updated = savedBlogs.filter((_, i) => i !== index)
|
||||||
|
// setSavedBlogs(updated)
|
||||||
|
// }
|
||||||
|
const updatedStatus = activeStatus === 'A' ? 'D' : 'A';
|
||||||
|
const res = await dispatch(deleteBlog({
|
||||||
|
blogId: blogId,
|
||||||
|
activeStatus: updatedStatus,
|
||||||
|
updatedBy: userId
|
||||||
|
})).unwrap();
|
||||||
|
if (res?.data?.statusCode == 1) {
|
||||||
|
message.success('Blog status deleted successfully!')
|
||||||
|
await checkScheduledBlogs();
|
||||||
|
} else {
|
||||||
|
message.error('Failed to update blog status. Please try again.')
|
||||||
|
}
|
||||||
|
setEditingIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePublish = async (index) => {
|
||||||
|
const updated = [...savedBlogs];
|
||||||
|
const isCurrentlyPublished = updated[index].IsPublished === 'Y';
|
||||||
|
|
||||||
|
// Toggle publish status
|
||||||
|
updated[index].IsPublished = isCurrentlyPublished ? 'N' : 'Y';
|
||||||
|
|
||||||
|
// If unpublishing (Y -> N), clear the publish date and time
|
||||||
|
if (isCurrentlyPublished) {
|
||||||
|
updated[index].PublishDate = null;
|
||||||
|
updated[index].PublishTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await dispatch(putBlog(updated[index]))?.unwrap();
|
||||||
|
|
||||||
|
if (res?.data?.statusCode == 1) {
|
||||||
|
message.success('Publish status updated successfully!');
|
||||||
|
await checkScheduledBlogs();
|
||||||
|
} else {
|
||||||
|
message.error('Failed to update blog status. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
handleClear()
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setShowForm(false)
|
||||||
|
}
|
||||||
|
const handleClear = () => {
|
||||||
|
setBlogTitle('')
|
||||||
|
setBlogSubtitle('')
|
||||||
|
setPosterImage(null)
|
||||||
|
setPublishDate('')
|
||||||
|
setPublishTime('')
|
||||||
|
setDetailSections([{
|
||||||
|
DtlTitle: '',
|
||||||
|
DtlDescription: [''],
|
||||||
|
DtlImages: [null],
|
||||||
|
DtlVideos: [null]
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddmoreDetails = () => {
|
||||||
|
setMoreDetails(!moreDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModal = (content, type) => {
|
||||||
|
setModalContent(content)
|
||||||
|
setModalType(type)
|
||||||
|
setModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalOpen(false)
|
||||||
|
setModalContent(null)
|
||||||
|
setModalType('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = async (file, type, sectionIndex = null, mediaIndex = null) => {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await dispatch(uploadImage(file)).unwrap();
|
||||||
|
const fileUrl = res?.data?.status ? res?.data?.image : file;
|
||||||
|
|
||||||
|
// Update based on type
|
||||||
|
if (type === 'poster') {
|
||||||
|
setPosterImage(fileUrl);
|
||||||
|
}
|
||||||
|
else if (type === 'image' && sectionIndex !== null && mediaIndex !== null) {
|
||||||
|
const updated = [...detailSections];
|
||||||
|
updated[sectionIndex].DtlImages[mediaIndex] = fileUrl;
|
||||||
|
setDetailSections(updated);
|
||||||
|
}
|
||||||
|
else if (type === 'video' && sectionIndex !== null && mediaIndex !== null) {
|
||||||
|
const updated = [...detailSections];
|
||||||
|
updated[sectionIndex].DtlVideos[mediaIndex] = fileUrl;
|
||||||
|
setDetailSections(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
if (res?.data?.status) {
|
||||||
|
message.success(`${type === 'video' ? 'Video' : 'Image'} uploaded successfully!`);
|
||||||
|
} else {
|
||||||
|
message.warning(`${type === 'video' ? 'Video' : 'Image'} upload failed. Using local preview.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error uploading ${type}:`, err);
|
||||||
|
message.error(`Error uploading ${type}. Please try again.`);
|
||||||
|
|
||||||
|
// Fallback to local preview
|
||||||
|
if (type === 'poster') {
|
||||||
|
setPosterImage(file);
|
||||||
|
}
|
||||||
|
else if (type === 'image' && sectionIndex !== null && mediaIndex !== null) {
|
||||||
|
const updated = [...detailSections];
|
||||||
|
updated[sectionIndex].DtlImages[mediaIndex] = file;
|
||||||
|
setDetailSections(updated);
|
||||||
|
}
|
||||||
|
else if (type === 'video' && sectionIndex !== null && mediaIndex !== null) {
|
||||||
|
const updated = [...detailSections];
|
||||||
|
updated[sectionIndex].DtlVideos[mediaIndex] = file;
|
||||||
|
setDetailSections(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewURL = (fileOrUrl) => {
|
||||||
|
if (!fileOrUrl) return ''; // null/undefined → empty string
|
||||||
|
if (typeof fileOrUrl === 'string') return fileOrUrl; // backend URL → use directly
|
||||||
|
if (fileOrUrl instanceof File || fileOrUrl instanceof Blob) {
|
||||||
|
return URL.createObjectURL(fileOrUrl); // local file → preview
|
||||||
|
}
|
||||||
|
return ''; // fallback for anything else
|
||||||
|
};
|
||||||
|
|
||||||
|
const setShowFormNavigate = () => {
|
||||||
|
navigate(`${subDirectory}PostEditorPage`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTo12Hour(time24) {
|
||||||
|
// Split "HH:MM:SS" into parts
|
||||||
|
const [hourStr, minuteStr] = time24.split(":");
|
||||||
|
let hour = parseInt(hourStr, 10);
|
||||||
|
const minute = minuteStr;
|
||||||
|
|
||||||
|
// Determine AM/PM
|
||||||
|
const ampm = hour >= 12 ? "PM" : "AM";
|
||||||
|
|
||||||
|
// Convert hour to 12-hour format
|
||||||
|
hour = hour % 12;
|
||||||
|
hour = hour === 0 ? 12 : hour; // 0 should be 12
|
||||||
|
|
||||||
|
// Return formatted string
|
||||||
|
return `${hour.toString().padStart(2, "0")}:${minute} ${ampm}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter blogs based on status and search term
|
||||||
|
const filteredBlogs = savedBlogs?.filter(blog => {
|
||||||
|
// Status filter
|
||||||
|
let statusMatch = true;
|
||||||
|
if (filterStatus === 'published') statusMatch = blog.IsPublished === 'Y';
|
||||||
|
else if (filterStatus === 'unpublished') statusMatch = blog.IsPublished === 'N';
|
||||||
|
else if (filterStatus === 'draft') statusMatch = !blog.PublishDate && blog.IsPublished === 'N';
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
const searchMatch = !searchTerm ||
|
||||||
|
blog.BlogTitle?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
blog.BlogSubtitle?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
blog.Author?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
return statusMatch && searchMatch;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='blog-form-master'>
|
||||||
|
<div className='blog-form-header'>
|
||||||
|
<div className='blog-form-header-content'>
|
||||||
|
<h2>Blog Management</h2>
|
||||||
|
<p>Create and manage your blog posts and content.</p>
|
||||||
|
</div>
|
||||||
|
<button className='blog-form-create-btn'
|
||||||
|
// onClick={() => setShowForm(!showForm)}
|
||||||
|
onClick={setShowFormNavigate}
|
||||||
|
>
|
||||||
|
{showForm ? (
|
||||||
|
<><span>←</span> Back</>
|
||||||
|
) : (
|
||||||
|
<><span>+</span> Create Blog Post</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='blog-form-layout'>
|
||||||
|
{showForm && (
|
||||||
|
<div className='blog-form-left'>
|
||||||
|
<>
|
||||||
|
{/* Poster Section */}
|
||||||
|
<div className='blog-form-container'>
|
||||||
|
<h3>Poster Section</h3>
|
||||||
|
<div className='blog-form-field'>
|
||||||
|
<label>Blog Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={blogTitle}
|
||||||
|
onChange={(e) => setBlogTitle(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='blog-form-field'>
|
||||||
|
<label>Blog Subtitle</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={blogSubtitle}
|
||||||
|
onChange={(e) => setBlogSubtitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='blog-form-field'>
|
||||||
|
<label>Upload Image</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
// onChange={(e) => setPosterImage(e.target.files[0]) }
|
||||||
|
onChange={(e) => handleFileUpload(e.target.files[0], 'poster')}
|
||||||
|
/>
|
||||||
|
{posterImage && (
|
||||||
|
<div className='file-preview'>
|
||||||
|
<img
|
||||||
|
src={getPreviewURL(posterImage)}
|
||||||
|
alt="Poster preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100px',
|
||||||
|
maxHeight: '100px',
|
||||||
|
marginTop: '10px',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* <div className='blog-form-field'>
|
||||||
|
<label>Publish Date (optional - leave empty for immediate publish)</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={publishDate}
|
||||||
|
onChange={(e) => setPublishDate(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='blog-form-field'>
|
||||||
|
<label>Publish Time (optional)</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={publishTime}
|
||||||
|
onChange={(e) => setPublishTime(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
{/* Details Section */}
|
||||||
|
{/* <div className='blog-form-container'> */}
|
||||||
|
<div className='section-header' style={{ marginTop: "3rem" }}>
|
||||||
|
{/* <h3 onClick={handleAddmoreDetails} style={{ cursor: 'pointer' }}>
|
||||||
|
{moreDetails ? '▼ Hide Details' : '▶ Add More Details'}
|
||||||
|
</h3> */}
|
||||||
|
{!moreDetails ?
|
||||||
|
<h3 onClick={handleAddmoreDetails} style={{ cursor: 'pointer' }}>
|
||||||
|
Add More Details
|
||||||
|
</h3> :
|
||||||
|
<div onClick={handleAddmoreDetails} style={{ cursor: 'pointer' }}>
|
||||||
|
Hide Details
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{moreDetails && (
|
||||||
|
<>
|
||||||
|
{detailSections.map((section, sectionIndex) => (
|
||||||
|
|
||||||
|
<div key={sectionIndex} className='detail-section'>
|
||||||
|
<div>More Details ({detailSections.length})</div>
|
||||||
|
<div className='blog-form-field'>
|
||||||
|
<label>Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={section.DtlTitle}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlTitle = e.target.value
|
||||||
|
setDetailSections(updated)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dynamic-inputs'>
|
||||||
|
<div className='input-group'>
|
||||||
|
<div className='group-header'>
|
||||||
|
<label>Description</label>
|
||||||
|
<button className='add-btn' onClick={() => addDescription(sectionIndex)}>+ Add</button>
|
||||||
|
</div>
|
||||||
|
{section?.DtlDescription.map((desc, descIndex) => (
|
||||||
|
<div key={descIndex} className='input-wrapper'>
|
||||||
|
<textarea
|
||||||
|
value={desc}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = [...detailSections]
|
||||||
|
updated[sectionIndex].DtlDescription[descIndex] = e.target.value
|
||||||
|
setDetailSections(updated)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{section?.DtlDescription.length > 1 && (
|
||||||
|
<button
|
||||||
|
className='remove-btn'
|
||||||
|
onClick={() => removeDescription(sectionIndex, descIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='input-group'>
|
||||||
|
<div className='group-header'>
|
||||||
|
<label>Images</label>
|
||||||
|
</div>
|
||||||
|
{(section?.DtlImages || [null])?.map((img, imgIndex) => (
|
||||||
|
<div key={imgIndex} className='input-wrapper'>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => handleFileUpload(e.target.files[0], 'image', sectionIndex, imgIndex)}
|
||||||
|
/>
|
||||||
|
{img && (
|
||||||
|
<div className='file-preview'>
|
||||||
|
<img
|
||||||
|
src={getPreviewURL(img)}
|
||||||
|
alt={`Image ${imgIndex + 1}`}
|
||||||
|
style={{
|
||||||
|
maxWidth: '150px',
|
||||||
|
maxHeight: '100px',
|
||||||
|
marginTop: '8px',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='button-group'>
|
||||||
|
<button className='add-btn' onClick={() => addImage(sectionIndex)}>Add</button>
|
||||||
|
{section?.DtlImages?.length > 1 && (
|
||||||
|
<button
|
||||||
|
className='remove-btn'
|
||||||
|
onClick={() => removeImage(sectionIndex, imgIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='input-group'>
|
||||||
|
<div className='group-header'>
|
||||||
|
<label>Videos</label>
|
||||||
|
</div>
|
||||||
|
{(section?.DtlVideos || [null])?.map((vid, vidIndex) => (
|
||||||
|
<div key={vidIndex} className='input-wrapper'>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="video/*"
|
||||||
|
onChange={(e) => handleFileUpload(e.target.files[0], 'video', sectionIndex, vidIndex)}
|
||||||
|
/>
|
||||||
|
{vid && (
|
||||||
|
<div className='file-preview'>
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
style={{
|
||||||
|
maxWidth: '200px',
|
||||||
|
maxHeight: '120px',
|
||||||
|
marginTop: '8px',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src={typeof vid === 'string' ? vid : URL.createObjectURL(vid)}
|
||||||
|
type={typeof vid === 'string' ? 'video/mp4' : vid.type}
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='button-group'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='add-btn'
|
||||||
|
onClick={() => addVideo(sectionIndex)}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{section?.DtlVideos?.length > 1 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='remove-btn'
|
||||||
|
onClick={() => removeVideo(sectionIndex, vidIndex)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className='section-buttons'>
|
||||||
|
<button className='add-btn' onClick={addDetailSection}>+ Add More</button>
|
||||||
|
{detailSections.length > 1 && (
|
||||||
|
<button className='remove-btn' onClick={() => removeDetailSection(detailSections.length - 1)}>Remove</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* </div> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='blog-form-actions'>
|
||||||
|
<button
|
||||||
|
className='blog-form-btn-primary'
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button className='blog-form-btn-secondary' onClick={handleCancel}>Cancel</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Clear form?"
|
||||||
|
description="This will clear all your work "
|
||||||
|
onConfirm={handleClear}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='blog-form-btn-secondary'>Clear</button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showForm && (
|
||||||
|
<div className='blog-form-right'>
|
||||||
|
<div className="blogFilter">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="search-container">
|
||||||
|
<RiSearchLine className="search-icon" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="search-input"
|
||||||
|
placeholder="Search blogs by title, subtitle, or author..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Filter */}
|
||||||
|
<div className="filter-buttons">
|
||||||
|
<span className="filter-label">Filter:</span>
|
||||||
|
{[
|
||||||
|
{ value: 'all', label: 'All', count: savedBlogs?.length || 0 },
|
||||||
|
{ value: 'published', label: 'Published', count: savedBlogs?.filter(b => b.IsPublished === 'Y')?.length || 0 },
|
||||||
|
{ value: 'unpublished', label: 'Unpublished', count: savedBlogs?.filter(b => b.IsPublished === 'N')?.length || 0 },
|
||||||
|
{ value: 'draft', label: 'Draft', count: savedBlogs?.filter(b => !b.PublishDate && b.IsPublished === 'N')?.length || 0 }
|
||||||
|
].map(filter => (
|
||||||
|
<button
|
||||||
|
key={filter.value}
|
||||||
|
className={`filter-btn ${filterStatus === filter.value ? 'active' : ''}`}
|
||||||
|
onClick={() => setFilterStatus(filter.value)}
|
||||||
|
>
|
||||||
|
{filter.label}
|
||||||
|
<span className={`count-badge ${filterStatus === filter.value ? 'active' : ''}`}>
|
||||||
|
{filter.count}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{filteredBlogs?.length > 0 ? (
|
||||||
|
<div className='blog-display'>
|
||||||
|
<div className='blog-grid'>
|
||||||
|
{filteredBlogs?.map((blog, index) => (
|
||||||
|
<div key={blog.BlogId} className={`blog-card ${blog.IsPublished === 'N' ? 'blog-inactive' : ''}`}>
|
||||||
|
|
||||||
|
{/* Blog Header */}
|
||||||
|
<div className='blog-card-header'>
|
||||||
|
<h3>{blog.BlogTitle}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blog Content */}
|
||||||
|
<div className='blog-card-content'>
|
||||||
|
{blog.BlogSubtitle && <p> {blog.BlogSubtitle}</p>}
|
||||||
|
|
||||||
|
{/* Poster Image */}
|
||||||
|
{blog.HeaderImage && (
|
||||||
|
<div className='poster-preview'>
|
||||||
|
<img
|
||||||
|
src={getPreviewURL(blog.HeaderImage)}
|
||||||
|
alt="Poster"
|
||||||
|
style={{
|
||||||
|
width: '80px',
|
||||||
|
maxHeight: '120px',
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginBottom: '10px'
|
||||||
|
}}
|
||||||
|
onClick={() => openModal(blog.HeaderImage, 'image')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Blog Details */}
|
||||||
|
{blog?.BlogDetails?.map((section, sIndex) => (
|
||||||
|
(section.DtlTitle || section.DtlDescription?.length > 0 || section.DtlImages?.length > 0 || section.DtlVideos?.length > 0) && (
|
||||||
|
<div key={sIndex}>
|
||||||
|
{section.DtlTitle && <p><strong>Section {sIndex + 1}:</strong> {section.DtlTitle}</p>}
|
||||||
|
|
||||||
|
{/* Descriptions */}
|
||||||
|
{section.DtlDescription?.map((desc, i) => (
|
||||||
|
<p key={i}>• {desc.substring(0, 50)}{desc.length > 50 ? '...' : ''}</p>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Section Images */}
|
||||||
|
{section.DtlImages?.length > 0 && (
|
||||||
|
<div className='media-preview'>
|
||||||
|
<p><RiImageLine style={{ marginRight: '5px' }} /> {section.DtlImages.length} image(s)</p>
|
||||||
|
<div style={{ display: 'flex', gap: '5px', flexWrap: 'wrap', marginBottom: '8px' }}>
|
||||||
|
{section.DtlImages.slice(0, 3).map((img, imgIdx) => (
|
||||||
|
<img
|
||||||
|
key={imgIdx}
|
||||||
|
src={getPreviewURL(img)}
|
||||||
|
alt={`Image ${imgIdx + 1}`}
|
||||||
|
style={{
|
||||||
|
maxWidth: '150px',
|
||||||
|
maxHeight: '100px',
|
||||||
|
marginTop: '8px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => openModal(img, 'image')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{section.DtlImages.length > 3 && (
|
||||||
|
<div style={{
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
+{section.DtlImages.length - 3}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Section Videos */}
|
||||||
|
{section.DtlVideos?.length > 0 && (
|
||||||
|
<div className='media-preview'>
|
||||||
|
<p><RiVideoLine style={{ marginRight: '5px' }} /> {section.DtlVideos.length} video(s)</p>
|
||||||
|
<div style={{ display: 'flex', gap: '5px', flexWrap: 'wrap', marginBottom: '8px' }}>
|
||||||
|
{section.DtlVideos.slice(0, 2).map((vid, vidIdx) => (
|
||||||
|
<div
|
||||||
|
key={vidIdx}
|
||||||
|
style={{
|
||||||
|
width: '60px',
|
||||||
|
height: '40px',
|
||||||
|
background: '#000',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '16px'
|
||||||
|
}}
|
||||||
|
onClick={() => openModal(vid, 'video')}
|
||||||
|
>
|
||||||
|
<RiPlayFill />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{section.DtlVideos.length > 2 && (
|
||||||
|
<div style={{
|
||||||
|
width: '60px',
|
||||||
|
height: '40px',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
+{section.DtlVideos.length - 2}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Blog Status & Schedule */}
|
||||||
|
<p className={`blog-status ${blog.IsPublished === 'Y' ? 'blog-active' : 'blog-inactive'}`}>
|
||||||
|
Status: {blog.IsPublished === 'Y' ? 'Published' : 'Unpublished'}
|
||||||
|
</p>
|
||||||
|
{blog.PublishDate && (
|
||||||
|
<p><strong>Scheduled:</strong> {blog.PublishDate?.split('T')?.[0]} {formatTo12Hour(blog.PublishTime) || '00:00'}</p>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', color: '#666', fontFamily: "Poppins" }}>
|
||||||
|
Posted By: {blog.Author || "Admin"}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px', color: '#666', fontFamily: "Poppins" }}>
|
||||||
|
Created: {blog.CreatedDate ? new Date(blog.CreatedDate).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : 'N/A'} {blog.CreatedDate ? formatTo12Hour(blog.CreatedDate.split('T')[1]?.split('.')[0] || '00:00:00') : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className='blog-card-actions'>
|
||||||
|
<button className='blog-edit-btn' onClick={() => handleEdit(index)}><AiOutlineEdit size={18} /></button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Blog"
|
||||||
|
description="Are you sure you want to delete this blog post?"
|
||||||
|
onConfirm={() => handleDelete(blog?.BlogId, blog?.ActiveStatus)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='blog-delete-btn'><AiOutlineDelete size={18} /></button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`blog-status-btn ${blog.IsPublished === 'Y' ? 'blog-deactivate' : 'blog-activate'}`}
|
||||||
|
onClick={() => togglePublish(index)}
|
||||||
|
>
|
||||||
|
{blog.IsPublished === 'Y' ? 'Unpublish' : 'Publish'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='blog-placeholder-content'>
|
||||||
|
<div className='blog-placeholder-icon'>
|
||||||
|
<RiArticleLine />
|
||||||
|
</div>
|
||||||
|
<h2>No Blog Posts Found</h2>
|
||||||
|
<p>Click "Create Blog Post" to start creating a new blog post</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DefaultModal
|
||||||
|
open={modalOpen}
|
||||||
|
title={modalType === 'image' ? 'Image Preview' : 'Video Preview'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
{modalContent && modalType === 'image' && (
|
||||||
|
<img
|
||||||
|
src={getPreviewURL(modalContent)}
|
||||||
|
alt="Preview"
|
||||||
|
style={{ width: '100%', maxHeight: '500px', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{modalContent && modalType === 'video' && (
|
||||||
|
<video
|
||||||
|
controls
|
||||||
|
style={{
|
||||||
|
maxWidth: '200px',
|
||||||
|
maxHeight: '120px',
|
||||||
|
marginTop: '8px',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src={getPreviewURL(modalContent)}
|
||||||
|
type={typeof vid === 'string' ? 'video/mp4' : modalContent.type}
|
||||||
|
/>
|
||||||
|
</video>
|
||||||
|
)}
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogForm
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { useAdminPanel } from '../AdminPanelContext'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/CTASectionForm.scss"
|
||||||
|
import { getSession } from "../../Services/others.js"
|
||||||
|
import { getAdminPanel, postAdminPanel, putAdminPanel } from '../../features/AdminPanel/AdminPanel.js'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { Messages } from '../../Components/Notifications/Messages.jsx'
|
||||||
|
|
||||||
|
const CTASectionForm = ({ sectionKey = null }) => {
|
||||||
|
const [editState, setEditState] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
// const { sectionData, updateSectionData } = useAdminPanel()
|
||||||
|
// const ctaData = sectionData.CTASectionForm || [];
|
||||||
|
const [messageData, setMessageData] = useState(null);
|
||||||
|
const [messageType, setMessageType] = useState(null);
|
||||||
|
const [ctaSectionData, setCTASectionData] = useState([]);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [subtitle, setSubtitle] = useState('')
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1);
|
||||||
|
const [editData, setEditData] = useState(null);
|
||||||
|
const [titleError, setTitleError] = useState('')
|
||||||
|
const [subtitleError, setSubtitleError] = useState('')
|
||||||
|
const UserId = getSession('UserId') || 1;
|
||||||
|
console.log(editingIndex, "editingIndex")
|
||||||
|
// UUID generator fallback for browser compatibility
|
||||||
|
const generateUUID = () => {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = Math.random() * 16 | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getCTASectionData = async () => {
|
||||||
|
const res = await dispatch(getAdminPanel({ sectionName: sectionKey }))?.unwrap();
|
||||||
|
if (res?.data?.statusCode === 1) {
|
||||||
|
setCTASectionData(res?.data?.data);
|
||||||
|
} else {
|
||||||
|
setCTASectionData([]);
|
||||||
|
setMessageData(res?.data?.response);
|
||||||
|
setMessageType("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sectionKey) {
|
||||||
|
getCTASectionData();
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [sectionKey])
|
||||||
|
|
||||||
|
const openModal = (isEdit = false) => {
|
||||||
|
setEditState(isEdit)
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setTitle('')
|
||||||
|
setSubtitle('')
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setTitleError('')
|
||||||
|
setSubtitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setSubtitle('')
|
||||||
|
setTitleError('')
|
||||||
|
setSubtitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// debugger
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
setTitleError('Title is required');
|
||||||
|
hasError = true;
|
||||||
|
} else setTitleError('');
|
||||||
|
|
||||||
|
if (!subtitle.trim()) {
|
||||||
|
setSubtitleError('Subtitle is required');
|
||||||
|
hasError = true;
|
||||||
|
} else setSubtitleError('');
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
message.error('Please fix the highlighted fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
SectionName: sectionKey,
|
||||||
|
SectionHdr: title,
|
||||||
|
SectionDesc: subtitle,
|
||||||
|
HomePageDetails: [],
|
||||||
|
CreatedBy: UserId,
|
||||||
|
...(editingIndex > -1 && editData ? { SectionId: editData.SectionId } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiAction = editingIndex > -1 ? putAdminPanel : postAdminPanel;
|
||||||
|
const res = await dispatch(apiAction(data))?.unwrap();
|
||||||
|
console.log(`${editingIndex > -1 ? 'Put' : 'Post'} response:`, res);
|
||||||
|
|
||||||
|
const success = res?.data?.statusCode === 1;
|
||||||
|
const messageText = editingIndex > -1
|
||||||
|
? "Section Updated Successfully"
|
||||||
|
: "Section Added Successfully";
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setMessageData(messageText);
|
||||||
|
setMessageType("success");
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
const refreshRes = await dispatch(getAdminPanel({ sectionName: sectionKey }))?.unwrap();
|
||||||
|
if (refreshRes?.data?.statusCode === 1) {
|
||||||
|
setCTASectionData(refreshRes.data.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMessage = res?.response || res?.data?.response || res?.message || "Error saving section";
|
||||||
|
setMessageData(errorMessage);
|
||||||
|
setMessageType("error");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
setMessageData(error?.message || "Network error occurred");
|
||||||
|
setMessageType("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedCTAs = ctaSectionData.filter((_, i) => i !== index)
|
||||||
|
setCTASectionData(updatedCTAs)
|
||||||
|
message.success('CTA section deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (ctaData, index) => {
|
||||||
|
setTitle(ctaSectionData[index].SectionHdr || '')
|
||||||
|
setSubtitle(ctaSectionData[index].SectionDesc || '')
|
||||||
|
setEditingIndex(index);
|
||||||
|
setEditData(ctaData)
|
||||||
|
openModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...ctaSectionData]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
setCTASectionData(updated)
|
||||||
|
message.success('Status updated')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onComplete = useCallback(() => {
|
||||||
|
setMessageData(null);
|
||||||
|
setMessageType(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cta-section-form-master'>
|
||||||
|
<Messages
|
||||||
|
messageType={messageType}
|
||||||
|
messageData={messageData}
|
||||||
|
onComplete={onComplete}
|
||||||
|
/>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='cta-section-form-header'>
|
||||||
|
<div className='cta-section-header-left'>
|
||||||
|
{/* <div className='cta-section-header-icon'>
|
||||||
|
<span>📢</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='cta-section-header-content'>
|
||||||
|
<h2>CTA Section Management</h2>
|
||||||
|
<p>Manage and organize your call-to-action content and settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => openModal(false)} className='cta-section-create-btn'>
|
||||||
|
<span>+</span> Create CTA Section
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='cta-section-tab-navigation'>
|
||||||
|
<div className='cta-section-tab-buttons'>
|
||||||
|
<button className='cta-section-tab-btn cta-section-active'>
|
||||||
|
CTA Sections ({ctaSectionData.length})
|
||||||
|
</button>
|
||||||
|
<button className='cta-section-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='cta-section-tab-controls'>
|
||||||
|
<div className='cta-section-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='cta-section-view-toggle'>
|
||||||
|
<button className='cta-section-view-btn cta-section-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='cta-section-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{ctaSectionData.length > 0 ? (
|
||||||
|
<div className='cta-section-display'>
|
||||||
|
<div className='cta-section-grid'>
|
||||||
|
{ctaSectionData.map((cta, index) => (
|
||||||
|
<div key={cta.id || index} className={`cta-section-card ${!cta.active ? 'cta-section-inactive' : ''}`}>
|
||||||
|
<div className='cta-section-card-header'>
|
||||||
|
<h3>{cta.SectionHdr}</h3>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='cta-section-card-content'>
|
||||||
|
<p><strong>Subtitle : </strong> {cta.SectionDesc || 'No subtitle'}</p>
|
||||||
|
{/* <p className='cta-section-created-date'>Created: {cta.date || cta.createdAt}</p> */}
|
||||||
|
<p className={`cta-section-status ${cta.active ? 'cta-section-active' : 'cta-section-inactive'}`}>
|
||||||
|
Status: {cta.active ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='cta-section-card-actions'>
|
||||||
|
<button className='cta-section-edit-btn' onClick={() => handleEdit(cta, index)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete CTA Section"
|
||||||
|
description="Are you sure you want to delete this CTA section?"
|
||||||
|
onConfirm={() => handleDelete(index)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='cta-section-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`cta-section-status-btn ${cta.active ? 'cta-section-deactivate' : 'cta-section-activate'}`}
|
||||||
|
onClick={() => handleToggleActive(index)}
|
||||||
|
>
|
||||||
|
{cta.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='cta-section-placeholder-content'>
|
||||||
|
<div className='cta-section-placeholder-icon'>
|
||||||
|
<span>📢</span>
|
||||||
|
</div>
|
||||||
|
<h2>No CTA Sections Found</h2>
|
||||||
|
<p>Get started by creating your first call-to-action section</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingIndex >= 0 ? 'Edit CTA Section' : 'Create New CTA Section'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingIndex >= 0 ? 'Update CTA' : 'Add CTA'}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='cta-section-form'>
|
||||||
|
<div className='cta-section-form-group'>
|
||||||
|
<label htmlFor='ctaTitle'>Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='ctaTitle'
|
||||||
|
value={title?.trim()}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
if (titleError) setTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter CTA title'
|
||||||
|
className={titleError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='cta-section-form-group'>
|
||||||
|
<label htmlFor='ctaSubtitle'>Subtitle</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='ctaSubtitle'
|
||||||
|
value={subtitle?.trim()}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSubtitle(e.target.value)
|
||||||
|
if (subtitleError) setSubtitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter CTA subtitle'
|
||||||
|
className={subtitleError ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{subtitleError && <span className='error-message'>{subtitleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='cta-section-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='cta-section-save-btn'>
|
||||||
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='cta-section-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='cta-section-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CTASectionForm
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { FaSave, FaTimes, FaImage, FaHeading, FaAlignLeft, FaUpload } from 'react-icons/fa'
|
||||||
|
import '../Styles/CreateModalForm.scss'
|
||||||
|
|
||||||
|
const CreateModalForm = ({ sectionType, onClose, onSave }) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
priority: 'medium',
|
||||||
|
image: null,
|
||||||
|
imagePreview: ''
|
||||||
|
})
|
||||||
|
const [errors, setErrors] = useState({})
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
setFormData({ ...formData, image: file })
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setFormData({ ...formData, image: file, imagePreview: reader.result })
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const newErrors = {}
|
||||||
|
if (!formData.title.trim()) newErrors.title = 'Title is required'
|
||||||
|
if (!formData.description.trim()) newErrors.description = 'Description is required'
|
||||||
|
|
||||||
|
setErrors(newErrors)
|
||||||
|
return Object.keys(newErrors).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItem = {
|
||||||
|
id: Date.now(),
|
||||||
|
title: formData.title,
|
||||||
|
description: formData.description,
|
||||||
|
priority: formData.priority,
|
||||||
|
image: formData.imagePreview || 'https://images.unsplash.com/photo-1559027615-cd4628902d4a?w=400&h=300&fit=crop',
|
||||||
|
active: true,
|
||||||
|
date: new Date().toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(newItem)
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setFormData({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
priority: 'medium',
|
||||||
|
image: null,
|
||||||
|
imagePreview: ''
|
||||||
|
})
|
||||||
|
setErrors({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSectionIcon = () => {
|
||||||
|
switch (sectionType) {
|
||||||
|
case 'BannerSectionForm': return <FaImage />
|
||||||
|
case 'CTASectionForm': return <FaHeading />
|
||||||
|
case 'EcosystemForm': return <FaImage />
|
||||||
|
case 'FaqSectionForm': return <FaAlignLeft />
|
||||||
|
case 'FooterForm': return <FaHeading />
|
||||||
|
case 'HeroSectionForm': return <FaImage />
|
||||||
|
case 'OfferingsForm': return <FaImage />
|
||||||
|
case 'TrendingAppsForm': return <FaImage />
|
||||||
|
case 'AppDemo': return <FaImage />
|
||||||
|
default: return <FaImage />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="create-modal-form">
|
||||||
|
<div className="form-header">
|
||||||
|
<div className="header-icon">
|
||||||
|
{getSectionIcon()}
|
||||||
|
</div>
|
||||||
|
<div className="header-content">
|
||||||
|
<h3>Create New {sectionType?.replace('Form', '')}</h3>
|
||||||
|
<p>Add a new item to your {sectionType?.replace('Form', '').toLowerCase()} collection</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-body">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="title">
|
||||||
|
<FaHeading className="label-icon" />
|
||||||
|
Title *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
placeholder="Enter title..."
|
||||||
|
className={errors.title ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{errors.title && <span className="error-text">{errors.title}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="description">
|
||||||
|
<FaAlignLeft className="label-icon" />
|
||||||
|
Description *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
placeholder="Enter description..."
|
||||||
|
rows="4"
|
||||||
|
className={errors.description ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{errors.description && <span className="error-text">{errors.description}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="priority">Priority</label>
|
||||||
|
<select
|
||||||
|
id="priority"
|
||||||
|
value={formData.priority}
|
||||||
|
onChange={(e) => setFormData({ ...formData, priority: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="low">Low Priority</option>
|
||||||
|
<option value="medium">Medium Priority</option>
|
||||||
|
<option value="high">High Priority</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="image">
|
||||||
|
<FaImage className="label-icon" />
|
||||||
|
Image
|
||||||
|
</label>
|
||||||
|
<div className="image-upload">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="image"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="file-input"
|
||||||
|
/>
|
||||||
|
<label htmlFor="image" className="file-label">
|
||||||
|
<FaUpload />
|
||||||
|
Choose Image
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{formData.imagePreview && (
|
||||||
|
<div className="image-preview">
|
||||||
|
<img src={formData.imagePreview} alt="Preview" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" className="cancel-btn" onClick={onClose}>
|
||||||
|
<FaTimes /> Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="save-btn">
|
||||||
|
<FaSave /> Create Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateModalForm
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { useAdminPanel } from '../AdminPanelContext'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/EcosystemForm.scss"
|
||||||
|
|
||||||
|
const EcosystemForm = () => {
|
||||||
|
const { sectionData, updateSectionData } = useAdminPanel()
|
||||||
|
const ecosystemData = sectionData.EcosystemForm || []
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [posters, setPosters] = useState([{ id: 1, title: '', description: '', image: '' }])
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
const [titleError, setTitleError] = useState('')
|
||||||
|
const [posterTitleError, setPosterTitleError] = useState('')
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setTitle('')
|
||||||
|
setPosters([{ id: 1, title: '', description: '', image: '' }])
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setTitleError('')
|
||||||
|
setPosterTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setPosters([{ id: 1, title: '', description: '', image: '' }])
|
||||||
|
setTitleError('')
|
||||||
|
setPosterTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPoster = () => {
|
||||||
|
if (posters.length >= 4) {
|
||||||
|
message.warning('Maximum 4 posters allowed per ecosystem')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newId = Math.max(...posters.map(p => p.id)) + 1
|
||||||
|
setPosters([...posters, { id: newId, title: '', description: '', image: '' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePoster = (id) => {
|
||||||
|
if (posters.length === 1) {
|
||||||
|
message.warning('At least one poster is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPosters(posters.filter(p => p.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePosterChange = (id, field, value) => {
|
||||||
|
setPosters(posters.map(p => p.id === id ? { ...p, [field]: value } : p))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageChange = (id, e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
handlePosterChange(id, 'image', reader.result)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
setTitleError('Title is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasEmptyPoster = posters.some(p => !p.title.trim())
|
||||||
|
if (hasEmptyPoster) {
|
||||||
|
setPosterTitleError('All poster titles are required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setPosterTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
message.error('Please fix the highlighted fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEcosystem = {
|
||||||
|
id: editingIndex >= 0 ? ecosystemData[editingIndex].id : Date.now(),
|
||||||
|
title: title.trim(),
|
||||||
|
posters: posters.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
title: p.title.trim(),
|
||||||
|
description: p.description.trim(),
|
||||||
|
image: p.image
|
||||||
|
})),
|
||||||
|
active: true,
|
||||||
|
createdAt: editingIndex >= 0 ? ecosystemData[editingIndex].createdAt : new Date().toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex >= 0) {
|
||||||
|
const updatedEcosystems = ecosystemData.map((eco, index) => index === editingIndex ? newEcosystem : eco)
|
||||||
|
updateSectionData('EcosystemForm', updatedEcosystems)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
message.success('Ecosystem updated!')
|
||||||
|
} else {
|
||||||
|
updateSectionData('EcosystemForm', [...ecosystemData, newEcosystem])
|
||||||
|
message.success('Ecosystem added!')
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedEcosystems = ecosystemData.filter((_, i) => i !== index)
|
||||||
|
updateSectionData('EcosystemForm', updatedEcosystems)
|
||||||
|
message.success('Ecosystem deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
const eco = ecosystemData[index]
|
||||||
|
setTitle(eco.title)
|
||||||
|
if (eco.posters && eco.posters.length > 0) {
|
||||||
|
setPosters(eco.posters)
|
||||||
|
} else {
|
||||||
|
// Handle old format
|
||||||
|
setPosters([{
|
||||||
|
id: 1,
|
||||||
|
title: eco.posterTitle || '',
|
||||||
|
description: eco.posterDescription || '',
|
||||||
|
image: eco.image || ''
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
setEditingIndex(index)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...ecosystemData]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
updateSectionData('EcosystemForm', updated)
|
||||||
|
message.success('Status updated')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='ecosystem-section-form-master'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='ecosystem-section-form-header'>
|
||||||
|
<div className='ecosystem-section-header-left'>
|
||||||
|
{/* <div className='ecosystem-section-header-icon'>
|
||||||
|
<span>🌍</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='ecosystem-section-header-content'>
|
||||||
|
<h2>Ecosystem Management</h2>
|
||||||
|
<p>Manage and organize your ecosystem content and settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='ecosystem-section-create-btn'>
|
||||||
|
<span>+</span> Create Ecosystem
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='ecosystem-section-tab-navigation'>
|
||||||
|
<div className='ecosystem-section-tab-buttons'>
|
||||||
|
<button className='ecosystem-section-tab-btn ecosystem-section-active'>
|
||||||
|
Ecosystems ({ecosystemData.length})
|
||||||
|
</button>
|
||||||
|
<button className='ecosystem-section-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='ecosystem-section-tab-controls'>
|
||||||
|
<div className='ecosystem-section-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='ecosystem-section-view-toggle'>
|
||||||
|
<button className='ecosystem-section-view-btn ecosystem-section-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='ecosystem-section-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{ecosystemData.length > 0 ? (
|
||||||
|
<div className='ecosystem-section-display'>
|
||||||
|
<div className='ecosystem-section-grid'>
|
||||||
|
{ecosystemData.map((eco, index) => (
|
||||||
|
<div key={eco.id} className={`ecosystem-section-card ${!eco.active ? 'ecosystem-section-inactive' : ''}`}>
|
||||||
|
<div className='ecosystem-section-card-header'>
|
||||||
|
<h3>{eco.title}</h3>
|
||||||
|
<div className='ecosystem-section-card-actions'>
|
||||||
|
<button className='ecosystem-section-edit-btn' onClick={() => handleEdit(index)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Ecosystem"
|
||||||
|
description="Are you sure you want to delete this ecosystem?"
|
||||||
|
onConfirm={() => handleDelete(index)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='ecosystem-section-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`ecosystem-section-status-btn ${eco.active ? 'ecosystem-section-deactivate' : 'ecosystem-section-activate'}`}
|
||||||
|
onClick={() => handleToggleActive(index)}
|
||||||
|
>
|
||||||
|
{eco.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='ecosystem-section-card-content'>
|
||||||
|
{eco.posters && eco.posters.length > 0 ? (
|
||||||
|
<div className='ecosystem-section-posters'>
|
||||||
|
<strong>Posters ({eco.posters.length}):</strong>
|
||||||
|
{eco.posters.map(poster => (
|
||||||
|
<div key={poster.id} className='ecosystem-section-poster-item'>
|
||||||
|
{poster.image && (
|
||||||
|
<div className='ecosystem-section-poster-image'>
|
||||||
|
<img src={poster.image} alt={poster.title} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='ecosystem-section-poster-content'>
|
||||||
|
<div className='ecosystem-section-poster-title'>{poster.title}</div>
|
||||||
|
{poster.description && <div className='ecosystem-section-poster-desc'>{poster.description}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Fallback for old format
|
||||||
|
<div>
|
||||||
|
{eco.image && (
|
||||||
|
<div className='ecosystem-section-poster-image'>
|
||||||
|
<img src={eco.image} alt={eco.posterTitle} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p><strong>Poster Title:</strong> {eco.posterTitle}</p>
|
||||||
|
<p><strong>Description:</strong> {eco.posterDescription || 'No description'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className='ecosystem-section-created-date'>Created: {eco.date || eco.createdAt}</p>
|
||||||
|
<p className={`ecosystem-section-status ${eco.active ? 'ecosystem-section-active' : 'ecosystem-section-inactive'}`}>
|
||||||
|
Status: {eco.active ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='ecosystem-section-placeholder-content'>
|
||||||
|
{/* <div className='ecosystem-section-placeholder-icon'>
|
||||||
|
<span>🌍</span>
|
||||||
|
</div> */}
|
||||||
|
<h2>No Ecosystems Found</h2>
|
||||||
|
<p>Get started by creating your first ecosystem</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingIndex >= 0 ? 'Edit Ecosystem' : 'Create New Ecosystem'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingIndex >= 0 ? 'Update Ecosystem' : 'Add Ecosystem'}
|
||||||
|
width={700}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='ecosystem-section-form'>
|
||||||
|
<div className='ecosystem-section-form-group'>
|
||||||
|
<label htmlFor='ecosystemTitle'>Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='ecosystemTitle'
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
if (titleError) setTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter ecosystem title'
|
||||||
|
className={titleError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-section-posters-container'>
|
||||||
|
<div className='ecosystem-section-posters-header'>
|
||||||
|
<h3>Ecosystem Posters</h3>
|
||||||
|
<p>Add up to 4 posters for this ecosystem</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{posters.map((poster, idx) => (
|
||||||
|
<div key={poster.id} className='ecosystem-section-poster'>
|
||||||
|
<div className='ecosystem-section-poster-number'>
|
||||||
|
Poster {idx + 1}
|
||||||
|
{posters.length > 1 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='ecosystem-section-remove-poster-btn'
|
||||||
|
onClick={() => removePoster(poster.id)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-section-form-group'>
|
||||||
|
<label>Poster Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Enter poster title'
|
||||||
|
value={poster.title}
|
||||||
|
onChange={(e) => handlePosterChange(poster.id, 'title', e.target.value)}
|
||||||
|
className={posterTitleError ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-section-form-group'>
|
||||||
|
<label>Image Upload</label>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
onChange={(e) => handleImageChange(poster.id, e)}
|
||||||
|
className='ecosystem-section-file-input'
|
||||||
|
/>
|
||||||
|
{poster.image && (
|
||||||
|
<div className='ecosystem-section-image-preview'>
|
||||||
|
<img src={poster.image} alt='Preview' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-section-form-group'>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea
|
||||||
|
placeholder='Enter poster description'
|
||||||
|
value={poster.description}
|
||||||
|
onChange={(e) => handlePosterChange(poster.id, 'description', e.target.value)}
|
||||||
|
rows='3'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{posterTitleError && <span className='error-message'>{posterTitleError}</span>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='ecosystem-section-add-poster-btn'
|
||||||
|
onClick={addPoster}
|
||||||
|
disabled={posters.length >= 4}
|
||||||
|
>
|
||||||
|
+ Add Poster
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-section-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='ecosystem-section-save-btn'>
|
||||||
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='ecosystem-section-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='ecosystem-section-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EcosystemForm
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/FaqSectionForm.scss"
|
||||||
|
|
||||||
|
const FaqSectionForm = () => {
|
||||||
|
const [question, setQuestion] = useState('')
|
||||||
|
const [answer, setAnswer] = useState('')
|
||||||
|
const [faqs, setFaqs] = useState([])
|
||||||
|
const [editingId, setEditingId] = useState(null)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [questionError, setQuestionError] = useState('')
|
||||||
|
const [answerError, setAnswerError] = useState('')
|
||||||
|
const questionInputRef = useRef(null)
|
||||||
|
|
||||||
|
// UUID generator fallback for browser compatibility
|
||||||
|
const generateUUID = () => {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = Math.random() * 16 | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
handleClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
if (!question.trim()) {
|
||||||
|
setQuestionError('Question is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setQuestionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answer.trim()) {
|
||||||
|
setAnswerError('Answer is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setAnswerError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
message.error('Please fix the highlighted fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFaq = {
|
||||||
|
id: editingId || generateUUID(),
|
||||||
|
question: question.trim(),
|
||||||
|
answer: answer.trim(),
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date().toLocaleDateString('en-US')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingId) {
|
||||||
|
setFaqs(prev => prev.map(faq => faq.id === editingId ? newFaq : faq))
|
||||||
|
setEditingId(null)
|
||||||
|
message.success('FAQ updated!')
|
||||||
|
} else {
|
||||||
|
setFaqs(prev => [...prev, newFaq])
|
||||||
|
message.success('FAQ added!')
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setQuestion('')
|
||||||
|
setAnswer('')
|
||||||
|
setEditingId(null)
|
||||||
|
setQuestionError('')
|
||||||
|
setAnswerError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (faq) => {
|
||||||
|
setQuestion(faq.question)
|
||||||
|
setAnswer(faq.answer)
|
||||||
|
setEditingId(faq.id)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingId && questionInputRef.current) {
|
||||||
|
questionInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [editingId])
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
setFaqs(prev => prev.filter(faq => faq.id !== id))
|
||||||
|
message.success('FAQ deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleStatus = (id) => {
|
||||||
|
setFaqs(prev => prev.map(faq =>
|
||||||
|
faq.id === id ? { ...faq, isActive: !faq.isActive } : faq
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='faq-form-master'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='faq-form-header'>
|
||||||
|
<div className='faq-header-left'>
|
||||||
|
{/* <div className='faq-header-icon'>
|
||||||
|
<span>❓</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='faq-header-content'>
|
||||||
|
<h2>FAQ Management</h2>
|
||||||
|
<p>Manage and organize your frequently asked questions.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='faq-create-btn'>
|
||||||
|
<span>+</span> Create FAQ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='faq-tab-navigation'>
|
||||||
|
<div className='faq-tab-buttons'>
|
||||||
|
<button className='faq-tab-btn faq-active'>
|
||||||
|
FAQs ({faqs.length})
|
||||||
|
</button>
|
||||||
|
<button className='faq-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='faq-tab-controls'>
|
||||||
|
<div className='faq-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='faq-view-toggle'>
|
||||||
|
<button className='faq-view-btn faq-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='faq-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{faqs.length > 0 ? (
|
||||||
|
<div className='faq-display'>
|
||||||
|
<div className='faq-grid'>
|
||||||
|
{faqs.map((faq, index) => (
|
||||||
|
<div key={faq.id} className={`faq-card ${!faq.isActive ? 'faq-inactive' : ''}`}>
|
||||||
|
<div className='faq-card-header'>
|
||||||
|
<h3>{faq.question}</h3>
|
||||||
|
<div className='faq-card-actions'>
|
||||||
|
<button className='faq-edit-btn' onClick={() => handleEdit(faq)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete FAQ"
|
||||||
|
description="Are you sure you want to delete this FAQ?"
|
||||||
|
onConfirm={() => handleDelete(faq.id)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='faq-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`faq-status-btn ${faq.isActive ? 'faq-deactivate' : 'faq-activate'}`}
|
||||||
|
onClick={() => handleToggleStatus(faq.id)}
|
||||||
|
>
|
||||||
|
{faq.isActive ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='faq-card-content'>
|
||||||
|
<p><strong>Answer:</strong> {faq.answer}</p>
|
||||||
|
<p className='faq-created-date'>Created: {faq.createdAt}</p>
|
||||||
|
<p className={`faq-status ${faq.isActive ? 'faq-active' : 'faq-inactive'}`}>
|
||||||
|
Status: {faq.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='faq-placeholder-content'>
|
||||||
|
<div className='faq-placeholder-icon'>
|
||||||
|
<span>❓</span>
|
||||||
|
</div>
|
||||||
|
<h2>No FAQs Found</h2>
|
||||||
|
<p>Get started by creating your first FAQ</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingId ? 'Edit FAQ' : 'Create New FAQ'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingId ? 'Update FAQ' : 'Add FAQ'}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='faq-form'>
|
||||||
|
<div className='faq-form-group'>
|
||||||
|
<label htmlFor='question'>Question</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='question'
|
||||||
|
ref={questionInputRef}
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => {
|
||||||
|
setQuestion(e.target.value)
|
||||||
|
if (questionError) setQuestionError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter FAQ question'
|
||||||
|
className={questionError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{questionError && <span className='error-message'>{questionError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='faq-form-group'>
|
||||||
|
<label htmlFor='answer'>Answer</label>
|
||||||
|
<textarea
|
||||||
|
id='answer'
|
||||||
|
value={answer}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAnswer(e.target.value)
|
||||||
|
if (answerError) setAnswerError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter FAQ answer'
|
||||||
|
className={answerError ? 'error' : ''}
|
||||||
|
rows='4'
|
||||||
|
/>
|
||||||
|
{answerError && <span className='error-message'>{answerError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='faq-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='faq-save-btn'>
|
||||||
|
{editingId ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='faq-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='faq-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FaqSectionForm
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import "../Styles/FooterForm.scss"
|
||||||
|
|
||||||
|
const FooterForm = () => {
|
||||||
|
const [mobileNumber, setMobileNumber] = useState('')
|
||||||
|
const [savedNumbers, setSavedNumbers] = useState([])
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
|
||||||
|
const [socialImage, setSocialImage] = useState('')
|
||||||
|
const [socialLink, setSocialLink] = useState('')
|
||||||
|
const [savedSocialLinks, setSavedSocialLinks] = useState([])
|
||||||
|
const [editingSocialIndex, setEditingSocialIndex] = useState(-1)
|
||||||
|
|
||||||
|
const handleNumberChange = (e) => {
|
||||||
|
const value = e.target.value.replace(/[^0-9]/g, '')
|
||||||
|
setMobileNumber(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (mobileNumber.trim()) {
|
||||||
|
const newNumber = {
|
||||||
|
number: mobileNumber,
|
||||||
|
active: true,
|
||||||
|
id: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex >= 0) {
|
||||||
|
const updated = [...savedNumbers]
|
||||||
|
updated[editingIndex] = { ...updated[editingIndex], number: mobileNumber }
|
||||||
|
setSavedNumbers(updated)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
} else {
|
||||||
|
setSavedNumbers([...savedNumbers, newNumber])
|
||||||
|
}
|
||||||
|
|
||||||
|
setMobileNumber('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setMobileNumber('')
|
||||||
|
setEditingIndex(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
setMobileNumber(savedNumbers[index].number)
|
||||||
|
setEditingIndex(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
setSavedNumbers(savedNumbers.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...savedNumbers]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
setSavedNumbers(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSocialSave = () => {
|
||||||
|
if (socialImage.trim() && socialLink.trim()) {
|
||||||
|
const newSocialLink = {
|
||||||
|
image: socialImage,
|
||||||
|
link: socialLink,
|
||||||
|
active: true,
|
||||||
|
id: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingSocialIndex >= 0) {
|
||||||
|
const updated = [...savedSocialLinks]
|
||||||
|
updated[editingSocialIndex] = { ...updated[editingSocialIndex], image: socialImage, link: socialLink }
|
||||||
|
setSavedSocialLinks(updated)
|
||||||
|
setEditingSocialIndex(-1)
|
||||||
|
} else {
|
||||||
|
setSavedSocialLinks([...savedSocialLinks, newSocialLink])
|
||||||
|
}
|
||||||
|
|
||||||
|
setSocialImage('')
|
||||||
|
setSocialLink('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSocialClear = () => {
|
||||||
|
setSocialImage('')
|
||||||
|
setSocialLink('')
|
||||||
|
setEditingSocialIndex(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSocialEdit = (index) => {
|
||||||
|
setSocialImage(savedSocialLinks[index].image)
|
||||||
|
setSocialLink(savedSocialLinks[index].link)
|
||||||
|
setEditingSocialIndex(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSocialDelete = (index) => {
|
||||||
|
setSavedSocialLinks(savedSocialLinks.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSocialToggleActive = (index) => {
|
||||||
|
const updated = [...savedSocialLinks]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
setSavedSocialLinks(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='FooterFormMaster'>
|
||||||
|
<div className="footer-header">
|
||||||
|
<h1>Footer Settings</h1>
|
||||||
|
</div>
|
||||||
|
<div className="footer-form">
|
||||||
|
<div className="input-group">
|
||||||
|
<label htmlFor="mobileNumber">Mobile Number</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="mobileNumber"
|
||||||
|
value={mobileNumber}
|
||||||
|
onChange={handleNumberChange}
|
||||||
|
placeholder="Enter mobile number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-buttons">
|
||||||
|
<button onClick={handleSave} className="save-btn">Save</button>
|
||||||
|
<button onClick={handleClear} className="clear-btn">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="saved-numbers">
|
||||||
|
<h3>Saved Mobile Numbers:</h3>
|
||||||
|
{savedNumbers.length > 0 ? (
|
||||||
|
<div className="numbers-cards">
|
||||||
|
{savedNumbers.map((numberObj, index) => (
|
||||||
|
<div key={numberObj.id} className={`number-card ${!numberObj.active ? 'inactive' : ''}`}>
|
||||||
|
<div className="card-content">
|
||||||
|
<div className="number-header">
|
||||||
|
<h4>{numberObj.number}</h4>
|
||||||
|
<span className={`status-badge ${numberObj.active ? 'active' : 'inactive'}`}>
|
||||||
|
{numberObj.active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">
|
||||||
|
<span className="card-badge">Number #{index + 1}</span>
|
||||||
|
<div className="number-actions">
|
||||||
|
<button onClick={() => handleEdit(index)} className="edit-btn">Edit</button>
|
||||||
|
<button onClick={() => handleDelete(index)} className="delete-btn">Delete</button>
|
||||||
|
<button onClick={() => handleToggleActive(index)} className="toggle-btn">
|
||||||
|
{numberObj.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="no-data">
|
||||||
|
<p>No mobile numbers found. Add your first number above.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="social-media-section">
|
||||||
|
<div className="input-group">
|
||||||
|
<label htmlFor="socialImage">Social Media Image URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="socialImage"
|
||||||
|
value={socialImage}
|
||||||
|
onChange={(e) => setSocialImage(e.target.value)}
|
||||||
|
placeholder="Enter image URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="input-group">
|
||||||
|
<label htmlFor="socialLink">Social Media Link</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="socialLink"
|
||||||
|
value={socialLink}
|
||||||
|
onChange={(e) => setSocialLink(e.target.value)}
|
||||||
|
placeholder="Enter social media link"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-buttons">
|
||||||
|
<button onClick={handleSocialSave} className="save-btn">Save</button>
|
||||||
|
<button onClick={handleSocialClear} className="clear-btn">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="saved-numbers">
|
||||||
|
<h3>Saved Social Media Links:</h3>
|
||||||
|
{savedSocialLinks.length > 0 ? (
|
||||||
|
<div className="numbers-cards">
|
||||||
|
{savedSocialLinks.map((socialObj, index) => (
|
||||||
|
<div key={socialObj.id} className={`number-card ${!socialObj.active ? 'inactive' : ''}`}>
|
||||||
|
<div className="card-content">
|
||||||
|
<div className="number-header">
|
||||||
|
<img src={socialObj.image} alt="Social" style={{width: '40px', height: '40px', borderRadius: '4px'}} />
|
||||||
|
<h4>{socialObj.link}</h4>
|
||||||
|
<span className={`status-badge ${socialObj.active ? 'active' : 'inactive'}`}>
|
||||||
|
{socialObj.active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">
|
||||||
|
<span className="card-badge">Link #{index + 1}</span>
|
||||||
|
<div className="number-actions">
|
||||||
|
<button onClick={() => handleSocialEdit(index)} className="edit-btn">Edit</button>
|
||||||
|
<button onClick={() => handleSocialDelete(index)} className="delete-btn">Delete</button>
|
||||||
|
<button onClick={() => handleSocialToggleActive(index)} className="toggle-btn">
|
||||||
|
{socialObj.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="no-data">
|
||||||
|
<p>No social media links found. Add your first link above.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FooterForm
|
||||||
|
|
@ -0,0 +1,415 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { useAdminPanel } from '../AdminPanelContext'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/HeroSectionForm.scss"
|
||||||
|
|
||||||
|
const HeroSectionForm = () => {
|
||||||
|
const { sectionData, updateSectionData } = useAdminPanel()
|
||||||
|
const heroData = sectionData.HeroSectionForm || []
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [subsections, setSubsections] = useState([{ id: 1, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
const [titleError, setTitleError] = useState('')
|
||||||
|
const [descriptionError, setDescriptionError] = useState('')
|
||||||
|
|
||||||
|
// UUID generator fallback for browser compatibility
|
||||||
|
const generateUUID = () => {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
subsections.forEach(s => { if (s.previewUrl) URL.revokeObjectURL(s.previewUrl) })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setTitle('')
|
||||||
|
setDescription('')
|
||||||
|
setSubsections([{ id: 1, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setTitleError('')
|
||||||
|
setDescriptionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setDescription('')
|
||||||
|
setSubsections([{ id: 1, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||||||
|
setTitleError('')
|
||||||
|
setDescriptionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSubsection = () => {
|
||||||
|
if (subsections.length >= 3) {
|
||||||
|
message.warning('Only 3 subsections allowed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newId = Math.max(...subsections.map(s => s.id)) + 1
|
||||||
|
setSubsections([...subsections, { id: newId, image: null, previewUrl: null, heading: '', subheading: '' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSubsection = (id) => {
|
||||||
|
if (subsections.length === 1) {
|
||||||
|
message.warning('At least one subsection is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const toRemove = subsections.find(s => s.id === id)
|
||||||
|
if (toRemove?.previewUrl) {
|
||||||
|
URL.revokeObjectURL(toRemove.previewUrl)
|
||||||
|
}
|
||||||
|
setSubsections(subsections.filter(s => s.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = (id, file) => {
|
||||||
|
setSubsections(subsections.map(s => {
|
||||||
|
if (s.id !== id) return s
|
||||||
|
if (s.previewUrl) {
|
||||||
|
URL.revokeObjectURL(s.previewUrl)
|
||||||
|
}
|
||||||
|
const nextUrl = file ? URL.createObjectURL(file) : null
|
||||||
|
return { ...s, image: file || null, previewUrl: nextUrl }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubsectionChange = (id, field, value) => {
|
||||||
|
setSubsections(subsections.map(s =>
|
||||||
|
s.id === id ? { ...s, [field]: value } : s
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
setTitleError('Title is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description.trim()) {
|
||||||
|
setDescriptionError('Description is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setDescriptionError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
message.error('Please fix the highlighted fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHero = {
|
||||||
|
id: editingIndex >= 0 ? heroData[editingIndex].id : generateUUID(),
|
||||||
|
title: title.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
subsections: subsections.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
heading: s.heading,
|
||||||
|
subheading: s.subheading,
|
||||||
|
displayUrl: s.previewUrl || (s.image ? URL.createObjectURL(s.image) : null),
|
||||||
|
})),
|
||||||
|
active: true,
|
||||||
|
createdAt: editingIndex >= 0 ? heroData[editingIndex].createdAt : new Date().toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex >= 0) {
|
||||||
|
const updatedHeros = heroData.map((hero, index) => index === editingIndex ? newHero : hero)
|
||||||
|
updateSectionData('HeroSectionForm', updatedHeros)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
message.success('Hero section updated!')
|
||||||
|
} else {
|
||||||
|
updateSectionData('HeroSectionForm', [...heroData, newHero])
|
||||||
|
message.success('Hero section added!')
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedHeros = heroData.filter((_, i) => i !== index)
|
||||||
|
updateSectionData('HeroSectionForm', updatedHeros)
|
||||||
|
message.success('Hero section deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
setTitle(heroData[index].title)
|
||||||
|
setDescription(heroData[index].description)
|
||||||
|
if (heroData[index].subsections && heroData[index].subsections.length > 0) {
|
||||||
|
setSubsections(heroData[index].subsections.map(ss => ({
|
||||||
|
id: ss.id,
|
||||||
|
image: null,
|
||||||
|
previewUrl: ss.displayUrl || null,
|
||||||
|
heading: ss.heading || '',
|
||||||
|
subheading: ss.subheading || ''
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
setEditingIndex(index)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...heroData]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
updateSectionData('HeroSectionForm', updated)
|
||||||
|
message.success('Status updated')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='hero-section-form-master'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='hero-section-form-header'>
|
||||||
|
<div className='hero-section-header-left'>
|
||||||
|
<div className='hero-section-header-content'>
|
||||||
|
<h2>Hero Section Management</h2>
|
||||||
|
<p>Manage and organize your hero section content and settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='hero-section-create-btn'>
|
||||||
|
<span>+</span> Create Hero Section
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='hero-section-tab-navigation'>
|
||||||
|
<div className='hero-section-tab-buttons'>
|
||||||
|
<button className='hero-section-tab-btn hero-section-active'>
|
||||||
|
Hero Sections ({heroData.length})
|
||||||
|
</button>
|
||||||
|
<button className='hero-section-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='hero-section-tab-controls'>
|
||||||
|
<div className='hero-section-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='hero-section-view-toggle'>
|
||||||
|
<button className='hero-section-view-btn hero-section-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='hero-section-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{heroData.length > 0 ? (
|
||||||
|
<div className='hero-section-display'>
|
||||||
|
<div className='hero-section-grid'>
|
||||||
|
{heroData.map((hero, index) => (
|
||||||
|
<div key={hero.id} className={`hero-section-card ${!hero.active ? 'hero-section-inactive' : ''}`}>
|
||||||
|
<div className='hero-section-card-header'>
|
||||||
|
<h3>{hero.title}</h3>
|
||||||
|
<div className='hero-section-card-actions'>
|
||||||
|
<button className='hero-section-edit-btn' onClick={() => handleEdit(index)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Hero Section"
|
||||||
|
description="Are you sure you want to delete this hero section?"
|
||||||
|
onConfirm={() => handleDelete(index)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='hero-section-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`hero-section-status-btn ${hero.active ? 'hero-section-deactivate' : 'hero-section-activate'}`}
|
||||||
|
onClick={() => handleToggleActive(index)}
|
||||||
|
>
|
||||||
|
{hero.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='hero-section-card-content'>
|
||||||
|
<p><strong>Description:</strong> {hero.description || 'No description'}</p>
|
||||||
|
<p className='hero-section-created-date'>Created: {hero.date || hero.createdAt}</p>
|
||||||
|
<p className={`hero-section-status ${hero.active ? 'hero-section-active' : 'hero-section-inactive'}`}>
|
||||||
|
Status: {hero.active ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
{hero.subsections && hero.subsections.length > 0 && (
|
||||||
|
<div className='hero-section-subsections'>
|
||||||
|
<strong>Subsections:</strong>
|
||||||
|
{hero.subsections.map(ss => (
|
||||||
|
<div key={ss.id} className='hero-section-subsection-item'>
|
||||||
|
{ss.displayUrl && (
|
||||||
|
<div className='hero-section-thumb'>
|
||||||
|
<img src={ss.displayUrl} alt={ss.heading || 'Subsection'} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='hero-section-texts'>
|
||||||
|
{ss.heading && <div className='hero-section-heading'>{ss.heading}</div>}
|
||||||
|
{ss.subheading && <div className='hero-section-subheading'>{ss.subheading}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='hero-section-placeholder-content'>
|
||||||
|
<div className='hero-section-placeholder-icon'>
|
||||||
|
<span>🎯</span>
|
||||||
|
</div>
|
||||||
|
<h2>No Hero Sections Found</h2>
|
||||||
|
<p>Get started by creating your first hero section</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingIndex >= 0 ? 'Edit Hero Section' : 'Create New Hero Section'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingIndex >= 0 ? 'Update Hero' : 'Add Hero'}
|
||||||
|
width={700}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='hero-section-form'>
|
||||||
|
<div className='hero-section-form-group'>
|
||||||
|
<label htmlFor='heroTitle'>Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='heroTitle'
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
if (titleError) setTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter hero title'
|
||||||
|
className={titleError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hero-section-form-group'>
|
||||||
|
<label htmlFor='heroDescription'>Description</label>
|
||||||
|
<textarea
|
||||||
|
id='heroDescription'
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => {
|
||||||
|
setDescription(e.target.value)
|
||||||
|
if (descriptionError) setDescriptionError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter hero description'
|
||||||
|
rows='4'
|
||||||
|
className={descriptionError ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{descriptionError && <span className='error-message'>{descriptionError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hero-section-subsections-container'>
|
||||||
|
<div className='hero-section-subsections-header'>
|
||||||
|
<h3>Subsections</h3>
|
||||||
|
<p>Add up to 3 subsections with images and content</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subsections.map((subsection, idx) => (
|
||||||
|
<div key={subsection.id} className='hero-section-subsection'>
|
||||||
|
<div className='hero-section-subsection-number'>
|
||||||
|
Subsection {idx + 1}
|
||||||
|
{subsections.length > 1 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='hero-section-remove-subsection-btn'
|
||||||
|
onClick={() => removeSubsection(subsection.id)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hero-section-form-group'>
|
||||||
|
<label>Image Upload</label>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
onChange={(e) => handleImageUpload(subsection.id, e.target.files[0])}
|
||||||
|
className='hero-section-file-input'
|
||||||
|
aria-label={`Upload image for subsection ${idx + 1}`}
|
||||||
|
/>
|
||||||
|
{subsection.previewUrl && (
|
||||||
|
<div className='hero-section-image-preview'>
|
||||||
|
<img src={subsection.previewUrl} alt='Preview' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hero-section-form-group'>
|
||||||
|
<label>Heading</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Enter heading'
|
||||||
|
value={subsection.heading}
|
||||||
|
onChange={(e) => handleSubsectionChange(subsection.id, 'heading', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hero-section-form-group'>
|
||||||
|
<label>Sub Heading</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Enter sub heading'
|
||||||
|
value={subsection.subheading}
|
||||||
|
onChange={(e) => handleSubsectionChange(subsection.id, 'subheading', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='hero-section-add-subsection-btn'
|
||||||
|
onClick={addSubsection}
|
||||||
|
disabled={subsections.length >= 3}
|
||||||
|
>
|
||||||
|
+ Add Subsection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='hero-section-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='hero-section-save-btn'>
|
||||||
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='hero-section-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='hero-section-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeroSectionForm
|
||||||
|
|
@ -0,0 +1,381 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Popconfirm } from 'antd'
|
||||||
|
import { useAdminPanel } from '../AdminPanelContext'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/OfferingsForm.scss"
|
||||||
|
|
||||||
|
const OfferingsForm = () => {
|
||||||
|
const { sectionData, updateSectionData } = useAdminPanel()
|
||||||
|
const ecosystemData = sectionData.EcosystemForm || []
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [posters, setPosters] = useState([{ id: 1, title: '', description: '', image: '' }])
|
||||||
|
const [editingIndex, setEditingIndex] = useState(-1)
|
||||||
|
const [titleError, setTitleError] = useState('')
|
||||||
|
const [posterErrors, setPosterErrors] = useState({})
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setTitle('')
|
||||||
|
setPosters([{ id: 1, title: '', description: '', image: '' }])
|
||||||
|
setEditingIndex(-1)
|
||||||
|
setTitleError('')
|
||||||
|
setPosterErrors({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setPosters([{ id: 1, title: '', description: '', image: '' }])
|
||||||
|
setTitleError('')
|
||||||
|
setPosterErrors({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addPoster = () => {
|
||||||
|
const newId = Math.max(...posters.map(p => p.id)) + 1
|
||||||
|
setPosters([...posters, { id: newId, title: '', description: '', image: '' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePoster = (id) => {
|
||||||
|
if (posters.length > 1) {
|
||||||
|
setPosters(posters.filter(p => p.id !== id))
|
||||||
|
const newErrors = { ...posterErrors }
|
||||||
|
delete newErrors[id]
|
||||||
|
setPosterErrors(newErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePosterChange = (id, field, value) => {
|
||||||
|
setPosters(posters.map(p => p.id === id ? { ...p, [field]: value } : p))
|
||||||
|
if (posterErrors[id]) {
|
||||||
|
const newErrors = { ...posterErrors }
|
||||||
|
delete newErrors[id]
|
||||||
|
setPosterErrors(newErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageChange = (id, e) => {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
handlePosterChange(id, 'image', reader.result)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
let hasError = false
|
||||||
|
const errors = {}
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
setTitleError('Title is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
posters.forEach(poster => {
|
||||||
|
if (!poster.title.trim()) {
|
||||||
|
errors[poster.id] = 'Poster title is required'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setPosterErrors(errors)
|
||||||
|
if (hasError) return
|
||||||
|
|
||||||
|
const newEcosystem = {
|
||||||
|
id: editingIndex >= 0 ? ecosystemData[editingIndex].id : Date.now(),
|
||||||
|
title: title.trim(),
|
||||||
|
posters: posters.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
title: p.title.trim(),
|
||||||
|
description: p.description.trim(),
|
||||||
|
image: p.image
|
||||||
|
})),
|
||||||
|
active: true,
|
||||||
|
createdAt: editingIndex >= 0 ? ecosystemData[editingIndex].createdAt : new Date().toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex >= 0) {
|
||||||
|
const updatedEcosystems = ecosystemData.map((ecosystem, index) => index === editingIndex ? newEcosystem : ecosystem)
|
||||||
|
updateSectionData('EcosystemForm', updatedEcosystems)
|
||||||
|
setEditingIndex(-1)
|
||||||
|
} else {
|
||||||
|
updateSectionData('EcosystemForm', [...ecosystemData, newEcosystem])
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedEcosystems = ecosystemData.filter((_, i) => i !== index)
|
||||||
|
updateSectionData('EcosystemForm', updatedEcosystems)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
setTitle(ecosystemData[index].title)
|
||||||
|
if (ecosystemData[index].posters && ecosystemData[index].posters.length > 0) {
|
||||||
|
setPosters(ecosystemData[index].posters)
|
||||||
|
} else {
|
||||||
|
// Handle legacy data structure
|
||||||
|
setPosters([{
|
||||||
|
id: 1,
|
||||||
|
title: ecosystemData[index].posterTitle || '',
|
||||||
|
description: ecosystemData[index].posterDescription || '',
|
||||||
|
image: ecosystemData[index].posterImage || ''
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
setEditingIndex(index)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleActive = (index) => {
|
||||||
|
const updated = [...ecosystemData]
|
||||||
|
updated[index].active = !updated[index].active
|
||||||
|
updateSectionData('EcosystemForm', updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='OfferingsFormMaster'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='ecosystem-form-header'>
|
||||||
|
<div className='ecosystem-header-left'>
|
||||||
|
{/* <div className='ecosystem-header-icon'>
|
||||||
|
<span>🌱</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='ecosystem-header-content'>
|
||||||
|
<h2>Offerings Management</h2>
|
||||||
|
<p>Manage and organize your ecosystem content and settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='ecosystem-create-btn'>
|
||||||
|
<span>+</span> Create Ecosystem
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='ecosystem-tab-navigation'>
|
||||||
|
<div className='ecosystem-tab-buttons'>
|
||||||
|
<button className='ecosystem-tab-btn ecosystem-active'>
|
||||||
|
Offerings ({ecosystemData.length})
|
||||||
|
</button>
|
||||||
|
<button className='ecosystem-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='ecosystem-tab-controls'>
|
||||||
|
<div className='ecosystem-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='ecosystem-view-toggle'>
|
||||||
|
<button className='ecosystem-view-btn ecosystem-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='ecosystem-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{ecosystemData.length > 0 ? (
|
||||||
|
<div className='ecosystem-display'>
|
||||||
|
<div className='ecosystem-grid'>
|
||||||
|
{ecosystemData.map((ecosystem, index) => (
|
||||||
|
<div key={ecosystem.id} className={`ecosystem-card ${!ecosystem.active ? 'ecosystem-inactive' : ''}`}>
|
||||||
|
<div className='ecosystem-card-header'>
|
||||||
|
<h3>{ecosystem.title}</h3>
|
||||||
|
<div className='ecosystem-card-actions'>
|
||||||
|
<button className='ecosystem-edit-btn' onClick={() => handleEdit(index)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Ecosystem"
|
||||||
|
description="Are you sure you want to delete this ecosystem?"
|
||||||
|
onConfirm={() => handleDelete(index)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='ecosystem-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`ecosystem-status-btn ${ecosystem.active ? 'ecosystem-deactivate' : 'ecosystem-activate'}`}
|
||||||
|
onClick={() => handleToggleActive(index)}
|
||||||
|
>
|
||||||
|
{ecosystem.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='ecosystem-card-content'>
|
||||||
|
{ecosystem.posters && ecosystem.posters.length > 0 ? (
|
||||||
|
<div className='ecosystem-posters'>
|
||||||
|
<strong>Posters ({ecosystem.posters.length}):</strong>
|
||||||
|
{ecosystem.posters.map(poster => (
|
||||||
|
<div key={poster.id} className='ecosystem-poster-item'>
|
||||||
|
{poster.image && (
|
||||||
|
<div className='ecosystem-poster-image'>
|
||||||
|
<img src={poster.image} alt={poster.title} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='ecosystem-poster-content'>
|
||||||
|
<p><strong>{poster.title}</strong></p>
|
||||||
|
{poster.description && <p>{poster.description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Legacy data structure support
|
||||||
|
<>
|
||||||
|
{ecosystem.posterImage && (
|
||||||
|
<div className='ecosystem-poster-image'>
|
||||||
|
<img src={ecosystem.posterImage} alt={ecosystem.posterTitle} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p><strong>Poster Title:</strong> {ecosystem.posterTitle}</p>
|
||||||
|
<p><strong>Description:</strong> {ecosystem.posterDescription || 'No description'}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<p className='ecosystem-created-date'>Created: {ecosystem.date || ecosystem.createdAt}</p>
|
||||||
|
<p className={`ecosystem-status ${ecosystem.active ? 'ecosystem-active' : 'ecosystem-inactive'}`}>
|
||||||
|
Status: {ecosystem.active ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='ecosystem-placeholder-content'>
|
||||||
|
<div className='ecosystem-placeholder-icon'>
|
||||||
|
<span>🌱</span>
|
||||||
|
</div>
|
||||||
|
<h2>No Ecosystems Found</h2>
|
||||||
|
<p>Get started by creating your first ecosystem</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingIndex >= 0 ? 'Edit Ecosystem' : 'Create New Ecosystem'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingIndex >= 0 ? 'Update Ecosystem' : 'Add Ecosystem'}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='ecosystem-form'>
|
||||||
|
<div className='ecosystem-form-group'>
|
||||||
|
<label htmlFor='ecosystemTitle'>Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='ecosystemTitle'
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
if (titleError) setTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter ecosystem title'
|
||||||
|
className={titleError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='posters-container'>
|
||||||
|
<div className='posters-header'>
|
||||||
|
<h3>Poster Sections</h3>
|
||||||
|
<p>Add multiple poster sections for your ecosystem</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{posters.map((poster, idx) => (
|
||||||
|
<div key={poster.id} className='poster-section'>
|
||||||
|
<div className='poster-section-header'>
|
||||||
|
<span>Poster {idx + 1}</span>
|
||||||
|
{posters.length > 1 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='remove-poster-btn'
|
||||||
|
onClick={() => removePoster(poster.id)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-form-group'>
|
||||||
|
<label>Poster Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={poster.title}
|
||||||
|
onChange={(e) => handlePosterChange(poster.id, 'title', e.target.value)}
|
||||||
|
placeholder='Enter poster title'
|
||||||
|
className={posterErrors[poster.id] ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{posterErrors[poster.id] && <span className='error-message'>{posterErrors[poster.id]}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-form-group'>
|
||||||
|
<label>Poster Description</label>
|
||||||
|
<textarea
|
||||||
|
value={poster.description}
|
||||||
|
onChange={(e) => handlePosterChange(poster.id, 'description', e.target.value)}
|
||||||
|
placeholder='Enter poster description'
|
||||||
|
rows='3'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-form-group'>
|
||||||
|
<label>Poster Image</label>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
onChange={(e) => handleImageChange(poster.id, e)}
|
||||||
|
className='ecosystem-file-input'
|
||||||
|
/>
|
||||||
|
{poster.image && (
|
||||||
|
<div className='ecosystem-image-preview'>
|
||||||
|
<img src={poster.image} alt='Preview' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='add-poster-btn'
|
||||||
|
onClick={addPoster}
|
||||||
|
>
|
||||||
|
+ Add More Poster
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='ecosystem-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='ecosystem-save-btn'>
|
||||||
|
{editingIndex >= 0 ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='ecosystem-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='ecosystem-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OfferingsForm
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { postSeo, getSeo, putSeo } from "../../features/SEO/seo.js";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { getConfigNames } from "../../features/configmasterPage/configmasterPage.js";
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
// Function to update static HTML (you'll need to implement this API call)
|
||||||
|
const updateStaticHTML = async (seoData) => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/update-seo', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(seoData)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update static HTML:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
import { RiSettings4Line } from 'react-icons/ri';
|
||||||
|
import "../Styles/SeoForm.scss"
|
||||||
|
|
||||||
|
const SeoForm = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [showForm, setShowForm] = useState(true);
|
||||||
|
const [detailSections, setDetailSections] = useState([{}]);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
pageName: "",
|
||||||
|
metaTitle: "",
|
||||||
|
metaDescription: "",
|
||||||
|
keywords: "",
|
||||||
|
imageAltText: "",
|
||||||
|
});
|
||||||
|
const [allpagesData, setAllpagesData] = useState([]);
|
||||||
|
const [isExistingData, setIsExistingData] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPagesName();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getPagesName = async () => {
|
||||||
|
let Res = await dispatch(getConfigNames({ TypeName: "SEO" })).unwrap();
|
||||||
|
if (Res?.data?.statusCode == 1) {
|
||||||
|
setAllpagesData(Res?.data?.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleChange = async (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
// If page is selected, fetch existing SEO data
|
||||||
|
if (name === 'pageName') {
|
||||||
|
console.log('Page selected:', value);
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
console.log('Fetching SEO data for PageId:', value);
|
||||||
|
const response = await dispatch(getSeo({ PageId: value })).unwrap();
|
||||||
|
console.log('SEO API Response:', response);
|
||||||
|
|
||||||
|
if (response?.data?.statusCode === 1 && response?.data?.data?.length > 0) {
|
||||||
|
const seoData = response.data.data[0];
|
||||||
|
console.log('Found existing SEO data:', seoData);
|
||||||
|
setFormData({
|
||||||
|
pageName: value,
|
||||||
|
metaTitle: seoData.MetaTitle || '',
|
||||||
|
metaDescription: seoData.MetaDesc || '',
|
||||||
|
keywords: seoData.Keywords || '',
|
||||||
|
imageAltText: seoData.ImgAltText || '',
|
||||||
|
});
|
||||||
|
setIsExistingData(true);
|
||||||
|
} else {
|
||||||
|
console.log('No existing SEO data found');
|
||||||
|
setFormData({
|
||||||
|
pageName: value,
|
||||||
|
metaTitle: '',
|
||||||
|
metaDescription: '',
|
||||||
|
keywords: '',
|
||||||
|
imageAltText: '',
|
||||||
|
});
|
||||||
|
setIsExistingData(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching SEO data:', error);
|
||||||
|
setFormData({
|
||||||
|
pageName: value,
|
||||||
|
metaTitle: '',
|
||||||
|
metaDescription: '',
|
||||||
|
keywords: '',
|
||||||
|
imageAltText: '',
|
||||||
|
});
|
||||||
|
setIsExistingData(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
pageName: '',
|
||||||
|
metaTitle: '',
|
||||||
|
metaDescription: '',
|
||||||
|
keywords: '',
|
||||||
|
imageAltText: '',
|
||||||
|
});
|
||||||
|
setIsExistingData(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFormData({ ...formData, [name]: value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.pageName) {
|
||||||
|
message.error("Please select a page");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postData = {
|
||||||
|
PageId: formData.pageName,
|
||||||
|
MetaTitle: formData.metaTitle,
|
||||||
|
MetaDesc: formData.metaDescription,
|
||||||
|
Keywords: formData.keywords,
|
||||||
|
ImgAltText: formData.imageAltText,
|
||||||
|
ImageUrl: "",
|
||||||
|
ActiveStatus: "A",
|
||||||
|
CreatedBy: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isExistingData) {
|
||||||
|
await dispatch(putSeo(postData)).unwrap();
|
||||||
|
message.success("SEO Settings updated successfully!");
|
||||||
|
} else {
|
||||||
|
await dispatch(postSeo(postData)).unwrap();
|
||||||
|
message.success("SEO Settings created successfully!");
|
||||||
|
setIsExistingData(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update static HTML file for social media crawlers
|
||||||
|
await updateStaticHTML({
|
||||||
|
metaTitle: formData.metaTitle,
|
||||||
|
metaDescription: formData.metaDescription,
|
||||||
|
keywords: formData.keywords
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
message.error("Failed to save SEO settings");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='seo-form-master'>
|
||||||
|
<div className='seo-form-header'>
|
||||||
|
<div className='seo-form-header-content'>
|
||||||
|
<h2>SEO Management</h2>
|
||||||
|
<p>Configure SEO settings for pages and content.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='seo-form-layout'>
|
||||||
|
<div className='seo-form-left'>
|
||||||
|
<div className='seo-form-container'>
|
||||||
|
<h3>SEO Configuration</h3>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className='seo-form-field'>
|
||||||
|
<label>Select Page *</label>
|
||||||
|
<select
|
||||||
|
name="pageName"
|
||||||
|
value={formData.pageName}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Choose a page...</option>
|
||||||
|
{allpagesData?.map((item) => (
|
||||||
|
<option key={item.ConfigId} value={item.ConfigId}>
|
||||||
|
{item.ConfigName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='seo-form-field'>
|
||||||
|
<label>Meta Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="metaTitle"
|
||||||
|
value={formData.metaTitle}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Enter meta title..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='seo-form-field'>
|
||||||
|
<label>Meta Description</label>
|
||||||
|
<textarea
|
||||||
|
name="metaDescription"
|
||||||
|
value={formData.metaDescription}
|
||||||
|
onChange={handleChange}
|
||||||
|
rows="4"
|
||||||
|
placeholder="Enter meta description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='seo-form-field'>
|
||||||
|
<label>Keywords</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="keywords"
|
||||||
|
value={formData.keywords}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Enter keywords separated by commas..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='seo-form-field'>
|
||||||
|
<label>Image Alt Text</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="imageAltText"
|
||||||
|
value={formData.imageAltText}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Enter image alt text..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='seo-form-actions'>
|
||||||
|
<button type="submit" className='seo-form-btn-primary'>
|
||||||
|
{isExistingData ? 'Update SEO Settings' : 'Create SEO Settings'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className='seo-form-btn-secondary' onClick={() => setShowForm(false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeoForm;
|
||||||
|
|
@ -0,0 +1,322 @@
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import { Popconfirm, message } from 'antd'
|
||||||
|
import { DefaultModal } from '../../Components/Modal/DefaultModal'
|
||||||
|
import "../Styles/TrendingAppsForm.scss"
|
||||||
|
|
||||||
|
const TrendingAppsForm = () => {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [secondTitle, setSecondTitle] = useState('')
|
||||||
|
const [selectedIndustries, setSelectedIndustries] = useState([])
|
||||||
|
const [trendingApps, setTrendingApps] = useState([])
|
||||||
|
const [editingId, setEditingId] = useState(null)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [titleError, setTitleError] = useState('')
|
||||||
|
const [secondTitleError, setSecondTitleError] = useState('')
|
||||||
|
const [industriesError, setIndustriesError] = useState('')
|
||||||
|
const titleInputRef = useRef(null)
|
||||||
|
|
||||||
|
// UUID generator fallback for browser compatibility
|
||||||
|
const generateUUID = () => {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = Math.random() * 16 | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const industries = [
|
||||||
|
'Bakery', 'Restaurant', 'Boating', 'Healthcare', 'Education',
|
||||||
|
'Retail', 'Technology', 'Finance', 'Real Estate', 'Automotive'
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleIndustryChange = (industry) => {
|
||||||
|
setSelectedIndustries(prev =>
|
||||||
|
prev.includes(industry)
|
||||||
|
? prev.filter(item => item !== industry)
|
||||||
|
: [...prev, industry]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
handleClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
setTitleError('Title is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secondTitle.trim()) {
|
||||||
|
setSecondTitleError('Second title is required')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setSecondTitleError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIndustries.length === 0) {
|
||||||
|
setIndustriesError('Please select at least one industry')
|
||||||
|
hasError = true
|
||||||
|
} else {
|
||||||
|
setIndustriesError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
message.error('Please fix the highlighted fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newApp = {
|
||||||
|
id: editingId || generateUUID(),
|
||||||
|
title: title.trim(),
|
||||||
|
secondTitle: secondTitle.trim(),
|
||||||
|
industries: [...selectedIndustries],
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date().toLocaleDateString('en-US')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingId) {
|
||||||
|
setTrendingApps(prev => prev.map(app => app.id === editingId ? newApp : app))
|
||||||
|
setEditingId(null)
|
||||||
|
message.success('Trending app updated!')
|
||||||
|
} else {
|
||||||
|
setTrendingApps(prev => [...prev, newApp])
|
||||||
|
message.success('Trending app added!')
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setTitle('')
|
||||||
|
setSecondTitle('')
|
||||||
|
setSelectedIndustries([])
|
||||||
|
setEditingId(null)
|
||||||
|
setTitleError('')
|
||||||
|
setSecondTitleError('')
|
||||||
|
setIndustriesError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (app) => {
|
||||||
|
setTitle(app.title)
|
||||||
|
setSecondTitle(app.secondTitle)
|
||||||
|
setSelectedIndustries(app.industries)
|
||||||
|
setEditingId(app.id)
|
||||||
|
openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingId && titleInputRef.current) {
|
||||||
|
titleInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [editingId])
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
setTrendingApps(prev => prev.filter(app => app.id !== id))
|
||||||
|
message.success('Trending app deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleStatus = (id) => {
|
||||||
|
setTrendingApps(prev => prev.map(app =>
|
||||||
|
app.id === id ? { ...app, isActive: !app.isActive } : app
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='trending-apps-form-master'>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className='trending-apps-form-header'>
|
||||||
|
<div className='trending-apps-header-left'>
|
||||||
|
{/* <div className='trending-apps-header-icon'>
|
||||||
|
<span>📱</span>
|
||||||
|
</div> */}
|
||||||
|
<div className='trending-apps-header-content'>
|
||||||
|
<h2>Trending Apps Management</h2>
|
||||||
|
<p>Manage and organize your trending apps content and settings.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={openModal} className='trending-apps-create-btn'>
|
||||||
|
<span>+</span> Create Trending App
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className='trending-apps-tab-navigation'>
|
||||||
|
<div className='trending-apps-tab-buttons'>
|
||||||
|
<button className='trending-apps-tab-btn trending-apps-active'>
|
||||||
|
Trending Apps ({trendingApps.length})
|
||||||
|
</button>
|
||||||
|
<button className='trending-apps-tab-btn'>
|
||||||
|
All Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='trending-apps-tab-controls'>
|
||||||
|
<div className='trending-apps-sort-dropdown'>
|
||||||
|
<select>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
<option value="alphabetical">Alphabetical</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='trending-apps-view-toggle'>
|
||||||
|
<button className='trending-apps-view-btn trending-apps-active'>
|
||||||
|
<span>⊞</span>
|
||||||
|
</button>
|
||||||
|
<button className='trending-apps-view-btn'>
|
||||||
|
<span>☰</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
{trendingApps.length > 0 ? (
|
||||||
|
<div className='trending-apps-display'>
|
||||||
|
<div className='trending-apps-grid'>
|
||||||
|
{trendingApps.map((app) => (
|
||||||
|
<div key={app.id} className={`trending-apps-card ${!app.isActive ? 'trending-apps-inactive' : ''}`}>
|
||||||
|
<div className='trending-apps-card-header'>
|
||||||
|
<h3>{app.title}</h3>
|
||||||
|
<div className='trending-apps-card-actions'>
|
||||||
|
<button className='trending-apps-edit-btn' onClick={() => handleEdit(app)}>Edit</button>
|
||||||
|
<Popconfirm
|
||||||
|
title="Delete Trending App"
|
||||||
|
description="Are you sure you want to delete this trending app?"
|
||||||
|
onConfirm={() => handleDelete(app.id)}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<button className='trending-apps-delete-btn'>Delete</button>
|
||||||
|
</Popconfirm>
|
||||||
|
<button
|
||||||
|
className={`trending-apps-status-btn ${app.isActive ? 'trending-apps-deactivate' : 'trending-apps-activate'}`}
|
||||||
|
onClick={() => handleToggleStatus(app.id)}
|
||||||
|
>
|
||||||
|
{app.isActive ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='trending-apps-card-content'>
|
||||||
|
<p><strong>Second Title:</strong> {app.secondTitle}</p>
|
||||||
|
<div className='trending-apps-industries-display'>
|
||||||
|
<strong>Industries:</strong>
|
||||||
|
<div className='trending-apps-industry-tags'>
|
||||||
|
{app.industries.map((industry) => (
|
||||||
|
<span key={industry} className='trending-apps-industry-tag'>{industry}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='trending-apps-created-date'>Created: {app.createdAt}</p>
|
||||||
|
<p className={`trending-apps-status ${app.isActive ? 'trending-apps-active' : 'trending-apps-inactive'}`}>
|
||||||
|
Status: {app.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='trending-apps-placeholder-content'>
|
||||||
|
<div className='trending-apps-placeholder-icon'>
|
||||||
|
<span>📱</span>
|
||||||
|
</div>
|
||||||
|
<h2>No Trending Apps Found</h2>
|
||||||
|
<p>Get started by creating your first trending app</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<DefaultModal
|
||||||
|
open={isModalOpen}
|
||||||
|
title={editingId ? 'Edit Trending App' : 'Create New Trending App'}
|
||||||
|
handleCancel={closeModal}
|
||||||
|
handleSubmit={handleSave}
|
||||||
|
buttonText={editingId ? 'Update App' : 'Add App'}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<div className='trending-apps-form'>
|
||||||
|
<div className='trending-apps-form-group'>
|
||||||
|
<label htmlFor='title'>Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='title'
|
||||||
|
ref={titleInputRef}
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
if (titleError) setTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter title'
|
||||||
|
className={titleError ? 'error' : ''}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{titleError && <span className='error-message'>{titleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='trending-apps-form-group'>
|
||||||
|
<label htmlFor='secondTitle'>Second Title</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='secondTitle'
|
||||||
|
value={secondTitle}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSecondTitle(e.target.value)
|
||||||
|
if (secondTitleError) setSecondTitleError('')
|
||||||
|
}}
|
||||||
|
placeholder='Enter second title'
|
||||||
|
className={secondTitleError ? 'error' : ''}
|
||||||
|
/>
|
||||||
|
{secondTitleError && <span className='error-message'>{secondTitleError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='trending-apps-form-group'>
|
||||||
|
<label>Select Industries</label>
|
||||||
|
<div className={`trending-apps-industries-grid ${industriesError ? 'error' : ''}`}>
|
||||||
|
{industries.map((industry) => (
|
||||||
|
<label key={industry} className='trending-apps-checkbox-item'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={selectedIndustries.includes(industry)}
|
||||||
|
onChange={() => {
|
||||||
|
handleIndustryChange(industry)
|
||||||
|
if (industriesError) setIndustriesError('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{industry}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{industriesError && <span className='error-message'>{industriesError}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='trending-apps-form-actions'>
|
||||||
|
<button type='button' onClick={handleSave} className='trending-apps-save-btn'>
|
||||||
|
{editingId ? 'Update' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={closeModal} className='trending-apps-cancel-btn'>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type='button' onClick={handleClear} className='trending-apps-clear-btn'>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaultModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrendingAppsForm
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { AdminPanelProvider } from './AdminPanelContext'
|
||||||
|
import AdminPanel from './AdminPanel'
|
||||||
|
|
||||||
|
const AdminPanelWrapper = () => {
|
||||||
|
return (
|
||||||
|
<AdminPanelProvider>
|
||||||
|
<AdminPanel />
|
||||||
|
</AdminPanelProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminPanelWrapper
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import './Blog.scss';
|
||||||
|
import Navbar from '../../PozoApp/Components/Navbar';
|
||||||
|
import IndustriesList from '../../PozoApp/Components/IndustriesList';
|
||||||
|
import CompanyList from '../../PozoApp/Components/companyList';
|
||||||
|
import Lenis from "@studio-freight/lenis";
|
||||||
|
import Loader from '../../PozoApp/Components/Loader/PozoLoader';
|
||||||
|
import { RiInstagramFill, RiTwitterXFill } from 'react-icons/ri';
|
||||||
|
import { FaFacebook, FaYoutube } from 'react-icons/fa';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { getBlog, getPublishedBlogs } from '../../features/Blog/Blog';
|
||||||
|
import { getSeo } from '../../features/SEO/seo';
|
||||||
|
import { getConfigNames } from '../../features/configmasterPage/configmasterPage';
|
||||||
|
import SEO from '../../Components/SEO/SEO';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const Blogs = () => {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const [industries, setAppCategory] = useState([]);
|
||||||
|
const [closingIndustry, setClosingIndustry] = useState(false);
|
||||||
|
const [industry, setIndustry] = useState(false);
|
||||||
|
const [company, setCompany] = useState(false);
|
||||||
|
const companyRef = useRef(null);
|
||||||
|
const industryRef = useRef(null);
|
||||||
|
const [closingCompany, setClosingCompany] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [blogPosts, setBlogPosts] = useState([]);
|
||||||
|
const [seoData, setSeoData] = useState({});
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
console.log(blogPosts, "blogPosts")
|
||||||
|
const subDirectory = import.meta.env.ENV_BASE_URL;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getAppData = () => {
|
||||||
|
// Simulate data fetching
|
||||||
|
console.log('Fetching app data...');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 500);
|
||||||
|
getAppData();
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlogPosts();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSEOData = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await dispatch(getConfigNames({ TypeName: "SEO" })).unwrap();
|
||||||
|
if (res?.data?.statusCode === 1) {
|
||||||
|
const seoId = res?.data?.data?.find(e => e.ConfigName === "blog")?.ConfigId;
|
||||||
|
if (!seoId) {
|
||||||
|
console.warn("Blog ConfigId not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await dispatch(getSeo({ PageId: seoId })).unwrap();
|
||||||
|
if (response?.data?.statusCode === 1 && response?.data?.data?.length > 0) {
|
||||||
|
const data = response.data.data[0];
|
||||||
|
setSeoData({
|
||||||
|
metaTitle: data?.MetaTitle,
|
||||||
|
metaDescription: data?.MetaDesc,
|
||||||
|
keywords: data?.Keywords,
|
||||||
|
imageAltText: data?.ImgAltText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No SEO data found for blog page');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSEOData();
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const fetchBlogPosts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await dispatch(getPublishedBlogs()).unwrap();
|
||||||
|
if (response?.data.statusCode == 1) {
|
||||||
|
setBlogPosts(response?.data?.data?.map((blog) => ({
|
||||||
|
id: blog?.BlogId,
|
||||||
|
title: blog?.BlogTitle,
|
||||||
|
excerpt: blog?.BlogSubtitle,
|
||||||
|
author: blog?.BlogAuthor,
|
||||||
|
date: (() => {
|
||||||
|
if (!blog?.PublishDate) return '';
|
||||||
|
|
||||||
|
// --- Format date ---
|
||||||
|
const dateStr = blog.PublishDate.split('T')[0]; // "2025-10-24"
|
||||||
|
const [year, month, day] = dateStr.split('-');
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
const monthName = months[parseInt(month, 10) - 1];
|
||||||
|
const formattedDate = `${parseInt(day, 10)} ${monthName} ${year}`; // "24 Oct 2025"
|
||||||
|
|
||||||
|
// --- Format time ---
|
||||||
|
if (!blog?.PublishTime) return formattedDate; // If no time, return date only
|
||||||
|
|
||||||
|
const [hourStr, minuteStr] = blog.PublishTime.split(':'); // "17:25:00" → ["17","25","00"]
|
||||||
|
let hour = parseInt(hourStr, 10);
|
||||||
|
const minute = minuteStr;
|
||||||
|
const ampm = hour >= 12 ? "PM" : "AM";
|
||||||
|
hour = hour % 12 || 12; // Convert 0 → 12
|
||||||
|
const formattedTime = `${hour.toString().padStart(2, "0")}:${minute} ${ampm}`; // "05:25 PM"
|
||||||
|
|
||||||
|
return `${formattedDate} ${formattedTime}`; // "24 Oct 2025 05:25 PM"
|
||||||
|
})(),
|
||||||
|
readTime: '5 min read',
|
||||||
|
CreatedDate: (() => {
|
||||||
|
if (!blog?.CreatedDate) return '';
|
||||||
|
const dateStr = blog.CreatedDate.split('T')[0]; // Get date part
|
||||||
|
const [year, month, day] = dateStr.split('-');
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
const monthName = months[parseInt(month) - 1];
|
||||||
|
return `${parseInt(day)} ${monthName} ${year}`;
|
||||||
|
})(),
|
||||||
|
imageUrl: blog?.HeaderImage,
|
||||||
|
slug: blog?.Slug
|
||||||
|
})) || []);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setBlogPosts([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
message.error('Failed to fetch blog posts. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const blogPosts = [
|
||||||
|
// {
|
||||||
|
// id: 1,
|
||||||
|
// title: "Modern Web Development Trends in 2024",
|
||||||
|
// excerpt: "Explore the latest technologies and frameworks shaping the future of web development with React, Next.js and modern CSS.",
|
||||||
|
// author: "John Doe",
|
||||||
|
// date: "Dec 15, 2024",
|
||||||
|
// readTime: "5 min read",
|
||||||
|
// imageUrl: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 2,
|
||||||
|
// title: "Mastering React Hooks in 2024",
|
||||||
|
// excerpt: "Learn how to effectively use React Hooks to build modern, efficient React applications with best practices.",
|
||||||
|
// author: "Sarah Wilson",
|
||||||
|
// date: "Dec 12, 2024",
|
||||||
|
// readTime: "8 min read",
|
||||||
|
// imageUrl: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 3,
|
||||||
|
// title: "CSS Grid vs Flexbox: When to Use What",
|
||||||
|
// excerpt: "A comprehensive guide to understanding the differences between CSS Grid and Flexbox with practical examples.",
|
||||||
|
// author: "Mike Chen",
|
||||||
|
// date: "Dec 10, 2024",
|
||||||
|
// readTime: "6 min read",
|
||||||
|
// imageUrl: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400",
|
||||||
|
// }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
|
||||||
|
const redirectApp = (AppName, AppId) => {
|
||||||
|
console.log('Redirect to app:', AppName, AppId);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lenis = new Lenis({
|
||||||
|
duration: 1.2,
|
||||||
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
|
smoothWheel: true,
|
||||||
|
smoothTouch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
lenis.on("scroll", (e) => {
|
||||||
|
setIsScrolled(e.scroll > 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
function raf(time) {
|
||||||
|
lenis.raf(time);
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
return () => lenis.destroy();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (industry || company) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [industry, company]);
|
||||||
|
|
||||||
|
const BlogCard = ({ post }) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
navigate(`${subDirectory}blog/${post.slug}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pozo-blog-card" onClick={handleClick}>
|
||||||
|
<div className="pozo-blog-card-image">
|
||||||
|
<img src={post.imageUrl} alt={post.title} />
|
||||||
|
<div className="pozo-blog-card-overlay"></div>
|
||||||
|
</div>
|
||||||
|
<div className="pozo-blog-card-content">
|
||||||
|
<div className="pozo-blog-tags-container">Recent Updates & Blogs
|
||||||
|
Announcements
|
||||||
|
</div>
|
||||||
|
<p className="pozo-blog-card-excerpt">{post.excerpt}</p>
|
||||||
|
<div className="pozo-blog-card-meta">
|
||||||
|
{/* <span className="pozo-blog-date">{post.date}</span> */}
|
||||||
|
<div>
|
||||||
|
<span className="pozo-blog-date">{`Created Date ${post.CreatedDate}`}</span>
|
||||||
|
<div className="pozo-blog-date">{post.Author || "Admin"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pozo-blog-publish-time">
|
||||||
|
<span className="pozo-blog-read-time">{post.readTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Blog page specific JSON-LD
|
||||||
|
const blogJsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "https://www.pozo.app/home/blog",
|
||||||
|
"name": "POZO Blog — Retail ERP, POS & Grocery Billing Guides",
|
||||||
|
"isPartOf": { "@id": "https://www.pozo.app/" },
|
||||||
|
"description": "Practical guides on POS billing, weighing-scale integration, GST e-invoices, multi-store ERP & inventory control for Indian retailers."
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbJsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.pozo.app/home/" },
|
||||||
|
{ "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://www.pozo.app/home/blog" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO
|
||||||
|
title={seoData.metaTitle || "POZO Blog — Retail ERP, POS & Grocery Billing Guides"}
|
||||||
|
description={seoData.metaDescription || "Practical guides on POS billing, weighing-scale integration, GST e-invoices, multi-store ERP & inventory control for Indian retailers."}
|
||||||
|
keywords={seoData.keywords || "POS billing, weighing scale integration, GST e-invoices, retail ERP, inventory management, grocery billing guides"}
|
||||||
|
url={`${subDirectory}blog`}
|
||||||
|
image={seoData.imageAltText || "/og/blog-og.jpg"}
|
||||||
|
type="website"
|
||||||
|
customJsonLd={[blogJsonLd, breadcrumbJsonLd]}
|
||||||
|
/>
|
||||||
|
<div className='pozo-blog-master-card'>
|
||||||
|
<Navbar
|
||||||
|
isScrolled={isScrolled}
|
||||||
|
forceTopZero={true}
|
||||||
|
AppCategory={industries}
|
||||||
|
closingIndustry={closingIndustry}
|
||||||
|
setClosingIndustry={setClosingIndustry}
|
||||||
|
industry={industry}
|
||||||
|
setIndustry={setIndustry}
|
||||||
|
companyRef={companyRef}
|
||||||
|
closingCompany={closingCompany}
|
||||||
|
setClosingCompany={setClosingCompany}
|
||||||
|
company={company}
|
||||||
|
setCompany={setCompany}
|
||||||
|
industryRef={industryRef}
|
||||||
|
|
||||||
|
/>
|
||||||
|
<div className="pozo-blogs-page">
|
||||||
|
<h1 className="pozo-blog-page-title">Recent Updates & Blogs</h1>
|
||||||
|
<p className="pozo-blog-page-subtitle">Stay updated with the latest news, announcements, and blog posts from PozoApp. Discover insights on business management, technology trends, and more.</p>
|
||||||
|
<div className="pozo-blogs-grid">
|
||||||
|
{blogPosts.length > 0 ? (
|
||||||
|
blogPosts.map((post) => (
|
||||||
|
<BlogCard key={post.id} post={post} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="pozo-blog-empty-state">
|
||||||
|
<p>No blog posts available at the moment. Please check back later for updates.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navbar IndustriesList */}
|
||||||
|
{industry && (
|
||||||
|
<IndustriesList
|
||||||
|
closingIndustry={closingIndustry}
|
||||||
|
AppCategory={industries}
|
||||||
|
redirectApp={redirectApp}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{company && (
|
||||||
|
<CompanyList
|
||||||
|
companyRef={companyRef}
|
||||||
|
closingCompany={closingCompany}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="liveFooterMain" style={{ marginTop: "5rem" }}>
|
||||||
|
<div className="liveFooterLeft">
|
||||||
|
<p className="listTitle">Quick Links</p>
|
||||||
|
<div className="LiveFooterList">
|
||||||
|
<p>About Pozo</p>
|
||||||
|
<p>All Products </p>
|
||||||
|
<p>Pricing Plans </p>
|
||||||
|
<p>Contact Us </p>
|
||||||
|
<p>FAQs </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="liveFooterRight">
|
||||||
|
<p className="listTitle">Support & Help</p>
|
||||||
|
<div className="LiveFooterList">
|
||||||
|
<p>info@pozo.app</p>
|
||||||
|
<p>73 24 00 00 11</p>
|
||||||
|
<p>73 24 00 00 12</p>
|
||||||
|
<span>
|
||||||
|
Let's get social
|
||||||
|
<RiInstagramFill />
|
||||||
|
<FaFacebook />
|
||||||
|
<RiTwitterXFill />
|
||||||
|
<FaYoutube />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="liveFooterRights">
|
||||||
|
<div>
|
||||||
|
© 2025 PozoApp <span>Privacy Policy Cookie Policy</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
For queries contact us: Manager, No 51 Step Colony, Dharga,Hosur,
|
||||||
|
Krishnagiri, Tamilnadu-635126, India
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default Blogs;
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
.pozo-blogs-page {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.pozo-blog-page-title {
|
||||||
|
margin-top: 5rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 108px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-page-subtitle {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 108px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blogs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
.pozo-blog-card-image img {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card-image {
|
||||||
|
position: relative;
|
||||||
|
height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(148, 86, 210, 0.1) 100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .pozo-blog-card-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card-content {
|
||||||
|
padding: 24px;
|
||||||
|
|
||||||
|
.pozo-blog-tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: "Poppins";
|
||||||
|
font-weight: 400;
|
||||||
|
color: #525252;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card-excerpt {
|
||||||
|
color: #000000;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.pozo-blog-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #505050;
|
||||||
|
font-family: "Poppins";
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-author-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.pozo-blog-author {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-date {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.2rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-read-time {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #666;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-publish-time {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.pozo-blog-read-time {
|
||||||
|
background: #4a90e2;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pozo-blogs-page {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.pozo-blog-page-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 0 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-page-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 18px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blogs-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-card .pozo-blog-card-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-master-card {
|
||||||
|
width: 100%;
|
||||||
|
.Navbar-Master,
|
||||||
|
.appNameLeft {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.navbarOption {
|
||||||
|
> p:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-blog-empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 2px dashed #e0e0e0;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pozo-no-blogs-message {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 2px dashed #e0e0e0;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
.pozo-no-blogs-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #c40000;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,499 @@
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import Lenis from "@studio-freight/lenis";
|
||||||
|
import {
|
||||||
|
RiArrowLeftLine, RiCalendarLine, RiTimeLine, RiUserLine, RiHomeLine, RiArticleLine,
|
||||||
|
RiShareLine,
|
||||||
|
RiCloseLine,
|
||||||
|
} from 'react-icons/ri';
|
||||||
|
import { RxHamburgerMenu } from 'react-icons/rx';
|
||||||
|
import { FaFacebook, FaInstagram, FaWhatsapp, FaCopy, FaLinkedin, FaTelegram } from "react-icons/fa";
|
||||||
|
import { FaXTwitter } from "react-icons/fa6";
|
||||||
|
import { getPublishedBlogs, getPublishedBlogsBySlug } from '../../features/Blog/Blog.js';
|
||||||
|
import Loader from '../../PozoApp/Components/Loader/PozoLoader';
|
||||||
|
import PozoAppLogo from '../../Images/PozoAppLogo.svg';
|
||||||
|
import "./BlogDetail.scss"
|
||||||
|
import BlogExtras from "./BlogExtras.jsx";
|
||||||
|
import SEO from '../../Components/SEO/SEO.jsx';
|
||||||
|
|
||||||
|
const mainDirectory = import.meta.env.ENV_MAIN_BASE_URL
|
||||||
|
|
||||||
|
const BlogDetail = () => {
|
||||||
|
const { slug } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [blog, setBlog] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
|
const shareRef = useRef(null);
|
||||||
|
console.log(blog, "blogblogblogblogblog")
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (shareRef.current && !shareRef.current.contains(event.target)) {
|
||||||
|
setShareOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEscKey = (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setShareOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shareOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleEscKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleEscKey);
|
||||||
|
};
|
||||||
|
}, [shareOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlogDetail();
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lenis = new Lenis({
|
||||||
|
duration: 1,
|
||||||
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
|
smoothWheel: true,
|
||||||
|
smoothTouch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function raf(time) {
|
||||||
|
lenis.raf(time);
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(raf);
|
||||||
|
return () => lenis.destroy();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchBlogDetail = async () => {
|
||||||
|
try {
|
||||||
|
const response = await dispatch(getPublishedBlogsBySlug({ slug })).unwrap();
|
||||||
|
|
||||||
|
if (response?.data?.statusCode === 1) {
|
||||||
|
const blogs = response.data.data?.[0];
|
||||||
|
setBlog(blogs);
|
||||||
|
} else {
|
||||||
|
message.error('Failed to load blog posts');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching blog:', error);
|
||||||
|
message.error('Failed to load blog post');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/blog');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blog) {
|
||||||
|
return (
|
||||||
|
<div className='BlogDetailMaster'>
|
||||||
|
<div className='blog-detail-error'>Blog post not found</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SEO
|
||||||
|
title={blog.BlogTitle}
|
||||||
|
description={blog.BlogSubtitle || "Read the latest blog on PozoApp"}
|
||||||
|
image={blog.HeaderImage}
|
||||||
|
url={`${mainDirectory}/home/blog/${slug}`}
|
||||||
|
type="article"
|
||||||
|
publishedTime={blog.CreatedDate}
|
||||||
|
modifiedTime={blog.UpdatedDate || blog.CreatedDate}
|
||||||
|
keywords={blog.Keywords?.join(', ') || "PozoApp, Blog, Technology, Articles"}
|
||||||
|
/>
|
||||||
|
{drawerOpen && <div className='drawer-overlay' onClick={() => setDrawerOpen(false)}></div>}
|
||||||
|
<div className={`drawer ${drawerOpen ? 'open' : ''}`}>
|
||||||
|
<div className='drawer-content'>
|
||||||
|
<Link to='/' onClick={() => setDrawerOpen(false)}>
|
||||||
|
<RiHomeLine /> Home
|
||||||
|
</Link>
|
||||||
|
<Link to='/blog' onClick={() => setDrawerOpen(false)}>
|
||||||
|
<RiArticleLine /> Blog
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='BlogDetailMaster'>
|
||||||
|
<header className='blog-detail-header'>
|
||||||
|
<nav className='navbar'>
|
||||||
|
<div className='navbar-left'>
|
||||||
|
<div className='hamburger-menu' onClick={() => setDrawerOpen(!drawerOpen)}>
|
||||||
|
<RxHamburgerMenu size={20} />
|
||||||
|
</div>
|
||||||
|
<div className='Blognavbar-logo'>
|
||||||
|
<Link to='/'>
|
||||||
|
<img src={PozoAppLogo} alt='Logo' className='logo-img' />
|
||||||
|
</Link>
|
||||||
|
<div className='blogdetailsNavbarText'>
|
||||||
|
| Blog
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div className="blog-detail-contentMain">
|
||||||
|
<article className='blog-detail-content'>
|
||||||
|
<header className='blog-header'>
|
||||||
|
<h1 className='blog-title'>{blog.BlogTitle}</h1>
|
||||||
|
<p className='blog-subtitle'>{blog.BlogSubtitle}</p>
|
||||||
|
|
||||||
|
<div className='blog-meta'>
|
||||||
|
<div className='meta-item'>
|
||||||
|
<RiUserLine />{blog.Author || "Admin"}
|
||||||
|
</div>
|
||||||
|
{blog.CreatedDate && (
|
||||||
|
<div className='meta-item'>
|
||||||
|
<RiCalendarLine /> {(() => {
|
||||||
|
if (!blog?.CreatedDate) return '';
|
||||||
|
const dateStr = blog.CreatedDate.split('T')[0]; // Get date part
|
||||||
|
const [year, month, day] = dateStr.split('-');
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
const monthName = months[parseInt(month) - 1];
|
||||||
|
return `${parseInt(day)} ${monthName} ${year}`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <div className={`status ${blog.IsPublished === 'Y' ? 'published' : 'draft'}`}>
|
||||||
|
{blog.IsPublished === 'Y' ? 'Published' : 'Draft'}
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{blog.HeaderImage && (
|
||||||
|
<div >
|
||||||
|
<img className='blog-poster' src={blog.HeaderImage} alt={blog.BlogTitle} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='blog-body'>
|
||||||
|
{blog.BlogContent && blog.BlogContent.map((block, index) => {
|
||||||
|
switch (block.type) {
|
||||||
|
case 'header':
|
||||||
|
case "oldHeader":
|
||||||
|
const HeaderTag = `h${block.data.level || 2}`;
|
||||||
|
return (
|
||||||
|
<HeaderTag
|
||||||
|
key={index}
|
||||||
|
className={`blog-header-block level-${block.data.level}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: block.data.text }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'paragraph':
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={index}
|
||||||
|
className="blog-paragraph"
|
||||||
|
dangerouslySetInnerHTML={{ __html: block.data.text }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
if (block.data.style === 'checklist') {
|
||||||
|
return (
|
||||||
|
<ul key={index} className='blog-list checklist'>
|
||||||
|
{block.data.items.map((item, i) => (
|
||||||
|
<li key={i} className='checklist-item'>
|
||||||
|
<input type="checkbox" checked={item.meta?.checked} readOnly />
|
||||||
|
<span>{item.content}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
} else if (block.data.style === 'ordered') {
|
||||||
|
return (
|
||||||
|
<ol key={index} className='blog-list ordered'>
|
||||||
|
{block.data.items.map((item, i) => (
|
||||||
|
<li key={i}>{item.content}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ul key={index} className='blog-list unordered'>
|
||||||
|
{block.data.items.map((item, i) => (
|
||||||
|
<li key={i}>{item.content}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "image":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-image">
|
||||||
|
<img
|
||||||
|
src={block.data.file?.url}
|
||||||
|
alt={block.data.caption || "Blog image"}
|
||||||
|
/>
|
||||||
|
{block.data.caption && (
|
||||||
|
<p className="caption">{block.data.caption}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "separator":
|
||||||
|
return (
|
||||||
|
<hr
|
||||||
|
key={index}
|
||||||
|
className={`blog-separator ${block.data.style || "line"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-video">
|
||||||
|
{block.data.file?.url && (
|
||||||
|
<video
|
||||||
|
src={block.data.file.url}
|
||||||
|
controls
|
||||||
|
className="video-player"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{block.data.caption && (
|
||||||
|
<p className="caption">{block.data.caption}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "link":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-link">
|
||||||
|
<a href={block.data.url} target={block.data.target || "_self"}>
|
||||||
|
{block.data.text}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "quote":
|
||||||
|
return (
|
||||||
|
<blockquote key={index} className="blog-quote">
|
||||||
|
<p>{block.data.text}</p>
|
||||||
|
{block.data.caption && (
|
||||||
|
<cite>{block.data.caption}</cite>
|
||||||
|
)}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "code":
|
||||||
|
return (
|
||||||
|
<pre key={index} className="blog-code">
|
||||||
|
<code>{block.data.code}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "cta":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-cta">
|
||||||
|
<h3>{block.data.heading}</h3>
|
||||||
|
<p>{block.data.subheading}</p>
|
||||||
|
<a href={block.data.buttonUrl} className={`cta-button ${block.data.style}`}>
|
||||||
|
{block.data.buttonText}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "email":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-email">
|
||||||
|
<h4>Subject: {block.data.subject}</h4>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: block.data.bodyHtml }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "bookmark":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-bookmark">
|
||||||
|
<a href={block.data.url} target="_blank" rel="noopener noreferrer" style={{ display: 'flex', alignItems: 'center', gap: '16px', textDecoration: 'none', border: '1px solid #e5e7eb', borderRadius: '8px', padding: '16px' }}>
|
||||||
|
<div className="bookmark-content" style={{ flex: 1 }}>
|
||||||
|
<h4 style={{ margin: '0 0 8px 0', color: '#1f2937' }}>{block.data.title}</h4>
|
||||||
|
<p style={{ margin: '0 0 8px 0', color: '#6b7280', fontSize: '14px' }}>{block.data.description}</p>
|
||||||
|
<span className="bookmark-url" style={{ color: '#3b82f6', fontSize: '12px' }}>{block.data.url}</span>
|
||||||
|
</div>
|
||||||
|
{block.data.image && (
|
||||||
|
<img src={block.data.image} alt={block.data.title} style={{ width: '120px', height: '90px', objectFit: 'cover', borderRadius: '6px', flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "callout":
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-callout">
|
||||||
|
<span className="callout-emoji">{block.data.emoji || "💡"}</span>
|
||||||
|
<p>{block.data.text}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "index":
|
||||||
|
const IndexListTag = block.data.style === "numbered" ? "ol" : "ul";
|
||||||
|
return (
|
||||||
|
<div key={index} className="blog-index">
|
||||||
|
<h4>Table of Contents</h4>
|
||||||
|
<IndexListTag>
|
||||||
|
{(block.data.items || []).map((item, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<a href={`#${item.anchor}`}>{item.title}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</IndexListTag>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
<div className="shareBlog">
|
||||||
|
<button
|
||||||
|
className="share-btn"
|
||||||
|
onClick={() => setShareOpen(!shareOpen)}
|
||||||
|
>
|
||||||
|
<RiShareLine /> Share
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{shareOpen && (
|
||||||
|
<div className="share-options" ref={shareRef}>
|
||||||
|
<div className="share-header">
|
||||||
|
<span>Share to</span>
|
||||||
|
<button onClick={() => setShareOpen(false)}>
|
||||||
|
<RiCloseLine size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="share-buttons">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
||||||
|
window.location.href
|
||||||
|
)}`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
className="share-facebook"
|
||||||
|
>
|
||||||
|
<FaFacebook />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = `https://wa.me/?text=${encodeURIComponent(
|
||||||
|
blog.BlogTitle + " " + window.location.href
|
||||||
|
)}`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
className="share-whatsapp"
|
||||||
|
>
|
||||||
|
<FaWhatsapp />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||||
|
blog.BlogTitle
|
||||||
|
)}&url=${encodeURIComponent(window.location.href)}`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
className="share-twitter"
|
||||||
|
>
|
||||||
|
<FaXTwitter />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(
|
||||||
|
window.location.href
|
||||||
|
)}`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
className="share-linkedin"
|
||||||
|
>
|
||||||
|
<FaLinkedin />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const url = `https://t.me/share/url?url=${encodeURIComponent(
|
||||||
|
window.location.href
|
||||||
|
)}&text=${encodeURIComponent(blog.BlogTitle)}`;
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}}
|
||||||
|
className="share-telegram"
|
||||||
|
>
|
||||||
|
<FaTelegram />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
window.open("https://www.instagram.com/", "_blank");
|
||||||
|
navigator.clipboard.writeText(window.location.href);
|
||||||
|
message.success(
|
||||||
|
"Instagram opened! Link copied to paste in your story or bio."
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="share-instagram"
|
||||||
|
>
|
||||||
|
<FaInstagram />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
window.location.href
|
||||||
|
);
|
||||||
|
message.success("Link copied to clipboard!");
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = window.location.href;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
message.success("Link copied to clipboard!");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="share-copy"
|
||||||
|
>
|
||||||
|
<FaCopy /> Copy Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<BlogExtras allowComments={blog?.AllowComments} blogId={blog?.BlogId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="liveFooterRights">
|
||||||
|
<div>
|
||||||
|
© 2025 PozoApp <span><Link>Privacy Policy </Link> Cookie Policy</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
For queries contact us: Manager, No 51 Step Colony, Dharga,Hosur,
|
||||||
|
Krishnagiri, Tamilnadu-635126, India
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlogDetail;
|
||||||
|
|
@ -0,0 +1,572 @@
|
||||||
|
.drawer-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: -300px;
|
||||||
|
width: 250px;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 5rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #374151;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.BlogDetailMaster {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
// background: linear-gradient(180deg, #fafafa 0%, #f5f7fa 100%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 40px 16px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.blog-detail-header {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 950px;
|
||||||
|
margin: 0 auto 25px auto;
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background: white;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.navbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: -10px;
|
||||||
|
|
||||||
|
.hamburger-menu {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Blognavbar-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.logo-img {
|
||||||
|
height: auto;
|
||||||
|
width: 6rem;
|
||||||
|
filter: invert(1) brightness(0);
|
||||||
|
/* Makes the logo black */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-detail-content {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0 26px;
|
||||||
|
margin: 2rem 0;
|
||||||
|
animation: fadeIn 0.6s ease-out;
|
||||||
|
width: 65%;
|
||||||
|
|
||||||
|
.blog-header {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
.blog-title {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 32px;
|
||||||
|
font-family: "Poppins";
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
|
||||||
|
&.published {
|
||||||
|
background: #daffe7;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.draft {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-featured-image {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 90%;
|
||||||
|
height: 400px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-poster {
|
||||||
|
width: 95%;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-body {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
.blog-section {
|
||||||
|
margin: 50px 0;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
border-left: 5px solid #2563eb;
|
||||||
|
padding-left: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-images {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 34px 0;
|
||||||
|
|
||||||
|
.section-image {
|
||||||
|
width: 80%;
|
||||||
|
height: 450px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-videos {
|
||||||
|
display: flex;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.section-video {
|
||||||
|
width: 80%;
|
||||||
|
height: 450px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.blog-detail-content {
|
||||||
|
padding: 28px 12px 0px 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.blog-header {
|
||||||
|
.blog-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-body {
|
||||||
|
margin: 0;
|
||||||
|
.blog-section {
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-body {
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #333;
|
||||||
|
padding: 1rem 0;
|
||||||
|
font-family: "Poppins";
|
||||||
|
|
||||||
|
.blog-header-block {
|
||||||
|
margin: 2.2rem 0 16px 0rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 32px;
|
||||||
|
font-family: "Poppins";
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-paragraph {
|
||||||
|
margin: 1.5rem 0 1rem 0;
|
||||||
|
text-align: justify;
|
||||||
|
font-family: "Poppins";
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #2783da;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-list {
|
||||||
|
margin: 2rem 0 2rem 1rem;
|
||||||
|
|
||||||
|
&.ordered {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unordered {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: "Poppins";
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-image {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 3rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #777;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-video {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 3rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #777;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-detail-contentMain {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-separator {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareBlog {
|
||||||
|
position: relative;
|
||||||
|
margin: 2rem 0;
|
||||||
|
|
||||||
|
.share-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #1d4ed8;
|
||||||
|
outline: none;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: "Poppins";
|
||||||
|
margin: 5rem 0 1rem 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-options {
|
||||||
|
position: absolute;
|
||||||
|
top: -120px;
|
||||||
|
left: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 16px;
|
||||||
|
min-width: 250px;
|
||||||
|
z-index: 100;
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
|
||||||
|
.share-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
font-family: "Poppins";
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 2px 2px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: "Poppins";
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background-color: unset;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-facebook {
|
||||||
|
color: #1877f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-whatsapp {
|
||||||
|
color: #25d366;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-twitter {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-linkedin {
|
||||||
|
color: #0077b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-telegram {
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-instagram {
|
||||||
|
color: #f03333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.share-copy {
|
||||||
|
background: #f2f9ff;
|
||||||
|
color: #374151;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogdetailsNavbarText {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #000000;
|
||||||
|
font-family: "Poppins";
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { FaFacebookF, FaInstagram, FaWhatsapp } from "react-icons/fa";
|
||||||
|
import { message } from "antd";
|
||||||
|
import "./BlogExtras.scss";
|
||||||
|
import { getPublishedBlogs, postComments } from "../../features/Blog/Blog";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { getSession } from "../../Services/others";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const subDirectory = import.meta.env.ENV_BASE_URL;
|
||||||
|
|
||||||
|
const BlogExtras = ({ allowComments, blogId }) => {
|
||||||
|
const [blogData, setBlogData] = useState(null);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const UserId = getSession("UserId");
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
comment: ""
|
||||||
|
});
|
||||||
|
console.log(blogData, "blogData")
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBlogData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await dispatch(getPublishedBlogs()).unwrap();
|
||||||
|
if (response.data.statusCode === 1) {
|
||||||
|
const data = response.data.data;
|
||||||
|
setBlogData(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching blog data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// if (blogId) {
|
||||||
|
fetchBlogData();
|
||||||
|
// }
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
Name: formData?.name,
|
||||||
|
Email: formData?.email,
|
||||||
|
Comment: formData?.comment,
|
||||||
|
CreatedBy: UserId || 0
|
||||||
|
}
|
||||||
|
const response = await dispatch(postComments(data))?.unwrap();
|
||||||
|
|
||||||
|
if (response?.data?.statusCode === 1) {
|
||||||
|
message.success('Comment submitted successfully!');
|
||||||
|
setFormData({ name: "", email: "", comment: "" });
|
||||||
|
} else {
|
||||||
|
message.error('Failed to submit comment. Please try again.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting comment:', error);
|
||||||
|
message.error('Failed to submit comment. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="blog-extras">
|
||||||
|
{/* Comments Form */}
|
||||||
|
|
||||||
|
{allowComments === "Y" && (
|
||||||
|
<div className="comments-section">
|
||||||
|
<h3>Leave a Comment</h3>
|
||||||
|
<form onSubmit={handleSubmit} className="comment-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="Your Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Your Email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<textarea
|
||||||
|
name="comment"
|
||||||
|
placeholder="Write your comment..."
|
||||||
|
value={formData.comment}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
rows="4"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="submit-btn">
|
||||||
|
Post Comment
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Featured Posts */}
|
||||||
|
{blogData && blogData?.filter(blog => blog?.Popular === "Y" && blog?.BlogId !== blogId)?.length > 0 && (
|
||||||
|
<div className="featured-posts">
|
||||||
|
<h3>Featured Posts</h3>
|
||||||
|
{blogData.filter(blog => blog.Popular === "Y" && blog?.BlogId !== blogId).slice(0, 3).map((blog) => (
|
||||||
|
<div key={blog.BlogId} className="featured-post-item" onClick={() => navigate(`${subDirectory}blog/${blog.Slug}`)} style={{ cursor: 'pointer' }}>
|
||||||
|
<img src={blog.HeaderImage} alt="Post" />
|
||||||
|
<div className="featured-post-content">
|
||||||
|
<h4>{blog.BlogTitle}</h4>
|
||||||
|
<span>{new Date(blog.CreatedDate).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Social Media Follow */}
|
||||||
|
<div className="social-section">
|
||||||
|
<h3>Follow Us</h3>
|
||||||
|
<div className="social-links">
|
||||||
|
<a href="https://www.facebook.com/pozomind/" className="social-link facebook" target="_blank" rel="noopener noreferrer">
|
||||||
|
<FaFacebookF />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.instagram.com/pozomind/" className="social-link instagram" target="_blank" rel="noopener noreferrer">
|
||||||
|
<FaInstagram />
|
||||||
|
</a>
|
||||||
|
<a href="https://wa.me/917324000012" className="social-link whatsapp" target="_blank" rel="noopener noreferrer">
|
||||||
|
<FaWhatsapp />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlogExtras;
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap");
|
||||||
|
|
||||||
|
.blog-extras {
|
||||||
|
margin: 2rem 0;
|
||||||
|
width: 30%;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
position: sticky;
|
||||||
|
top: 5rem;
|
||||||
|
|
||||||
|
.comments-section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form {
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.385rem 1rem;
|
||||||
|
border: 1px solid #e8ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background: linear-gradient(190deg, #007bff, #007bff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-posts {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-post-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0 -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-post-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-section {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #000000;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.blog-extras {
|
||||||
|
margin: 1rem auto;
|
||||||
|
width: 100%;
|
||||||
|
.comments-section,
|
||||||
|
.social-section {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
.social-link {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,752 @@
|
||||||
|
// AppDemo Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.video-section-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.video-section-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.video-section-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.video-section-header-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-create-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.video-section-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 40px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.video-section-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.video-section-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.video-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.video-section-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.video-section-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.video-section-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.video-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.video-section-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.video-section-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.video-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.video-section-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #f6ad55;
|
||||||
|
|
||||||
|
.video-section-status {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.video-section-edit-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-delete-btn {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-status-btn {
|
||||||
|
&.video-section-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-card-content {
|
||||||
|
.video-section-video-preview {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.video-section-active {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.video-section-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.video-section-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.video-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.video-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-file-input {
|
||||||
|
cursor: pointer;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&::-webkit-file-upload-button {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-video-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.video-section-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.video-section-clear-btn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for video section form
|
||||||
|
.ant-modal {
|
||||||
|
.video-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.video-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popconfirm styling for video section
|
||||||
|
.ant-popover.ant-popconfirm {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-default {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.video-section-form-master {
|
||||||
|
.video-section-display {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.video-section-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section-placeholder-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.video-section-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,723 @@
|
||||||
|
// BannerSectionForm Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.banner-section-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.banner-section-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.banner-section-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.banner-section-header-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-create-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.banner-section-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 40px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.banner-section-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.banner-section-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.banner-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.banner-section-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.banner-section-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.banner-section-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.banner-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.banner-section-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.banner-section-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.banner-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.banner-section-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #f6ad55;
|
||||||
|
|
||||||
|
.banner-section-status {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.banner-section-edit-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-delete-btn {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-status-btn {
|
||||||
|
&.banner-section-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-card-content {
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.banner-section-active {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.banner-section-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.banner-section-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.banner-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.banner-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.banner-section-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.banner-section-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.banner-section-clear-btn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for banner section form
|
||||||
|
.ant-modal {
|
||||||
|
.banner-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.banner-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.banner-section-form-master {
|
||||||
|
.banner-section-display {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.banner-section-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-section-placeholder-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.banner-section-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Error styles for form validation
|
||||||
|
.banner-section-form {
|
||||||
|
input.error,
|
||||||
|
textarea.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Popconfirm styling for banner section
|
||||||
|
.ant-popover.ant-popconfirm {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-default {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom styling for banner section popconfirm specifically
|
||||||
|
.banner-section-card .ant-popover.ant-popconfirm {
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,813 @@
|
||||||
|
// BlogForm Styles - Clean and Modern Design (Flat Structure)
|
||||||
|
|
||||||
|
.blog-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 16px 16px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-header-content h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-header-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-create-btn {
|
||||||
|
background-color: #5255c8;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-create-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(82, 85, 200, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
padding: 2px;
|
||||||
|
flex: 1;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-left {
|
||||||
|
width: 60%;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-right {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-container {
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px 20px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #00000014;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-container:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-container h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1e40af;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header div {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #bd0a0a;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background: #000000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
background: #333333;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-field {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-field label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-field input,
|
||||||
|
.blog-form-field textarea,
|
||||||
|
.blog-form-field select {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-field input:focus,
|
||||||
|
.blog-form-field textarea:focus,
|
||||||
|
.blog-form-field select:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-field input::placeholder,
|
||||||
|
.blog-form-field textarea::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-field textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-inputs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 4rem;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper input,
|
||||||
|
.input-wrapper textarea {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .add-btn {
|
||||||
|
background: #5255c8;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
min-width: 60px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .add-btn:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .remove-btn {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
min-width: 60px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group .remove-btn:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #5255c8 0%, #5255c8 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-btn-primary:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-btn-secondary {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-btn-secondary:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blog Display Section (CTA-style)
|
||||||
|
.blog-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.blog-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blog Card Styles (CTA-style)
|
||||||
|
.blog-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blog-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #ffffff;
|
||||||
|
|
||||||
|
.blog-status {
|
||||||
|
color: #991b1b;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&.blog-edit-btn {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blog-delete-btn {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blog-status-btn {
|
||||||
|
&.blog-activate {
|
||||||
|
color: #009238;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blog-deactivate {
|
||||||
|
color: #cc0000;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card-content {
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
height: max-content;
|
||||||
|
max-height: 80px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blog-status {
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: "Poppins";
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background-color: #166534;
|
||||||
|
width: max-content;
|
||||||
|
padding: 0px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&.blog-active {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blog-inactive {
|
||||||
|
background-color: #c20000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blog Placeholder Content (CTA-style)
|
||||||
|
.blog-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.blog-placeholder-icon {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #363a3f;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blog Filter Styles
|
||||||
|
.blogFilter {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 10px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px 10px 40px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.blog-form-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-display {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.blog-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-placeholder-content {
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.blog-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-form-btn-primary,
|
||||||
|
.blog-form-btn-secondary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,713 @@
|
||||||
|
// CTASectionForm Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.cta-section-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.cta-section-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 20px 20px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.cta-section-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.cta-section-header-icon {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
background: linear-gradient(135deg, #5255c8 0%, #5e62d3 50%, #8d91fa 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-create-btn {
|
||||||
|
background-color: #5255c8;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.cta-section-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.cta-section-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.cta-section-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.cta-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.cta-section-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.cta-section-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.cta-section-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.cta-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.cta-section-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.cta-section-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.cta-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.cta-section-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #ffffff;
|
||||||
|
|
||||||
|
.cta-section-status {
|
||||||
|
color: #991b1b;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background-color: none;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&.cta-section-edit-btn {
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-delete-btn {
|
||||||
|
color: #991b1b !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-status-btn {
|
||||||
|
&.cta-section-activate {
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-deactivate {
|
||||||
|
// background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-card-content {
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.cta-section-active {
|
||||||
|
color: #166534;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.cta-section-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.cta-section-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.cta-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.cta-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.cta-section-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cta-section-clear-btn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for cta section form
|
||||||
|
.ant-modal {
|
||||||
|
.cta-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.cta-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popconfirm styling for cta section
|
||||||
|
.ant-popover.ant-popconfirm {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
border: 1px solid #e5e7eb !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-default {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom styling for cta section popconfirm specifically
|
||||||
|
.cta-section-card .ant-popover.ant-popconfirm {
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cta-section-form-master {
|
||||||
|
.cta-section-display {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.cta-section-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section-placeholder-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.cta-section-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
.create-modal-form {
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: linear-gradient(135deg, #5255c8 0%, #8b5cf6 50%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: radial-gradient(circle at top right, rgba(255, 255, 255, 0.1), transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
opacity: 0.95;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
.label-icon {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border: 1.5px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
background: white;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #5255c8;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1), 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:focus) {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444;
|
||||||
|
background: #fef2f2;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
&.cancel-btn {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.save-btn {
|
||||||
|
background: linear-gradient(135deg, #5255c8, #8b5cf6);
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #5558e3, #7c3aed);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.35);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.create-modal-form {
|
||||||
|
.form-header {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,781 @@
|
||||||
|
// EcosystemForm Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.ecosystem-section-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.ecosystem-section-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.ecosystem-section-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.ecosystem-section-header-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-create-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.ecosystem-section-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 40px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.ecosystem-section-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.ecosystem-section-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.ecosystem-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.ecosystem-section-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.ecosystem-section-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.ecosystem-section-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.ecosystem-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.ecosystem-section-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.ecosystem-section-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.ecosystem-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.ecosystem-section-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #f6ad55;
|
||||||
|
|
||||||
|
.ecosystem-section-status {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.ecosystem-section-edit-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-delete-btn {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-status-btn {
|
||||||
|
&.ecosystem-section-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-card-content {
|
||||||
|
.ecosystem-section-poster-image {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.ecosystem-section-active {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.ecosystem-section-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.ecosystem-section-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.ecosystem-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.ecosystem-section-poster-section-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 24px 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-file-input {
|
||||||
|
cursor: pointer;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&::-webkit-file-upload-button {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-image-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.ecosystem-section-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-section-clear-btn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for ecosystem section form
|
||||||
|
.ant-modal {
|
||||||
|
.ecosystem-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.ecosystem-section-poster-section-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 24px 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popconfirm styling for ecosystem section
|
||||||
|
.ant-popover.ant-popconfirm {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-default {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ecosystem-section-form-master {
|
||||||
|
.ecosystem-section-display {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.ecosystem-section-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-section-placeholder-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.ecosystem-section-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,455 @@
|
||||||
|
// FAQ Form Master Container
|
||||||
|
.faq-form-master {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.faq-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.faq-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.faq-header-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-header-content {
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-create-btn {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.faq-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.faq-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.faq-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.faq-active {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.faq-active) {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.faq-sort-dropdown select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.faq-view-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.faq-active {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.faq-active) {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content Display
|
||||||
|
.faq-display {
|
||||||
|
.faq-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ Cards
|
||||||
|
.faq-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-inactive {
|
||||||
|
opacity: 0.6;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-card-header {
|
||||||
|
padding: 20px 20px 0 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.4;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.faq-edit-btn {
|
||||||
|
background: #f0f9ff;
|
||||||
|
color: #0369a1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e0f2fe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-delete-btn {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-status-btn {
|
||||||
|
&.faq-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-card-content {
|
||||||
|
padding: 16px 20px 20px 20px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-created-date {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-status {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.faq-active {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-inactive {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.faq-placeholder-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.faq-placeholder-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.faq-form {
|
||||||
|
.faq-form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #dc2626;
|
||||||
|
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.faq-save-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #5a67d8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faq-clear-btn {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.faq-form-master {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-form-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.faq-header-left {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-tab-navigation {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.faq-tab-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.faq-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
.FooterFormMaster {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "NeueMontreal", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.footer-header {
|
||||||
|
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 0;
|
||||||
|
box-shadow: 0 4px 20px rgba(45, 55, 72, 0.3);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-form {
|
||||||
|
padding: 2.5rem;
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d3748;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a5568;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(74, 85, 104, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.save-btn {
|
||||||
|
background: linear-gradient(135deg, #2d3748, #1a202c);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(45, 55, 72, 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(45, 55, 72, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clear-btn {
|
||||||
|
background: #edf2f7;
|
||||||
|
color: #4a5568;
|
||||||
|
border: 2px solid #cbd5e1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-numbers {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
background: linear-gradient(135deg, #f8fafc, #edf2f7);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.numbers-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-card {
|
||||||
|
background: linear-gradient(135deg, #ffffff, #f8fafc);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #48bb78;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
border-color: #f6ad55;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
|
||||||
|
.number-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d3748;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.375rem 0.875rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #c6f6d5;
|
||||||
|
color: #22543d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #742a2a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 2px solid #e2e8f0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
.card-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #718096;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.edit-btn {
|
||||||
|
background: #4299e1;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #3182ce;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.delete-btn {
|
||||||
|
background: #fc8181;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f56565;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggle-btn {
|
||||||
|
background: #f6ad55;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ed8936;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-media-section {
|
||||||
|
padding-top: 2.5rem;
|
||||||
|
border-top: 3px solid #e2e8f0;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,921 @@
|
||||||
|
// HeroSectionForm Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.hero-section-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.hero-section-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.hero-section-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.hero-section-header-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-create-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.hero-section-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 40px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.hero-section-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.hero-section-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.hero-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.hero-section-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.hero-section-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.hero-section-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.hero-section-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.hero-section-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.hero-section-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.hero-section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.hero-section-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #f6ad55;
|
||||||
|
|
||||||
|
.hero-section-status {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.hero-section-edit-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-delete-btn {
|
||||||
|
background: #fee2e2 !important;
|
||||||
|
color: #991b1b !important;
|
||||||
|
border: 1px solid #fca5a5 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(153, 27, 27, 0.2) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-status-btn {
|
||||||
|
&.hero-section-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-card-content {
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.hero-section-active {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-subsections {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-subsection-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-thumb {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-texts {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.hero-section-heading {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-subheading {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.hero-section-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.hero-section-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.hero-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.hero-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-file-input {
|
||||||
|
cursor: pointer;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&::-webkit-file-upload-button {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-image-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 150px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-subsections-container {
|
||||||
|
margin: 32px 0;
|
||||||
|
|
||||||
|
.hero-section-subsections-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-subsection {
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-subsection-number {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
|
||||||
|
.hero-section-remove-subsection-btn {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hero-section-add-subsection-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.hero-section-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hero-section-clear-btn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for hero section form
|
||||||
|
.ant-modal {
|
||||||
|
.hero-section-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.hero-section-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popconfirm styling for hero section
|
||||||
|
.ant-popover.ant-popconfirm {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
border: 1px solid #e5e7eb !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-default {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom styling for hero section popconfirm specifically
|
||||||
|
.hero-section-card .ant-popover.ant-popconfirm {
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-section-form-master {
|
||||||
|
.hero-section-display {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.hero-section-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section-placeholder-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.hero-section-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,612 @@
|
||||||
|
// OfferingsForm Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.OfferingsFormMaster {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.ecosystem-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.ecosystem-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.ecosystem-header-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-create-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.ecosystem-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 40px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.ecosystem-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.ecosystem-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.ecosystem-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.ecosystem-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.ecosystem-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.ecosystem-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.ecosystem-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.ecosystem-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.ecosystem-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.ecosystem-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.ecosystem-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #f6ad55;
|
||||||
|
|
||||||
|
.ecosystem-status {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.ecosystem-edit-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-delete-btn {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-status-btn {
|
||||||
|
&.ecosystem-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-card-content {
|
||||||
|
.ecosystem-poster-image {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.ecosystem-active {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ecosystem-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.ecosystem-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.ecosystem-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.ecosystem-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.poster-section-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 24px 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for ecosystem form
|
||||||
|
.ant-modal {
|
||||||
|
.ecosystem-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.poster-section-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 24px 0 16px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.OfferingsFormMaster {
|
||||||
|
.ecosystem-form-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
|
||||||
|
.ecosystem-header-left {
|
||||||
|
.ecosystem-header-content {
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-tab-navigation {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
.ecosystem-tab-controls {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-display {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.ecosystem-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ecosystem-placeholder-content {
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.ecosystem-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
.seo-form-master {
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
overflow: auto;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.seo-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #0000001a;
|
||||||
|
width: 100%;
|
||||||
|
.seo-form-header-content {
|
||||||
|
h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.seo-form-left {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
.seo-form-container {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: capitalize !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.seo-form-btn-primary,
|
||||||
|
.seo-form-btn-secondary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "NeueMontreal";
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-right {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.seo-placeholder-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
.seo-placeholder-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.seo-form-master {
|
||||||
|
.seo-form-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-layout {
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.seo-form-left {
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-right {
|
||||||
|
.seo-placeholder-content {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seo-form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,777 @@
|
||||||
|
// TrendingAppsForm Styles - Clean and Modern Design
|
||||||
|
|
||||||
|
.trending-apps-form-master {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family:
|
||||||
|
"NeueMontreal",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
// Header Section
|
||||||
|
.trending-apps-form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 40px 40px 32px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.trending-apps-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.trending-apps-header-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 50%, #c084fc 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-create-btn {
|
||||||
|
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 16px 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Navigation
|
||||||
|
.trending-apps-tab-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 40px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.trending-apps-tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.trending-apps-tab-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.trending-apps-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.trending-apps-active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.trending-apps-sort-dropdown {
|
||||||
|
select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.trending-apps-view-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.trending-apps-active {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.trending-apps-active) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apps Display Section
|
||||||
|
.trending-apps-display {
|
||||||
|
padding: 32px;
|
||||||
|
|
||||||
|
.trending-apps-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App Card Styles
|
||||||
|
.trending-apps-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #f6ad55;
|
||||||
|
|
||||||
|
.trending-apps-status {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.trending-apps-edit-btn {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-delete-btn {
|
||||||
|
background: #fee2e2 !important;
|
||||||
|
color: #991b1b !important;
|
||||||
|
border: 1px solid #fca5a5 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fecaca !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(153, 27, 27, 0.2) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-status-btn {
|
||||||
|
&.trending-apps-activate {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #bbf7d0;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-deactivate {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-card-content {
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-created-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.trending-apps-active {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-inactive {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-industries-display {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-industry-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.trending-apps-industry-tag {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Content
|
||||||
|
.trending-apps-placeholder-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 80px 32px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.trending-apps-placeholder-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Form Styles
|
||||||
|
.trending-apps-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.trending-apps-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-industries-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: #ef4444;
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: auto !important;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
|
||||||
|
&.trending-apps-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-cancel-btn {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.trending-apps-clear-btn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fbbf24;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fde68a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global modal overrides for trending apps form
|
||||||
|
.ant-modal {
|
||||||
|
.trending-apps-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.trending-apps-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 12px !important;
|
||||||
|
border: 1px solid #d1d5db !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: "NeueMontreal", sans-serif !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popconfirm styling for trending apps
|
||||||
|
.ant-popover.ant-popconfirm {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
border: 1px solid #e5e7eb !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-popconfirm-message-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popconfirm-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-family: "NeueMontreal", sans-serif;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-default {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
color: #334155;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom styling for trending apps popconfirm specifically
|
||||||
|
.trending-apps-card .ant-popover.ant-popconfirm {
|
||||||
|
.ant-popconfirm-inner-content {
|
||||||
|
.ant-popconfirm-message {
|
||||||
|
.ant-popconfirm-message-text {
|
||||||
|
.ant-popconfirm-title {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.trending-apps-form-master {
|
||||||
|
.trending-apps-display {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-card {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.trending-apps-card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-card-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trending-apps-placeholder-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 60px 16px;
|
||||||
|
|
||||||
|
.trending-apps-placeholder-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Wasted-Vindey";
|
||||||
|
src: url("./fonts/wasted-vindey-cdnfonts/wasted-vindey.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure editor content is actually interactable */
|
||||||
|
#pozo-editor,
|
||||||
|
#pozo-editor * {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
user-select: text !important;
|
||||||
|
}
|
||||||
|
/* app.css */
|
||||||
|
.dark #pozo-editor { background: #0b1220; color: #e5e7eb; }
|
||||||
|
.dark input, .dark textarea { background: #0f172a; color: #e5e7eb; border-color: #1f2937; }
|
||||||
|
|
||||||
|
/* If you have overlays/spinners, ensure they don't block input */
|
||||||
|
.overlay, .backdrop, .spinner {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pozo-editor, #pozo-editor * { pointer-events: auto !important; user-select: text !important; }
|
||||||
|
.overlay, .backdrop, .spinner { pointer-events: none; }
|
||||||
|
.preview-content img { display: block; max-width: 100%; height: auto; }
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
import { Routes, Route, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import AppPage from './Pages/appPage/AppPage.jsx';
|
||||||
|
import CompanyForm from './Pages/company/companyForm.jsx';
|
||||||
|
import CompanyList from "./Pages/company/companyList.jsx";
|
||||||
|
import AdminTax from './Pages/adminTax/adminTaxForm.jsx';
|
||||||
|
import BranchForm from './Pages/branch/branchForm.jsx';
|
||||||
|
import BranchList from './Pages/branch/branchList.jsx';
|
||||||
|
import ApplicationForm from "./Pages/application/applicationForm.jsx";
|
||||||
|
import ApplicationList from "./Pages/application/applicationList.jsx";
|
||||||
|
import CommonMaster from "./Pages/commonMaster/CommonMaster.jsx";
|
||||||
|
import ConfigTypeForm from "./Pages/configType/configtypeForm.jsx";
|
||||||
|
import ApplicationImageForm from "./Pages/applicationImage/applicationImageForm.jsx";
|
||||||
|
import AppPriceFeatureMapping from './Pages/appPriceFeatureMapping/appPriceFeatureMappingList.jsx'
|
||||||
|
import AppPriceFeatureMappingForm from './Pages/appPriceFeatureMapping/appPriceFeatureMappingForm.jsx'
|
||||||
|
import AppMenu from './Pages/appMenu/appMenuList.jsx'
|
||||||
|
import AppMenuForm from './Pages/appMenu/appMenuForm.jsx'
|
||||||
|
import Pricing from "./Pages/pricingType/pricingTypeList.jsx"
|
||||||
|
import PricingForm from "./Pages/pricingType/pricingTypeForm.jsx"
|
||||||
|
import CarouselForm from "./Pages/carousel/carouselForm.jsx";
|
||||||
|
import FeatureList from "./Pages/feature/featureList.jsx";
|
||||||
|
import FeatureForm from "./Pages/feature/featureForm.jsx"
|
||||||
|
import UserForm from "./Pages/user/userForm.jsx";
|
||||||
|
import UserList from "./Pages/user/userList.jsx";
|
||||||
|
import LandignPageLayout from "./Pages/LandingPage/landingPageLayout.jsx";
|
||||||
|
import Home from "./Pages/homeApp/home.jsx";
|
||||||
|
import PublicSignup from "../src/Pages/PublicSignup/Signup.jsx"
|
||||||
|
import PublicHome from "../src/Pages/publichome/HomePage.jsx"
|
||||||
|
import Signin from '../src/Pages/publicsignin/Signin.jsx'
|
||||||
|
import ModuleAccess from "../src/Pages/ModuleAccess/ModuleAccess.jsx"
|
||||||
|
import Application from "./Pages/applications/application.jsx";
|
||||||
|
import UserAccount from "./Pages/userAccount/userAccount.jsx";
|
||||||
|
import MessageTemplateForm from "./Pages/MessageTemplate/MessageTemplateForm.jsx";
|
||||||
|
import MessageTemplateList from "./Pages/MessageTemplate/MessageTemplateList.jsx";
|
||||||
|
import PaymentUPIDetailsForm from "./Pages/PaymentUPIDetails/PaymentUPIDetailsForm.jsx";
|
||||||
|
import PaymentUPIDetailsList from "./Pages/PaymentUPIDetails/PaymentUPIDetailsList.jsx";
|
||||||
|
import PdfPageCommon from "./Pages/paymentpdfPage/PaymentPdfCommon.jsx"
|
||||||
|
import PricingDetails from "./Pages/pricingTypeRestaurant/Pricing.jsx";
|
||||||
|
import InvoiceDetail from '../src/Pages/InvoiceDetail/InvoiceDetail';
|
||||||
|
import PaymentPage from '../src/Pages/PaymentMobilePage/PaymentPage';
|
||||||
|
import AppTemplate from "../src/Pages/themeTemplate/AppTemplate.jsx"
|
||||||
|
import Feature1 from './Pages/themeTemplate/TemplateComps/Features/Features1.jsx'
|
||||||
|
import OverView2 from "../src/Pages/themeTemplate/TemplateComps/OverVIews/OverView2.jsx"
|
||||||
|
import Template from "./Pages/themeTemplate/Template.jsx";
|
||||||
|
import AllTemplates from "./Pages/themeTemplate/AllTemplates.jsx";
|
||||||
|
import AllTemplatesEdit from "./Pages/themeTemplate/AllTemplateEdit.jsx";
|
||||||
|
import PdfPage from './Pages/paymentpdfPage/paymentpdfpage.jsx'
|
||||||
|
import PdfPageDownload from './Pages/PdfDoc/PdfDocDownload.jsx'
|
||||||
|
import CurrencyForm from "./Pages/currency/currencyForm.jsx";
|
||||||
|
import ComingSoon from "./Pages/comingSoon.jsx";
|
||||||
|
import PaymentMethod from "./Pages/ccavenueCustom/paymentMethod.jsx"
|
||||||
|
import PaymentDetail from "./Pages/ccavenueCustom/paymentDetails.jsx"
|
||||||
|
import CustomPaymentGateway from '../src/Pages/InvoiceDetail/customPaymentGateway';
|
||||||
|
import PaymentHistory from "./Pages/Payment/PaymentHistory.jsx";
|
||||||
|
import FeatureAddon from "./Pages/FeaturesPricing/FeatureAddon.jsx";
|
||||||
|
import FeatureAddonForm from "./Pages/FeaturesPricing/FeatureAddonForm.jsx";
|
||||||
|
import FeatureInvoice from "./Pages/FeaturesPricing/FeatureInvoice.jsx";
|
||||||
|
import FeaturesPricing from "./Pages/FeaturesPricing/featurespricing.jsx";
|
||||||
|
import FeaturesPricingForm from "./Pages/FeaturesPricing/FeaturePriceForm.jsx";
|
||||||
|
import FailedPaymentHistory from "./Pages/Payment/FailedPaymentHistory.jsx";
|
||||||
|
import { PaymentGatewayConfigForm } from "./Pages/paymentGatewayConfig/PaymentGatewayConfigForm.jsx";
|
||||||
|
import PaymentGatewayConfigList from "./Pages/paymentGatewayConfig/PaymentGatewayConfigList.jsx";
|
||||||
|
import PaymentDeviceConfigList from "./Pages/paymentDeviceConfig/PaymentDeviceConfigList.jsx";
|
||||||
|
import PaymentDeviceConfigForm from "./Pages/paymentDeviceConfig/PaymentDeviceConfigForm.jsx";
|
||||||
|
import DeviceAllocationlist from "./Pages/kisokDevice/DeviceAllocationList.jsx";
|
||||||
|
import DeviceAllocationform from "./Pages/kisokDevice/DeviceAllocationForm.jsx";
|
||||||
|
import DeviceInformation from "./Pages/kisokDevice/DeviceInformation.jsx";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { checkSession, GenerateLogout } from "./features/signInPage/signInPage.js";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { clearSession, getSession } from "./Services/others.js";
|
||||||
|
import LoyaltysettingList from "./Pages/LoyaltySetting/LoyaltysettingList.jsx"
|
||||||
|
import LoyaltysettingsForm from "./Pages/LoyaltySetting/LoyaltysettingsForm.jsx"
|
||||||
|
import ActivationkeyGeneration from "./Pages/ActivationkeyGeneration/ActivationkeyGeneration.jsx";
|
||||||
|
import FaqShow from "./Pages/themeTemplate/TemplateComps/Faq/FaqShow.jsx";
|
||||||
|
import SignIn from "./PozoApp/Components/SignIn.jsx";
|
||||||
|
import PostEditorPage from "../src/Pages/PostEditorPage.jsx";
|
||||||
|
|
||||||
|
const subDirectory = import.meta.env.ENV_BASE_URL
|
||||||
|
const fieldData = [
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2Title',
|
||||||
|
'FieldValue': 'POS App For Restaurant with & without GST fastest e-bill & normal Billing + KOT and Stock Management',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2SubTitle',
|
||||||
|
'FieldValue': 'Restaurant POS Smart Ordering via QR Code and Send Bill Via through SMS or Whatsapp very fast within min.',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2ButtonText',
|
||||||
|
'FieldValue': 'Get Started',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2AppLinkText',
|
||||||
|
'FieldValue': 'Download POZO Apps From',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2PlayStoreLink',
|
||||||
|
'FieldValue': 'http://playstore.com',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2AppStoreLink',
|
||||||
|
'FieldValue': 'http://appstore.com',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2PosLink',
|
||||||
|
'FieldValue': 'http://posapp.com',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2BannerImage',
|
||||||
|
'FieldValue': 'https://saveme.live/paypre-image-api/upload?fileId=649bfe6906bfb6b6b99227ac.png',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'FieldName': 'Overview2BannerVideo',
|
||||||
|
'FieldValue': 'http://saveme.live/paypre-image-api/upload?fileId=64f083a906bfb6b6b992341b.mp4',
|
||||||
|
'FieldAddData': ''
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const App = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sessionCheckFun()
|
||||||
|
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const sessionCheckFun=async()=>{
|
||||||
|
const UserId = getSession('UserId')
|
||||||
|
const SessionId = getSession('SessionId')
|
||||||
|
const res = await dispatch(checkSession({UserId,SessionId})).unwrap();
|
||||||
|
if (res?.data?.statusCode === 1) {
|
||||||
|
if(res?.data?.response==='False'){
|
||||||
|
|
||||||
|
|
||||||
|
clearSession();
|
||||||
|
navigate(`${subDirectory}`);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
// <>
|
||||||
|
// <Routes>
|
||||||
|
// <Route />
|
||||||
|
// </Routes>
|
||||||
|
// </>
|
||||||
|
<Routes>
|
||||||
|
<Route path={`${subDirectory}`} element={<PublicHome />} />
|
||||||
|
<Route path={`${subDirectory}development`} element={<ComingSoon />} />
|
||||||
|
<Route path={`${subDirectory}PostEditorPage`} element={<PostEditorPage />} />
|
||||||
|
<Route path={`${subDirectory}:appName`} element={<Template />} />
|
||||||
|
<Route path='feature1' element={<Feature1 />} />
|
||||||
|
<Route path={`${subDirectory}pdf`} element={<PdfPage />} />
|
||||||
|
<Route path={`${subDirectory}payment-page-download`} element={<PdfPageDownload />} />
|
||||||
|
<Route path={`${subDirectory}public-signup`} element={<PublicSignup />} />
|
||||||
|
<Route path={`${subDirectory}signinOld`} element={<Signin />} />
|
||||||
|
<Route path={`${subDirectory}signin`} element={<SignIn />} />
|
||||||
|
<Route path={`${subDirectory}payment-pdf`} element={<PdfPageCommon />} />
|
||||||
|
<Route path={`${subDirectory}pricing-details`} element={<PricingDetails />} />
|
||||||
|
<Route path={`${subDirectory}invoice-detail`} element={<InvoiceDetail />} />
|
||||||
|
<Route path={`${subDirectory}payment-page`} element={<PaymentPage />} />
|
||||||
|
<Route path={`${subDirectory}overview2`} element={<OverView2 data={fieldData} />} />
|
||||||
|
<Route path={`${subDirectory}FeatureInvoice`} element={<FeatureInvoice />} />
|
||||||
|
{/* <Route path={`${subDirectory}customPayment`} element={<CustomPaymentGateway/>}/> */}
|
||||||
|
<Route path={`${subDirectory}Faq`} element={<FaqShow />} />
|
||||||
|
|
||||||
|
<Route path={`${subDirectory}landing-page`} element={<LandignPageLayout />}>
|
||||||
|
<Route path={'home'} element={<Home />} />
|
||||||
|
<Route path={'apps'} element={<Application />} />
|
||||||
|
<Route path={'user-account'} element={<UserAccount />} />
|
||||||
|
<Route path={'user-account-edit'} element={<UserAccount status='edit'/>} />
|
||||||
|
|
||||||
|
</Route>
|
||||||
|
<Route path={`${subDirectory}setting`} element={<AppPage />}>
|
||||||
|
<Route path='themes' element={<AppTemplate />} />
|
||||||
|
<Route path='payment-gateway-config' element={<PaymentGatewayConfigForm />} />
|
||||||
|
<Route path='payment-gateway-configlist' element={<PaymentGatewayConfigList />} />
|
||||||
|
<Route path='all-templates' element={<AllTemplates />} />
|
||||||
|
<Route path='all-templates/update' element={<AllTemplatesEdit />} />
|
||||||
|
<Route path='payment-device-config'>
|
||||||
|
<Route index element={<PaymentDeviceConfigList/>} />
|
||||||
|
<Route path='new' element={<PaymentDeviceConfigForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<PaymentDeviceConfigForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='company-master'>
|
||||||
|
<Route index element={<CompanyList />} />
|
||||||
|
<Route path='new' element={<CompanyForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<CompanyForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='branch-master'>
|
||||||
|
<Route index element={<BranchList />} />
|
||||||
|
<Route path='new' element={<BranchForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<BranchForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='application-master'>
|
||||||
|
<Route index element={<ApplicationList />} />
|
||||||
|
<Route path='new' element={<ApplicationForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<ApplicationForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='app-menu'>
|
||||||
|
<Route index element={<AppMenu />} />
|
||||||
|
<Route path='new' element={<AppMenuForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<AppMenuForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='feature-mapping'>
|
||||||
|
<Route index element={<AppPriceFeatureMapping />} />
|
||||||
|
<Route path='new' element={<AppPriceFeatureMappingForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<AppPriceFeatureMappingForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='device-allocation'>
|
||||||
|
<Route index element={<DeviceAllocationlist />} />
|
||||||
|
<Route path='new' element={<DeviceAllocationform formType={"add"} />} />
|
||||||
|
<Route path='edit' element={<DeviceAllocationform formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='device-information' element={<DeviceInformation />} />
|
||||||
|
<Route path='feat-addon'>
|
||||||
|
<Route index element={<FeatureAddon />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path='feat-addon-form'>
|
||||||
|
<Route index element={<FeatureAddonForm />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='pricing'>
|
||||||
|
<Route index element={<Pricing />} />
|
||||||
|
<Route path='new' element={<PricingForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<Pricing formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='feature-master'>
|
||||||
|
<Route index element={<FeatureList />} />
|
||||||
|
<Route path='new' element={<FeatureForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<FeatureForm formType={"edit"} />
|
||||||
|
} />
|
||||||
|
|
||||||
|
</Route>
|
||||||
|
<Route path='user-master'>
|
||||||
|
<Route index element={<UserList />} />
|
||||||
|
<Route path='new' element={<UserForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<UserForm formType={"edit"} />} />
|
||||||
|
|
||||||
|
</Route>
|
||||||
|
<Route path='payment'>
|
||||||
|
<Route path='payment-history' element={<PaymentHistory />} />
|
||||||
|
<Route path='failed-payment-history' element={<FailedPaymentHistory />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='message-template'>
|
||||||
|
<Route index element={<MessageTemplateList />} />
|
||||||
|
<Route path='new' element={<MessageTemplateForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<MessageTemplateForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='PaymentUPIDetails'>
|
||||||
|
<Route index element={<PaymentUPIDetailsList />} />
|
||||||
|
<Route path='new' element={<PaymentUPIDetailsForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<PaymentUPIDetailsForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
|
||||||
|
<Route path='featurepricing'>
|
||||||
|
<Route index element={<FeaturesPricing />} />
|
||||||
|
{<Route path='new' element={<FeaturesPricingForm formType={"add"} />} />}
|
||||||
|
<Route path='update' element={<FeaturesPricingForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
<Route path='app-access' element={<ModuleAccess />} />
|
||||||
|
<Route path='config-master' element={<CommonMaster />} />
|
||||||
|
<Route path='config-type' element={<ConfigTypeForm />} />
|
||||||
|
<Route path='admin-tax' element={< AdminTax />} />
|
||||||
|
<Route path='application-image' element={< ApplicationImageForm />} />
|
||||||
|
<Route path='carousel' element={<CarouselForm />} />
|
||||||
|
<Route path='currency' element={<CurrencyForm />} />
|
||||||
|
<Route path='payment-data' element={<CustomPaymentGateway />} />
|
||||||
|
<Route path='payment-method' element={<PaymentMethod />} />
|
||||||
|
<Route path='payment-details' element={<PaymentDetail />} />
|
||||||
|
<Route path='activationkey-generation' element={<ActivationkeyGeneration />} />
|
||||||
|
|
||||||
|
<Route path='Referral-setting'>
|
||||||
|
<Route index element={<LoyaltysettingList />} />
|
||||||
|
<Route path='new' element={<LoyaltysettingsForm formType={"add"} />} />
|
||||||
|
<Route path='update' element={<LoyaltysettingsForm formType={"edit"} />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
</Route>
|
||||||
|
<Route path={`${subDirectory}check`} element={<div>check</div>} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
// AppRoutes.jsx
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import ProtectedRoutes from './ProtectedRoutes';
|
||||||
|
import { isMobile, isIOS } from "react-device-detect";
|
||||||
|
import { routesConfig } from './routesConfig';
|
||||||
|
import { checkSession } from "./features/signInPage/signInPage.js";
|
||||||
|
import { clearSession, getSession } from "./Services/others.js";
|
||||||
|
import devtools from 'devtools-detect';
|
||||||
|
import useVisitLogger from "./useVisitLogger.jsx";
|
||||||
|
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
const subDirectory = import.meta.env.ENV_MAIN_BASE_URL
|
||||||
|
const AppRoutes = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
useVisitLogger()
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [devToolsOpen, setDevToolsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("search raw:", window.location.search);
|
||||||
|
console.log("queryParams size:", [...searchParams.entries()].length);
|
||||||
|
console.log("queryParams entries:", [...searchParams.entries()]);
|
||||||
|
console.log("token:", searchParams.get("token"));
|
||||||
|
console.log("id:", searchParams.get("id"));
|
||||||
|
}, [searchParams]);
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = (e) => {
|
||||||
|
// Skip session clearing for PostEditorPage
|
||||||
|
if (window.location.pathname.includes('PostEditorPage')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const SessionId = getSession('SessionId');
|
||||||
|
if (!SessionId) {
|
||||||
|
clearSession();
|
||||||
|
window.location.replace(`${subDirectory}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.history.go(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('popstate', handlePopState);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
const hasAuthorizedSession = new URLSearchParams(window.location.search).has('SD');
|
||||||
|
console.log("hasAuthorizedSession", hasAuthorizedSession)
|
||||||
|
|
||||||
|
if (!hasAuthorizedSession) {
|
||||||
|
sessionCheckFun();
|
||||||
|
} else {
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.search = '';
|
||||||
|
window.history.replaceState({}, document.title, url.toString());
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
// mohan
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (isMobile || isIOS) {
|
||||||
|
// console.log("📱 Mobile/iOS device → skipping DevTools detection");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const checkDevTools = setInterval(() => {
|
||||||
|
// if (devtools.isOpen && !devToolsOpen) {
|
||||||
|
// setDevToolsOpen(true);
|
||||||
|
// document.body.innerHTML =
|
||||||
|
// "<h1 style='color: red; text-align: center;'>Close Inspect to continue using the application.</h1>";
|
||||||
|
// }
|
||||||
|
// else if (!devtools.isOpen && devToolsOpen) {
|
||||||
|
// setDevToolsOpen(false);
|
||||||
|
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// window.location.reload();
|
||||||
|
// }, 100);
|
||||||
|
|
||||||
|
// clearInterval(checkDevTools);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!devtools.isOpen) {
|
||||||
|
// let before = performance.now();
|
||||||
|
// let after = performance.now();
|
||||||
|
|
||||||
|
// let executionDelay = after - before;
|
||||||
|
|
||||||
|
// if (executionDelay > 100) {
|
||||||
|
// setDevToolsOpen(true);
|
||||||
|
// document.body.innerHTML =
|
||||||
|
// "<h1 style='color: red; text-align: center;'>Close Inspect to continue using the application.</h1>";
|
||||||
|
// } else {
|
||||||
|
// setDevToolsOpen(false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }, 1000);
|
||||||
|
|
||||||
|
// return () => clearInterval(checkDevTools);
|
||||||
|
// }, [devToolsOpen]);
|
||||||
|
|
||||||
|
const sessionCheckFun = async () => {
|
||||||
|
const UserId = getSession('UserId')
|
||||||
|
const SessionId = getSession('SessionId')
|
||||||
|
if (UserId && SessionId && sessionStorage.getItem("auth")) {
|
||||||
|
const res = await dispatch(checkSession({ UserId, SessionId })).unwrap();
|
||||||
|
if (res?.data?.statusCode === 1) {
|
||||||
|
if (res?.data?.response === 'False') {
|
||||||
|
clearSession();
|
||||||
|
window.location.replace(`${subDirectory}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ProtectedRoutes routesConfig={routesConfig} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { BsArrowLeft, BsArrowRight } from 'react-icons/bs';
|
||||||
|
|
||||||
|
const Slider = ({ children, showLeftButton, showRightButton }) => {
|
||||||
|
const scrollContainerRef = useRef(null);
|
||||||
|
const [showLeftButton1, setShowLeftButton] = useState(false);
|
||||||
|
const [showRightButton1, setShowRightButton] = useState(true);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startX, setStartX] = useState(0);
|
||||||
|
const [width, setWidth] = useState(window.innerWidth);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleResize() {
|
||||||
|
setWidth(window.innerWidth);
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setStartX(e.pageX - scrollContainerRef.current.offsetLeft);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setStartX(e.touches[0].clientX);
|
||||||
|
};
|
||||||
|
const handleTouchMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
|
const deltaX = e.touches[0].clientX - startX;
|
||||||
|
scrollContainer.scrollLeft -= deltaX;
|
||||||
|
setStartX(e.touches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - startX;
|
||||||
|
scrollContainer.scrollLeft -= deltaX;
|
||||||
|
setStartX(e.clientX);
|
||||||
|
};
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleScrollLeft = () => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollLeft -= 200;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollRight = () => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollLeft += 200;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
if (container) {
|
||||||
|
setShowLeftButton(container.scrollLeft > 0);
|
||||||
|
setShowRightButton(
|
||||||
|
container.scrollWidth - container.clientWidth - container.scrollLeft > 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
if (container) {
|
||||||
|
const isOverflowing = container.scrollWidth > container.clientWidth;
|
||||||
|
setShowLeftButton(isOverflowing);
|
||||||
|
setShowRightButton(isOverflowing);
|
||||||
|
}
|
||||||
|
}, [children,width]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="AppSliderDiv">
|
||||||
|
<div style={{ width: "2rem" }}>
|
||||||
|
{showLeftButton1 && (
|
||||||
|
<button className="swipee-icon" onClick={handleScrollLeft}>
|
||||||
|
<BsArrowLeft />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflowX: "hidden",
|
||||||
|
overflowY: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: 'start',
|
||||||
|
flexDirection: "row",
|
||||||
|
columnGap: "0.5rem",
|
||||||
|
cursor: "grab",
|
||||||
|
|
||||||
|
}}
|
||||||
|
ref={scrollContainerRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
onMouseLeave={handleMouseUp}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div style={{ width: "2rem" }}>
|
||||||
|
{showRightButton1 && (
|
||||||
|
<button className="swipee-icon" onClick={handleScrollRight}>
|
||||||
|
<BsArrowRight />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
showLeftButton: PropTypes.bool,
|
||||||
|
showRightButton: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.defaultProps = {
|
||||||
|
showLeftButton: true,
|
||||||
|
showRightButton: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default Slider;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
.slider-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-content {
|
||||||
|
display: flex;
|
||||||
|
will-change: transform;
|
||||||
|
animation: scroll-left infinite linear;
|
||||||
|
animation-duration: var(--scroll-speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-content.paused {
|
||||||
|
animation-play-state: paused; /* Pause on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll-left {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0); /* Start position */
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%); /* Move by the full width of the content */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './CommonSlider.scss';
|
||||||
|
|
||||||
|
const Slider = ({ children, scrollSpeed }) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
|
||||||
|
// Dynamically calculate content width
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentRef.current && containerRef.current) {
|
||||||
|
const contentWidth = contentRef.current.scrollWidth;
|
||||||
|
containerRef.current.style.setProperty('--content-width', `${contentWidth}px`);
|
||||||
|
}
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
console.log(children,"childrenjkhsdf")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="slider-container"
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
className={`slider-content ${isHovered ? 'paused' : ''}`}
|
||||||
|
style={{ animationDuration: `${scrollSpeed}s` }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{children}
|
||||||
|
{children} {/* Add more children to create continuous loop */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
scrollSpeed: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
Slider.defaultProps = {
|
||||||
|
scrollSpeed: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Slider;
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import EditorJS from '@editorjs/editorjs';
|
||||||
|
import Header from '@editorjs/header';
|
||||||
|
import List from '@editorjs/list';
|
||||||
|
import Paragraph from '@editorjs/paragraph';
|
||||||
|
import Image from '@editorjs/image';
|
||||||
|
import Video from '@weekwood/editorjs-video';
|
||||||
|
import Quote from '@editorjs/quote';
|
||||||
|
import Code from '@editorjs/code';
|
||||||
|
import Embed from '@editorjs/embed';
|
||||||
|
import LinkTool from '@editorjs/link';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useEditor } from '../context/EditorContext';
|
||||||
|
import {
|
||||||
|
Callout,
|
||||||
|
CTA,
|
||||||
|
EmailContent,
|
||||||
|
Separator,
|
||||||
|
BetterHeader,
|
||||||
|
SimpleLink,
|
||||||
|
Bookmark
|
||||||
|
} from '../editor/tools/index.js';
|
||||||
|
import InlineLink from '../editor/tools/InlineLink.js';
|
||||||
|
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { uploadImage } from '../features/upload/upload.js';
|
||||||
|
|
||||||
|
// --- Custom Video Tool with robust controls enforcement ---
|
||||||
|
class CustomVideo extends Video {
|
||||||
|
render() {
|
||||||
|
const wrapper = super.render();
|
||||||
|
|
||||||
|
// Helper to set controls and other attributes
|
||||||
|
const setVideoAttributes = (video) => {
|
||||||
|
if (!video) return;
|
||||||
|
video.controls = true;
|
||||||
|
video.setAttribute('controls', '');
|
||||||
|
video.playsInline = true;
|
||||||
|
video.preload = 'metadata';
|
||||||
|
video.style.width = '100%';
|
||||||
|
video.style.height = 'auto';
|
||||||
|
video.crossOrigin = 'anonymous';
|
||||||
|
|
||||||
|
// Remove loader when video is ready
|
||||||
|
video.addEventListener('canplay', () => {
|
||||||
|
removeSkeleton();
|
||||||
|
});
|
||||||
|
video.addEventListener('error', () => {
|
||||||
|
removeSkeleton();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...existing code for source/type...
|
||||||
|
const srcEl = video.querySelector('source');
|
||||||
|
const srcUrl = video.currentSrc || video.src || (srcEl && srcEl.src);
|
||||||
|
if (srcEl && !srcEl.type && srcUrl) {
|
||||||
|
const ext = (srcUrl.split('.').pop() || '').split('?')[0].toLowerCase();
|
||||||
|
if (ext === 'mp4') srcEl.type = 'video/mp4';
|
||||||
|
}
|
||||||
|
if (!video.src && srcEl && srcEl.src) {
|
||||||
|
video.src = srcEl.src;
|
||||||
|
}
|
||||||
|
video.load();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Skeleton Loader ---
|
||||||
|
let skeleton = null;
|
||||||
|
const showSkeleton = () => {
|
||||||
|
if (skeleton) return;
|
||||||
|
skeleton = document.createElement('div');
|
||||||
|
skeleton.className = 'custom-video-skeleton';
|
||||||
|
skeleton.style.cssText = `
|
||||||
|
position:absolute;
|
||||||
|
top:0;left:0;width:100%;height:100%;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
background:rgba(240,240,240,0.9);
|
||||||
|
z-index:10;
|
||||||
|
font-size:18px;
|
||||||
|
color:#888;
|
||||||
|
`;
|
||||||
|
skeleton.innerHTML = `<div>
|
||||||
|
<svg width="48" height="48" viewBox="0 0 50 50">
|
||||||
|
<circle cx="25" cy="25" r="20" fill="none" stroke="#bbb" stroke-width="5" stroke-linecap="round" stroke-dasharray="31.4 31.4" transform="rotate(-90 25 25)">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
<div>Loading video...</div>
|
||||||
|
</div>`;
|
||||||
|
// Find the video container
|
||||||
|
const videoContainer = wrapper.querySelector('.video-tool__video > div');
|
||||||
|
if (videoContainer) {
|
||||||
|
videoContainer.style.position = 'relative';
|
||||||
|
videoContainer.appendChild(skeleton);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const removeSkeleton = () => {
|
||||||
|
if (skeleton && skeleton.parentNode) {
|
||||||
|
skeleton.parentNode.removeChild(skeleton);
|
||||||
|
skeleton = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial skeleton logic
|
||||||
|
const video = wrapper.querySelector('video');
|
||||||
|
if (!video || !video.src) {
|
||||||
|
showSkeleton();
|
||||||
|
} else {
|
||||||
|
setVideoAttributes(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe for video element or src changes
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const video = wrapper.querySelector('video');
|
||||||
|
if (video) {
|
||||||
|
if (!video.src) {
|
||||||
|
showSkeleton();
|
||||||
|
} else {
|
||||||
|
removeSkeleton();
|
||||||
|
setVideoAttributes(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(wrapper, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentEditor = () => {
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const { post, setField } = useEditor();
|
||||||
|
const location = useLocation();
|
||||||
|
const { isEdit = false } = location?.state || {};
|
||||||
|
const varOcg = useRef(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const customVideo = {
|
||||||
|
class: CustomVideo,
|
||||||
|
config: {
|
||||||
|
inlineToolbar: true,
|
||||||
|
player: {
|
||||||
|
pip: false,
|
||||||
|
},
|
||||||
|
uploader: {
|
||||||
|
async uploadByFile(file) {
|
||||||
|
try {
|
||||||
|
const res = await dispatch(uploadImage(file)).unwrap();
|
||||||
|
if (res?.data?.image) {
|
||||||
|
if (res.data.image.startsWith('data:')) {
|
||||||
|
alert('Video upload returned a data URL. This will not work for large files. Please check your backend.');
|
||||||
|
throw new Error('Video upload returned a data URL');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: 1,
|
||||||
|
file: { url: res.data.image },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Video upload failed:', error);
|
||||||
|
return { success: 0, message: 'Video upload failed' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploadByUrl(url) {
|
||||||
|
return Promise.resolve({
|
||||||
|
success: 1,
|
||||||
|
file: { url },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let editorInstance;
|
||||||
|
|
||||||
|
const initEditor = async () => {
|
||||||
|
if (editorRef.current) return; // prevent multiple inits
|
||||||
|
|
||||||
|
editorInstance = new EditorJS({
|
||||||
|
holder: 'editorjs',
|
||||||
|
tools: {
|
||||||
|
link: LinkTool,
|
||||||
|
inlineLink: InlineLink,
|
||||||
|
header: BetterHeader,
|
||||||
|
paragraph: {
|
||||||
|
class: Paragraph,
|
||||||
|
config: {
|
||||||
|
// placeholder: ' Start typing...',
|
||||||
|
placeholder: 'Click the + button below to add content blocks (headings, images, paragraphs, etc.)...',
|
||||||
|
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
title: 'Paragraph',
|
||||||
|
icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 103-32z"/></svg>'
|
||||||
|
},
|
||||||
|
inlineToolbar: ['inlineLink', 'bold', 'italic']
|
||||||
|
},
|
||||||
|
list: List,
|
||||||
|
image: {
|
||||||
|
class: Image,
|
||||||
|
config: {
|
||||||
|
uploader: {
|
||||||
|
async uploadByFile(file) {
|
||||||
|
try {
|
||||||
|
// 🔹 Dispatch Redux thunk to upload image
|
||||||
|
const res = await dispatch(uploadImage(file)).unwrap();
|
||||||
|
if (res?.data?.image) {
|
||||||
|
return {
|
||||||
|
success: 1,
|
||||||
|
file: { url: res.data.image },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response from upload API');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Image upload failed:', error);
|
||||||
|
return {
|
||||||
|
success: 0,
|
||||||
|
message: 'Image upload failed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploadByUrl(url) {
|
||||||
|
return Promise.resolve({
|
||||||
|
success: 1,
|
||||||
|
file: { url },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
quote: Quote,
|
||||||
|
code: Code,
|
||||||
|
embed: Embed,
|
||||||
|
callout: Callout,
|
||||||
|
cta: CTA,
|
||||||
|
email: EmailContent,
|
||||||
|
separator: Separator,
|
||||||
|
bookmark: Bookmark,
|
||||||
|
video: customVideo,
|
||||||
|
link: SimpleLink,
|
||||||
|
},
|
||||||
|
placeholder:
|
||||||
|
'✨ Click the + button below to add content blocks (headings, images, paragraphs, etc.)...',
|
||||||
|
data: isEdit && post?.blocks?.length > 0 ? { blocks: post.blocks } : {},
|
||||||
|
onChange: async () => {
|
||||||
|
const content = await editorInstance.save();
|
||||||
|
setField('blocks', content?.blocks);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
editorRef.current = editorInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
initEditor();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.destroy();
|
||||||
|
editorRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isEdit, dispatch]);
|
||||||
|
|
||||||
|
// handle late-arriving post.blocks (e.g., async fetch)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isEdit &&
|
||||||
|
editorRef.current &&
|
||||||
|
post?.blocks?.length > 0 &&
|
||||||
|
!varOcg.current
|
||||||
|
) {
|
||||||
|
editorRef.current.isReady.then(() => {
|
||||||
|
editorRef.current.render({ blocks: post.blocks });
|
||||||
|
varOcg.current = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [post?.blocks, isEdit]);
|
||||||
|
|
||||||
|
return <div id="editorjs" style={{ minHeight: '400px', padding: '20px' }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentEditor;
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useEditor } from '../context/EditorContext';
|
||||||
|
import { countWordsFromBlocks, minutesRead } from '../utils/textUtils';
|
||||||
|
import { FaPen, FaFont, FaClock, FaCube, FaHeading, FaFileAlt, FaImage, FaList, FaChartBar } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const DetailedMetrics = () => {
|
||||||
|
const { post } = useEditor();
|
||||||
|
|
||||||
|
const metrics = useMemo(() => {
|
||||||
|
const blocks = post.blocks || [];
|
||||||
|
const words = countWordsFromBlocks(post.blocks);
|
||||||
|
const chars = blocks.reduce((count, block) => {
|
||||||
|
if (block.data?.text) return count + block.data.text.length;
|
||||||
|
return count;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const headings = blocks.filter(b => b.type === 'header');
|
||||||
|
const paragraphs = blocks.filter(b => b.type === 'paragraph');
|
||||||
|
const images = blocks.filter(b => b.type === 'image');
|
||||||
|
const lists = blocks.filter(b => b.type === 'list');
|
||||||
|
|
||||||
|
return {
|
||||||
|
words,
|
||||||
|
characters: chars,
|
||||||
|
charactersNoSpaces: chars - (post.title || '').split(' ').length + 1,
|
||||||
|
readingTime: minutesRead(words),
|
||||||
|
blocks: blocks.length,
|
||||||
|
headings: headings.length,
|
||||||
|
paragraphs: paragraphs.length,
|
||||||
|
images: images.length,
|
||||||
|
lists: lists.length
|
||||||
|
};
|
||||||
|
}, [post]);
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ label: 'Words', value: metrics.words, icon: <FaPen />, color: '#3b82f6' },
|
||||||
|
{ label: 'Characters', value: metrics.characters, icon: <FaFont />, color: '#8b5cf6' },
|
||||||
|
{ label: 'Reading Time', value: `${metrics.readingTime} min`, icon: <FaClock />, color: '#10b981' },
|
||||||
|
{ label: 'Blocks', value: metrics.blocks, icon: <FaCube />, color: '#f59e0b' },
|
||||||
|
{ label: 'Headings', value: metrics.headings, icon: <FaHeading />, color: '#ef4444' },
|
||||||
|
{ label: 'Paragraphs', value: metrics.paragraphs, icon: <FaFileAlt />, color: '#06b6d4' },
|
||||||
|
{ label: 'Images', value: metrics.images, icon: <FaImage />, color: '#84cc16' },
|
||||||
|
{ label: 'Lists', value: metrics.lists, icon: <FaList />, color: '#f97316' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: 12 }}>
|
||||||
|
<div style={{ fontSize: 14, fontWeight: 400, marginBottom: 12, color: '#007bff', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<FaChartBar style={{ color: '#3b82f6', fontSize: "14px" }} /> Content Metrics
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div key={index} style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '6px 8px', background: '#f8fafc', borderRadius: 4
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 12, color: item.color }}>{item.icon}</span>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 11, color: '#64748b' }}>{item.label}</div>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 600, color: '#1f2937' }}>{item.value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailedMetrics;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Drawer} from 'antd';
|
||||||
|
export const Drawers = ({open,placement,title,children,onClose}) => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={title}
|
||||||
|
placement={placement}
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
key={placement}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useEditor } from '../context/EditorContext';
|
||||||
|
import { uploadImage } from '../features/upload/upload';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { FaCamera, FaSpinner } from 'react-icons/fa';
|
||||||
|
import './FeatureImagePicker.scss';
|
||||||
|
import { IoCameraOutline } from "react-icons/io5";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const FeatureImagePicker = () => {
|
||||||
|
const { post, setField } = useEditor();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleImageChange = async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// dispatch your async thunk
|
||||||
|
const res = await dispatch(uploadImage(file)).unwrap(); // unwrap to get payload
|
||||||
|
if (res?.data?.image) {
|
||||||
|
setField('featureImage', res?.data?.image); // set uploaded URL
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Image upload failed:', err);
|
||||||
|
alert('Image upload failed. Try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-panel feature-image-picker">
|
||||||
|
<h3 className="panel-title">
|
||||||
|
<FaCamera /> Feature Image
|
||||||
|
</h3>
|
||||||
|
<p className="panel-instruction">
|
||||||
|
Upload a featured image for your post. Recommended size: 1200x630px for optimal social media sharing.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{post.featureImage && (
|
||||||
|
<div className="image-preview">
|
||||||
|
<img
|
||||||
|
src={post.featureImage}
|
||||||
|
alt="Feature"
|
||||||
|
className="preview-image"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="upload-section">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="file-input"
|
||||||
|
disabled={loading}
|
||||||
|
id="feature-image-input"
|
||||||
|
/>
|
||||||
|
<label htmlFor="feature-image-input" className={`upload-button ${loading ? 'loading' : ''}`}>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="spinner" /> Uploading...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaCamera /> {post.featureImage ? 'Change Image' : 'Upload Image'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeatureImagePicker;
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
.feature-image-picker {
|
||||||
|
.panel-instruction {
|
||||||
|
font-size: 11px;
|
||||||
|
margin: 0px 0 16px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #2e2e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(.loading) {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
color: #667eea;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
// import { useState } from 'react';
|
||||||
|
// import { useDispatch } from 'react-redux';
|
||||||
|
// import { Upload, Modal, message } from 'antd';
|
||||||
|
// import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
// import { uploadApk, uploadImage } from '../../features/upload/upload';
|
||||||
|
|
||||||
|
// const ApkUplaod = ({ updateAppUrl, ImageLink }) => {
|
||||||
|
// const dispatch = useDispatch();
|
||||||
|
// const [fileList, setFileList] = useState([]);
|
||||||
|
// const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
// const allowedTypes = ['application/vnd.android.package-archive'];
|
||||||
|
|
||||||
|
|
||||||
|
// const handleChange = async ({ fileList: newFileList }) => {
|
||||||
|
// if (newFileList.length > 0) {
|
||||||
|
// const file = newFileList[0].originFileObj;
|
||||||
|
// const isApk = file && allowedTypes.includes(file.type);
|
||||||
|
|
||||||
|
// if (!isApk) {
|
||||||
|
// Modal.error({
|
||||||
|
// title: 'Invalid File Type',
|
||||||
|
// content: 'Only .apk files are allowed.',
|
||||||
|
// });
|
||||||
|
// setFileList([]);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const data = await dispatch(uploadApk(file)).unwrap();
|
||||||
|
|
||||||
|
// if (data?.data?.status) {
|
||||||
|
// const uploadedUrl = data.data.image;
|
||||||
|
|
||||||
|
// updateAppUrl(uploadedUrl); // ⬅️ This sends the uploaded URL to parent/component state
|
||||||
|
// setFileList([{ url: uploadedUrl, name: file.name }]);
|
||||||
|
|
||||||
|
// message.success('APK uploaded successfully!');
|
||||||
|
// } else {
|
||||||
|
// message.error(data?.data?.message || 'Upload failed');
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Upload error:', error);
|
||||||
|
// message.error('Upload failed');
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// updateAppUrl('');
|
||||||
|
// setFileList([]);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handlePreview = () => {
|
||||||
|
// Modal.info({
|
||||||
|
// title: 'APK File',
|
||||||
|
// content: 'APK files cannot be previewed.',
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
// const beforeUpload = (file) => {
|
||||||
|
|
||||||
|
// const isApk = allowedTypes.includes(file.type);
|
||||||
|
// if (!isApk) {
|
||||||
|
// Modal.error({
|
||||||
|
// title: 'Invalid File Type',
|
||||||
|
// content: 'Only .apk files are allowed.',
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// return isApk || Upload.LIST_IGNORE;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// <Upload
|
||||||
|
// fileList={fileList}
|
||||||
|
// onChange={handleChange}
|
||||||
|
// onPreview={handlePreview}
|
||||||
|
// beforeUpload={beforeUpload}
|
||||||
|
// showUploadList={{ showPreviewIcon: false }}
|
||||||
|
// customRequest={({ file, onSuccess }) => {
|
||||||
|
// setTimeout(() => onSuccess("ok"), 0);
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// {fileList.length === 0 ? (
|
||||||
|
// <div style={{ border: "1px dashed gray", borderRadius: "50%", height: "4.5rem", width: "4.5rem", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", cursor: "pointer" }}>
|
||||||
|
// <UploadOutlined />
|
||||||
|
// <div style={{ marginTop: 8 }}>Upload</div>
|
||||||
|
// </div>
|
||||||
|
// ) : null}
|
||||||
|
// </Upload>
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default ApkUplaod;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { Upload, Modal, message, Spin } from 'antd';
|
||||||
|
import { UploadOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { uploadApk } from '../../features/upload/upload';
|
||||||
|
|
||||||
|
const ApkUplaod = ({ updateAppUrl, AppLinkLink }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [fileList, setFileList] = useState([]);
|
||||||
|
const [uploading, setUploading] = useState(false); // <-- loader state
|
||||||
|
|
||||||
|
const allowedTypes = ['application/vnd.android.package-archive','application/x-msdownload'];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (AppLinkLink == null) {
|
||||||
|
updateAppUrl('');
|
||||||
|
setFileList([]);
|
||||||
|
}
|
||||||
|
}, [AppLinkLink])
|
||||||
|
|
||||||
|
const handleChange = async ({ fileList: newFileList }) => {
|
||||||
|
if (newFileList.length > 0) {
|
||||||
|
const file = newFileList[0].originFileObj;
|
||||||
|
const isApk = file && allowedTypes.includes(file.type);
|
||||||
|
|
||||||
|
if (!isApk) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Invalid File Type',
|
||||||
|
content: 'Only .apk files are allowed.',
|
||||||
|
});
|
||||||
|
setFileList([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(true); // start loader
|
||||||
|
try {
|
||||||
|
const data = await dispatch(uploadApk(file)).unwrap();
|
||||||
|
|
||||||
|
if (data?.data?.status) {
|
||||||
|
const uploadedUrl = data.data.image;
|
||||||
|
updateAppUrl(uploadedUrl);
|
||||||
|
setFileList([{ url: uploadedUrl, name: file.name }]);
|
||||||
|
message.success('APK uploaded successfully!');
|
||||||
|
} else {
|
||||||
|
message.error(data?.data?.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
message.error('Upload failed');
|
||||||
|
} finally {
|
||||||
|
setUploading(false); // stop loader
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateAppUrl('');
|
||||||
|
setFileList([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = () => {
|
||||||
|
Modal.info({
|
||||||
|
title: 'APK File',
|
||||||
|
content: 'APK files cannot be previewed.',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file) => {
|
||||||
|
const isApk = allowedTypes.includes(file.type);
|
||||||
|
if (!isApk) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Invalid File Type',
|
||||||
|
content: 'Only .apk files are allowed.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return isApk || Upload.LIST_IGNORE;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload
|
||||||
|
fileList={fileList}
|
||||||
|
onChange={handleChange}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
beforeUpload={beforeUpload}
|
||||||
|
showUploadList={{ showPreviewIcon: false }}
|
||||||
|
customRequest={({ file, onSuccess }) => {
|
||||||
|
setTimeout(() => onSuccess("ok"), 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileList.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px dashed gray",
|
||||||
|
borderRadius: "50%",
|
||||||
|
height: "4.5rem",
|
||||||
|
width: "4.5rem",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{uploading ? (
|
||||||
|
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UploadOutlined />
|
||||||
|
<div style={{ marginTop: 8 }}>Upload</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Upload>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApkUplaod;
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import './main.scss'
|
||||||
|
|
||||||
|
const Buttons = ({ buttonText, color, handleSubmit, icon, disabled }) =>(
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
className="primary_Button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
color={color}
|
||||||
|
disabled={disabled ? disabled : false}
|
||||||
|
>
|
||||||
|
{buttonText}{icon}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default Buttons;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Checkbox, Divider } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import classnames from "classnames"
|
||||||
|
const CheckboxGroup = Checkbox.Group;
|
||||||
|
|
||||||
|
export const CheckBoxGroup = ({ fieldState, fieldApi, faClass,style,ListOfCheckBox,defaultCheckedList, ...props }) => {
|
||||||
|
|
||||||
|
const plainOptions = ListOfCheckBox ;
|
||||||
|
const { field, onBlur, initialValue, forwardedRef, className, content, ...rest} = props.props
|
||||||
|
const [checkedList, setCheckedList] = useState(defaultCheckedList);
|
||||||
|
const [indeterminate, setIndeterminate] = useState(true);
|
||||||
|
const [checkAll, setCheckAll] = useState(false);
|
||||||
|
const onChange = (list) => {
|
||||||
|
setCheckedList(list);
|
||||||
|
setIndeterminate(!!list.length && list.length < plainOptions.length);
|
||||||
|
setCheckAll(list.length === plainOptions.length);
|
||||||
|
};
|
||||||
|
const onCheckAllChange = (e) => {
|
||||||
|
setCheckedList(e.target.checked ? plainOptions : []);
|
||||||
|
setIndeterminate(false);
|
||||||
|
setCheckAll(e.target.checked);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Checkbox indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
|
||||||
|
Check all
|
||||||
|
</Checkbox>
|
||||||
|
<Divider />
|
||||||
|
<CheckboxGroup
|
||||||
|
options={plainOptions}
|
||||||
|
value={checkedList}
|
||||||
|
onChange={onChange}
|
||||||
|
className={classnames(` ${className}`, { "is-invalid": fieldState.error })}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { DatePicker, Space } from 'antd';
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
export const DatePic = ({ fieldState, fieldApi, ...props }) => {
|
||||||
|
|
||||||
|
const { value } = fieldState;
|
||||||
|
|
||||||
|
const { required,field, onChange, onBlur, canSelectPast,initialValue, forwardedRef, min, className, content, faClass, icon, ...rest } = props;
|
||||||
|
|
||||||
|
const disabledDate = (current) => {
|
||||||
|
|
||||||
|
var date = new Date();
|
||||||
|
date.setDate(date.getDate() + 1);
|
||||||
|
return current.valueOf() <= date.setDate(date.getDate() - 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
return(
|
||||||
|
|
||||||
|
|
||||||
|
<Space direction="vertical">
|
||||||
|
<DatePicker
|
||||||
|
{...rest}
|
||||||
|
id={field}
|
||||||
|
type="date"
|
||||||
|
ref={forwardedRef}
|
||||||
|
required={required}
|
||||||
|
value={value}
|
||||||
|
disabledDate={canSelectPast ? "": disabledDate}
|
||||||
|
format="DD-MM-YYYY"
|
||||||
|
className={classnames(`form-control ${className}`, {
|
||||||
|
"is-invalid": fieldState.error,
|
||||||
|
})}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { DatePicker, Space } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||||
|
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
|
export const DatePicProd = ({
|
||||||
|
onChange,
|
||||||
|
canSelectPast,
|
||||||
|
valueData,
|
||||||
|
disabled,
|
||||||
|
cancelFuture,
|
||||||
|
minDate,
|
||||||
|
dontallowfeature,
|
||||||
|
}) => {
|
||||||
|
const disabledDate = (current) => {
|
||||||
|
var date = new Date();
|
||||||
|
date.setDate(date.getDate() + 1);
|
||||||
|
return current.valueOf() <= date.setDate(date.getDate() - 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledFutureDate = (current) => {
|
||||||
|
// Can not select days before today and today
|
||||||
|
return current && current > dayjs().endOf('day');
|
||||||
|
};
|
||||||
|
const disabledMinDate = (current) => {
|
||||||
|
if (minDate) {
|
||||||
|
return (
|
||||||
|
current &&
|
||||||
|
current < dayjs(minDate, 'YYYY-MM-DDTHH:mm:ss').startOf('day')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical">
|
||||||
|
<DatePicker
|
||||||
|
format="DD-MM-YYYY"
|
||||||
|
value={valueData ? dayjs(valueData, 'YYYY-MM-DDTHH:mm:ss') : ''}
|
||||||
|
disabledDate={(current) => {
|
||||||
|
if (dontallowfeature) {
|
||||||
|
return current && current > dayjs().endOf('day');
|
||||||
|
} else if (canSelectPast) {
|
||||||
|
if (cancelFuture) {
|
||||||
|
return disabledFutureDate(current) || disabledMinDate(current);
|
||||||
|
}
|
||||||
|
return disabledMinDate(current);
|
||||||
|
}
|
||||||
|
return disabledDate(current) || disabledMinDate(current);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
inputReadOnly={true}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import classnames from "classnames"
|
||||||
|
import FloatLabel from "./FloatLabel/index";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
export const DropDowns = ({ options, onChangeFunction,
|
||||||
|
isOnchanges,
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
searchKeys = ["label"],
|
||||||
|
disabled, ...props }) => {
|
||||||
|
const [inpValue, setInputValue] = useState('')
|
||||||
|
const [isOnchange, setisOnchange] = useState(isOnchanges)
|
||||||
|
const changeStatus = async () => {
|
||||||
|
await setisOnchange(true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
onBlur,
|
||||||
|
forwardedRef,
|
||||||
|
required,
|
||||||
|
valueData,
|
||||||
|
labelChange,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<FloatLabel
|
||||||
|
label={label}
|
||||||
|
value={inpValue}
|
||||||
|
isOnChange={valueData != null ? true : isOnchange}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{
|
||||||
|
width: 250
|
||||||
|
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue ? defaultValue : null}
|
||||||
|
optionFilterProp="children"
|
||||||
|
filterOption={
|
||||||
|
!labelChange
|
||||||
|
? (input, option) => {
|
||||||
|
return searchKeys.some((key) =>
|
||||||
|
(option?.[key] ?? "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
filterSort={!labelChange ? (optionA, optionB) =>
|
||||||
|
(optionA?.label ?? '')?.toLowerCase()?.localeCompare((optionB?.label ?? '').toLowerCase()) : null
|
||||||
|
}
|
||||||
|
value={valueData}
|
||||||
|
onChange={(e) => onChangeFunction(e)}
|
||||||
|
onSelect={(e) => changeStatus(e)}
|
||||||
|
options={options}
|
||||||
|
className={classnames(` ${className}`)}
|
||||||
|
disabled={disabled ? true : false}
|
||||||
|
/>
|
||||||
|
</FloatLabel>
|
||||||
|
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import classnames from "classnames"
|
||||||
|
import FloatLabel from "./FloatLabel/index";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
export const FilterButton = ({options,onChangeFunction,isOnchanges,className,defaultValue,disabled,onSearch,onSearchChange,...props}) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const [inpValue , setInputValue] = useState('')
|
||||||
|
const [isOnchange,setisOnchange] =useState(isOnchanges)
|
||||||
|
|
||||||
|
const changeStatus =async()=>{
|
||||||
|
await setisOnchange(true)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
onBlur,
|
||||||
|
forwardedRef,
|
||||||
|
required,
|
||||||
|
valueData,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return(
|
||||||
|
|
||||||
|
<FloatLabel
|
||||||
|
label={label}
|
||||||
|
value = {inpValue}
|
||||||
|
isOnChange ={isOnchange}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
style={{
|
||||||
|
width: 220,
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue?defaultValue:null}
|
||||||
|
optionFilterProp="children"
|
||||||
|
filterOption={(input, option) => (option?.label ?? '').includes(input)}
|
||||||
|
filterSort={(optionA, optionB) =>
|
||||||
|
(optionA?.label ?? '')?.toLowerCase()?.localeCompare((optionB?.label ?? '').toLowerCase())
|
||||||
|
}
|
||||||
|
value={valueData}
|
||||||
|
onChange={(e) =>onChangeFunction(e)}
|
||||||
|
onSelect={(e)=>changeStatus(e)}
|
||||||
|
options={options}
|
||||||
|
className={classnames(` ${className}`)}
|
||||||
|
disabled={disabled ? true : false}
|
||||||
|
/>
|
||||||
|
</FloatLabel>
|
||||||
|
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterButton;
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
.float-label {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: normal;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 12px;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
top: 14px;
|
||||||
|
transition: 0.2s ease all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-float {
|
||||||
|
font-size: 12px !important;
|
||||||
|
top:2px;
|
||||||
|
left: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
color: #007eb5;
|
||||||
|
z-index: 23;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
const FloatLabel = props => {
|
||||||
|
const [focus, setFocus] = useState(false);
|
||||||
|
const { children, label, value, isOnChange } = props;
|
||||||
|
|
||||||
|
const labelClass =
|
||||||
|
focus || (value && value.length !== 0) || isOnChange? "label label-float" : "label" ;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="float-label"
|
||||||
|
onBlur={() => setFocus(false)}
|
||||||
|
onFocus={() => setFocus(true)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<label className={labelClass}>{label}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FloatLabel;
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Input } from "antd";
|
||||||
|
import FloatLabel from "./FloatLabel/index.jsx";
|
||||||
|
import "./main.scss";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const InputField = ({ fieldState, fieldApi, ...props }) => {
|
||||||
|
|
||||||
|
const { value } = fieldState;
|
||||||
|
|
||||||
|
const [inpValue, setInputValue] = useState('')
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
onChange,
|
||||||
|
isOnChange,
|
||||||
|
onBlur,
|
||||||
|
label,
|
||||||
|
forwardedRef,
|
||||||
|
required,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="example">
|
||||||
|
<FloatLabel
|
||||||
|
label={label}
|
||||||
|
value={inpValue}
|
||||||
|
isOnChange={isOnChange}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...rest}
|
||||||
|
spellCheck={false}
|
||||||
|
id={field}
|
||||||
|
ref={forwardedRef}
|
||||||
|
defaultValue={value?.toString()}
|
||||||
|
required={required}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInputValue(e?.target?.value)
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (onBlur) {
|
||||||
|
onBlur(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</FloatLabel>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||