refactor: init overhaul of admin panel
This commit is contained in:
		
							parent
							
								
									9d78f1fdb4
								
							
						
					
					
						commit
						69468dc5bf
					
				
					 6 changed files with 2686 additions and 295 deletions
				
			
		|  | @ -3,42 +3,866 @@ | |||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Admin Center - Subscribers</title> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> | ||||
|     <title>Newsletter Admin Dashboard</title> | ||||
|     <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | ||||
|     <style> | ||||
|         * { | ||||
|             margin: 0; | ||||
|             padding: 0; | ||||
|             box-sizing: border-box; | ||||
|         } | ||||
| 
 | ||||
|         :root { | ||||
|             --primary: #667eea; | ||||
|             --primary-dark: #5a67d8; | ||||
|             --secondary: #764ba2; | ||||
|             --success: #48bb78; | ||||
|             --warning: #ed8936; | ||||
|             --error: #f56565; | ||||
|             --info: #4299e1; | ||||
|             --dark: #2d3748; | ||||
|             --light: #f7fafc; | ||||
|             --gray-100: #f7fafc; | ||||
|             --gray-200: #edf2f7; | ||||
|             --gray-300: #e2e8f0; | ||||
|             --gray-400: #cbd5e0; | ||||
|             --gray-500: #a0aec0; | ||||
|             --gray-600: #718096; | ||||
|             --gray-700: #4a5568; | ||||
|             --gray-800: #2d3748; | ||||
|             --gray-900: #1a202c; | ||||
|             --white: #ffffff; | ||||
|             --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | ||||
|             --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | ||||
|             --border-radius: 8px; | ||||
|             --border-radius-lg: 12px; | ||||
|         } | ||||
| 
 | ||||
|         body { | ||||
|             font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|             background: linear-gradient(135deg, var(--gray-100) 0%, var(--gray-200) 100%); | ||||
|             color: var(--gray-800); | ||||
|             line-height: 1.6; | ||||
|             min-height: 100vh; | ||||
|         } | ||||
| 
 | ||||
|         .container { | ||||
|             max-width: 1200px; | ||||
|             margin: 0 auto; | ||||
|             padding: 2rem; | ||||
|         } | ||||
| 
 | ||||
|         .header { | ||||
|             background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); | ||||
|             color: white; | ||||
|             padding: 2rem 0; | ||||
|             margin-bottom: 2rem; | ||||
|             border-radius: var(--border-radius-lg); | ||||
|             box-shadow: var(--shadow-lg); | ||||
|         } | ||||
| 
 | ||||
|         .header-content { | ||||
|             max-width: 1200px; | ||||
|             margin: 0 auto; | ||||
|             padding: 0 2rem; | ||||
|             display: flex; | ||||
|             justify-content: space-between; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .header h1 { | ||||
|             font-size: 2rem; | ||||
|             font-weight: 600; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             gap: 0.75rem; | ||||
|         } | ||||
| 
 | ||||
|         .nav-links { | ||||
|             display: flex; | ||||
|             gap: 1.5rem; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .nav-links a { | ||||
|             color: white; | ||||
|             text-decoration: none; | ||||
|             padding: 0.5rem 1rem; | ||||
|             border-radius: var(--border-radius); | ||||
|             transition: all 0.3s ease; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             gap: 0.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .nav-links a:hover { | ||||
|             background: rgba(255, 255, 255, 0.2); | ||||
|             transform: translateY(-2px); | ||||
|         } | ||||
| 
 | ||||
|         .stats-grid { | ||||
|             display: grid; | ||||
|             grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||||
|             gap: 1.5rem; | ||||
|             margin-bottom: 2rem; | ||||
|         } | ||||
| 
 | ||||
|         .stat-card { | ||||
|             background: var(--white); | ||||
|             padding: 2rem; | ||||
|             border-radius: var(--border-radius-lg); | ||||
|             box-shadow: var(--shadow); | ||||
|             transition: all 0.3s ease; | ||||
|             border-left: 4px solid var(--primary); | ||||
|         } | ||||
| 
 | ||||
|         .stat-card:hover { | ||||
|             transform: translateY(-4px); | ||||
|             box-shadow: var(--shadow-lg); | ||||
|         } | ||||
| 
 | ||||
|         .stat-card.success { border-left-color: var(--success); } | ||||
|         .stat-card.warning { border-left-color: var(--warning); } | ||||
|         .stat-card.info { border-left-color: var(--info); } | ||||
|         .stat-card.error { border-left-color: var(--error); } | ||||
| 
 | ||||
|         .stat-header { | ||||
|             display: flex; | ||||
|             justify-content: space-between; | ||||
|             align-items: center; | ||||
|             margin-bottom: 1rem; | ||||
|         } | ||||
| 
 | ||||
|         .stat-icon { | ||||
|             width: 3rem; | ||||
|             height: 3rem; | ||||
|             border-radius: 50%; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|             font-size: 1.25rem; | ||||
|             color: white; | ||||
|         } | ||||
| 
 | ||||
|         .stat-icon.primary { background: var(--primary); } | ||||
|         .stat-icon.success { background: var(--success); } | ||||
|         .stat-icon.warning { background: var(--warning); } | ||||
|         .stat-icon.info { background: var(--info); } | ||||
| 
 | ||||
|         .stat-number { | ||||
|             font-size: 2.5rem; | ||||
|             font-weight: 700; | ||||
|             color: var(--gray-800); | ||||
|         } | ||||
| 
 | ||||
|         .stat-label { | ||||
|             color: var(--gray-600); | ||||
|             font-size: 0.875rem; | ||||
|             font-weight: 500; | ||||
|             text-transform: uppercase; | ||||
|             letter-spacing: 0.05em; | ||||
|         } | ||||
| 
 | ||||
|         .main-content { | ||||
|             display: grid; | ||||
|             grid-template-columns: 2fr 1fr; | ||||
|             gap: 2rem; | ||||
|             margin-bottom: 2rem; | ||||
|         } | ||||
| 
 | ||||
|         .card { | ||||
|             background: var(--white); | ||||
|             border-radius: var(--border-radius-lg); | ||||
|             box-shadow: var(--shadow); | ||||
|             overflow: hidden; | ||||
|         } | ||||
| 
 | ||||
|         .card-header { | ||||
|             padding: 1.5rem; | ||||
|             border-bottom: 1px solid var(--gray-200); | ||||
|             display: flex; | ||||
|             justify-content: between; | ||||
|             align-items: center; | ||||
|         } | ||||
| 
 | ||||
|         .card-title { | ||||
|             font-size: 1.25rem; | ||||
|             font-weight: 600; | ||||
|             color: var(--gray-800); | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             gap: 0.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .card-body { | ||||
|             padding: 1.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .search-box { | ||||
|             position: relative; | ||||
|             margin-bottom: 1.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .search-input { | ||||
|             width: 100%; | ||||
|             padding: 0.75rem 1rem 0.75rem 2.5rem; | ||||
|             border: 2px solid var(--gray-300); | ||||
|             border-radius: var(--border-radius); | ||||
|             font-size: 0.875rem; | ||||
|             transition: all 0.3s ease; | ||||
|         } | ||||
| 
 | ||||
|         .search-input:focus { | ||||
|             outline: none; | ||||
|             border-color: var(--primary); | ||||
|             box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | ||||
|         } | ||||
| 
 | ||||
|         .search-icon { | ||||
|             position: absolute; | ||||
|             left: 0.75rem; | ||||
|             top: 50%; | ||||
|             transform: translateY(-50%); | ||||
|             color: var(--gray-400); | ||||
|         } | ||||
| 
 | ||||
|         .table-container { | ||||
|             overflow-x: auto; | ||||
|         } | ||||
| 
 | ||||
|         .table { | ||||
|             width: 100%; | ||||
|             border-collapse: collapse; | ||||
|         } | ||||
| 
 | ||||
|         .table th, | ||||
|         .table td { | ||||
|             padding: 1rem; | ||||
|             text-align: left; | ||||
|             border-bottom: 1px solid var(--gray-200); | ||||
|         } | ||||
| 
 | ||||
|         .table th { | ||||
|             background: var(--gray-50); | ||||
|             font-weight: 600; | ||||
|             color: var(--gray-700); | ||||
|             font-size: 0.875rem; | ||||
|             text-transform: uppercase; | ||||
|             letter-spacing: 0.05em; | ||||
|         } | ||||
| 
 | ||||
|         .table tbody tr:hover { | ||||
|             background: var(--gray-50); | ||||
|         } | ||||
| 
 | ||||
|         .btn { | ||||
|             display: inline-flex; | ||||
|             align-items: center; | ||||
|             gap: 0.5rem; | ||||
|             padding: 0.75rem 1.5rem; | ||||
|             border: none; | ||||
|             border-radius: var(--border-radius); | ||||
|             font-size: 0.875rem; | ||||
|             font-weight: 500; | ||||
|             text-decoration: none; | ||||
|             cursor: pointer; | ||||
|             transition: all 0.3s ease; | ||||
|             text-align: center; | ||||
|             justify-content: center; | ||||
|         } | ||||
| 
 | ||||
|         .btn-primary { | ||||
|             background: var(--primary); | ||||
|             color: white; | ||||
|         } | ||||
| 
 | ||||
|         .btn-primary:hover { | ||||
|             background: var(--primary-dark); | ||||
|             transform: translateY(-2px); | ||||
|             box-shadow: var(--shadow-lg); | ||||
|         } | ||||
| 
 | ||||
|         .btn-success { | ||||
|             background: var(--success); | ||||
|             color: white; | ||||
|         } | ||||
| 
 | ||||
|         .btn-warning { | ||||
|             background: var(--warning); | ||||
|             color: white; | ||||
|         } | ||||
| 
 | ||||
|         .btn-error { | ||||
|             background: var(--error); | ||||
|             color: white; | ||||
|         } | ||||
| 
 | ||||
|         .btn-small { | ||||
|             padding: 0.5rem 1rem; | ||||
|             font-size: 0.75rem; | ||||
|         } | ||||
| 
 | ||||
|         .flash-messages { | ||||
|             margin-bottom: 1.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .flash { | ||||
|             padding: 1rem; | ||||
|             border-radius: var(--border-radius); | ||||
|             margin-bottom: 0.5rem; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             gap: 0.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .flash.success { | ||||
|             background: #f0fff4; | ||||
|             color: #22543d; | ||||
|             border: 1px solid #c6f6d5; | ||||
|         } | ||||
| 
 | ||||
|         .flash.error { | ||||
|             background: #fff5f5; | ||||
|             color: #742a2a; | ||||
|             border: 1px solid #fed7d7; | ||||
|         } | ||||
| 
 | ||||
|         .flash.warning { | ||||
|             background: #fffbeb; | ||||
|             color: #744210; | ||||
|             border: 1px solid #feebc8; | ||||
|         } | ||||
| 
 | ||||
|         .flash.info { | ||||
|             background: #ebf8ff; | ||||
|             color: #2a4a5a; | ||||
|             border: 1px solid #bee3f8; | ||||
|         } | ||||
| 
 | ||||
|         .recent-newsletters { | ||||
|             max-height: 400px; | ||||
|             overflow-y: auto; | ||||
|         } | ||||
| 
 | ||||
|         .newsletter-item { | ||||
|             padding: 1rem; | ||||
|             border-bottom: 1px solid var(--gray-200); | ||||
|             transition: all 0.3s ease; | ||||
|         } | ||||
| 
 | ||||
|         .newsletter-item:hover { | ||||
|             background: var(--gray-50); | ||||
|         } | ||||
| 
 | ||||
|         .newsletter-item:last-child { | ||||
|             border-bottom: none; | ||||
|         } | ||||
| 
 | ||||
|         .newsletter-subject { | ||||
|             font-weight: 600; | ||||
|             color: var(--gray-800); | ||||
|             margin-bottom: 0.25rem; | ||||
|         } | ||||
| 
 | ||||
|         .newsletter-meta { | ||||
|             font-size: 0.75rem; | ||||
|             color: var(--gray-500); | ||||
|             display: flex; | ||||
|             gap: 1rem; | ||||
|         } | ||||
| 
 | ||||
|         .pagination { | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             gap: 0.5rem; | ||||
|             margin-top: 1.5rem; | ||||
|         } | ||||
| 
 | ||||
|         .pagination a, | ||||
|         .pagination span { | ||||
|             padding: 0.5rem 0.75rem; | ||||
|             border: 1px solid var(--gray-300); | ||||
|             border-radius: var(--border-radius); | ||||
|             color: var(--gray-600); | ||||
|             text-decoration: none; | ||||
|             transition: all 0.3s ease; | ||||
|         } | ||||
| 
 | ||||
|         .pagination a:hover { | ||||
|             background: var(--primary); | ||||
|             color: white; | ||||
|             border-color: var(--primary); | ||||
|         } | ||||
| 
 | ||||
|         .pagination .current { | ||||
|             background: var(--primary); | ||||
|             color: white; | ||||
|             border-color: var(--primary); | ||||
|         } | ||||
| 
 | ||||
|         .add-subscriber { | ||||
|             display: flex; | ||||
|             gap: 0.5rem; | ||||
|             margin-bottom: 1rem; | ||||
|         } | ||||
| 
 | ||||
|         .add-subscriber input { | ||||
|             flex: 1; | ||||
|             padding: 0.75rem; | ||||
|             border: 2px solid var(--gray-300); | ||||
|             border-radius: var(--border-radius); | ||||
|             transition: all 0.3s ease; | ||||
|         } | ||||
| 
 | ||||
|         .add-subscriber input:focus { | ||||
|             outline: none; | ||||
|             border-color: var(--primary); | ||||
|             box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | ||||
|         } | ||||
| 
 | ||||
|         @media (max-width: 768px) { | ||||
|             .container { | ||||
|                 padding: 1rem; | ||||
|             } | ||||
| 
 | ||||
|             .header-content { | ||||
|                 flex-direction: column; | ||||
|                 gap: 1rem; | ||||
|                 text-align: center; | ||||
|             } | ||||
| 
 | ||||
|             .nav-links { | ||||
|                 flex-wrap: wrap; | ||||
|                 justify-content: center; | ||||
|             } | ||||
| 
 | ||||
|             .main-content { | ||||
|                 grid-template-columns: 1fr; | ||||
|             } | ||||
| 
 | ||||
|             .stats-grid { | ||||
|                 grid-template-columns: 1fr; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .loading { | ||||
|             display: none; | ||||
|             text-align: center; | ||||
|             padding: 2rem; | ||||
|         } | ||||
| 
 | ||||
|         .spinner { | ||||
|             display: inline-block; | ||||
|             width: 2rem; | ||||
|             height: 2rem; | ||||
|             border: 3px solid var(--gray-300); | ||||
|             border-top: 3px solid var(--primary); | ||||
|             border-radius: 50%; | ||||
|             animation: spin 1s linear infinite; | ||||
|         } | ||||
| 
 | ||||
|         @keyframes spin { | ||||
|             0% { transform: rotate(0deg); } | ||||
|             100% { transform: rotate(360deg); } | ||||
|         } | ||||
| 
 | ||||
|         .badge { | ||||
|             display: inline-flex; | ||||
|             align-items: center; | ||||
|             padding: 0.25rem 0.75rem; | ||||
|             border-radius: 9999px; | ||||
|             font-size: 0.75rem; | ||||
|             font-weight: 500; | ||||
|         } | ||||
| 
 | ||||
|         .badge.success { | ||||
|             background: #f0fff4; | ||||
|             color: #22543d; | ||||
|         } | ||||
| 
 | ||||
|         .badge.error { | ||||
|             background: #fff5f5; | ||||
|             color: #742a2a; | ||||
|         } | ||||
| 
 | ||||
|         .empty-state { | ||||
|             text-align: center; | ||||
|             padding: 3rem; | ||||
|             color: var(--gray-500); | ||||
|         } | ||||
| 
 | ||||
|         .empty-state i { | ||||
|             font-size: 3rem; | ||||
|             margin-bottom: 1rem; | ||||
|             color: var(--gray-400); | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <header class="header"> | ||||
|         <div class="header-content"> | ||||
|             <h1> | ||||
|                 <i class="fas fa-envelope"></i> | ||||
|                 Newsletter Admin | ||||
|             </h1> | ||||
|             <nav class="nav-links"> | ||||
|                 <a href="/"><i class="fas fa-tachometer-alt"></i> Dashboard</a> | ||||
|                 <a href="/subscribers"><i class="fas fa-users"></i> Subscribers</a> | ||||
|                 <a href="/send_newsletter"><i class="fas fa-paper-plane"></i> Send Newsletter</a> | ||||
|                 <a href="/newsletter_history"><i class="fas fa-history"></i> History</a> | ||||
|                 <a href="/logout"><i class="fas fa-sign-out-alt"></i> Logout</a> | ||||
|             </nav> | ||||
|         </div> | ||||
|     </header> | ||||
| 
 | ||||
|     <h1>Subscribers</h1> | ||||
|     <p> | ||||
|       <a href="{{ url_for('send_update') }}">Send Update Email</a>| | ||||
|       <a href="{{ url_for('logout') }}">Logout</a> | ||||
|     </p> | ||||
|     <div class="container"> | ||||
|         <!-- Flash Messages --> | ||||
|         {% with messages = get_flashed_messages(with_categories=true) %} | ||||
|         {% if messages %} | ||||
|         <div class="flash-messages"> | ||||
|             {% for category, message in messages %} | ||||
|             <div class="flash {{ category }}"> | ||||
|                 {% if category == 'success' %} | ||||
|                 <i class="fas fa-check-circle"></i> | ||||
|                 {% elif category == 'error' %} | ||||
|                 <i class="fas fa-exclamation-circle"></i> | ||||
|                 {% elif category == 'warning' %} | ||||
|                 <i class="fas fa-exclamation-triangle"></i> | ||||
|                 {% else %} | ||||
|                 <i class="fas fa-info-circle"></i> | ||||
|                 {% endif %} | ||||
|                 {{ message }} | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         {% endif %} | ||||
|         {% endwith %} | ||||
| 
 | ||||
|     {% with messages = get_flashed_messages(with_categories=true) %} | ||||
|       {% if messages %} | ||||
|         {% for category, message in messages %} | ||||
|           <div class="flash">{{ message }}</div> | ||||
|         {% endfor %} | ||||
|       {% endif %} | ||||
|     {% endwith %} | ||||
|         <!-- Statistics Cards --> | ||||
|         <div class="stats-grid"> | ||||
|             <div class="stat-card success"> | ||||
|                 <div class="stat-header"> | ||||
|                     <div> | ||||
|                         <div class="stat-number">{{ stats.total_active }}</div> | ||||
|                         <div class="stat-label">Active Subscribers</div> | ||||
|                     </div> | ||||
|                     <div class="stat-icon success"> | ||||
|                         <i class="fas fa-users"></i> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|     {% if emails %} | ||||
|         <table> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th>Email Address</th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 {% for email in emails %} | ||||
|                     <tr> | ||||
|                         <td>{{ email }}</td> | ||||
|                     </tr> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|     {% else %} | ||||
|         <p>No subscribers found.</p> | ||||
|     {% endif %} | ||||
|             <div class="stat-card info"> | ||||
|                 <div class="stat-header"> | ||||
|                     <div> | ||||
|                         <div class="stat-number">{{ stats.recent_signups }}</div> | ||||
|                         <div class="stat-label">New This Month</div> | ||||
|                     </div> | ||||
|                     <div class="stat-icon info"> | ||||
|                         <i class="fas fa-user-plus"></i> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="stat-card primary"> | ||||
|                 <div class="stat-header"> | ||||
|                     <div> | ||||
|                         <div class="stat-number">{{ stats.newsletters_sent }}</div> | ||||
|                         <div class="stat-label">Newsletters Sent</div> | ||||
|                     </div> | ||||
|                     <div class="stat-icon primary"> | ||||
|                         <i class="fas fa-paper-plane"></i> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="stat-card warning"> | ||||
|                 <div class="stat-header"> | ||||
|                     <div> | ||||
|                         <div class="stat-number">{{ stats.total_unsubscribed }}</div> | ||||
|                         <div class="stat-label">Unsubscribed</div> | ||||
|                     </div> | ||||
|                     <div class="stat-icon warning"> | ||||
|                         <i class="fas fa-user-minus"></i> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Main Content --> | ||||
|         <div class="main-content"> | ||||
|             <!-- Recent Subscribers --> | ||||
|             <div class="card"> | ||||
|                 <div class="card-header"> | ||||
|                     <h2 class="card-title"> | ||||
|                         <i class="fas fa-users"></i> | ||||
|                         Recent Subscribers | ||||
|                     </h2> | ||||
|                     <a href="/subscribers" class="btn btn-primary btn-small"> | ||||
|                         <i class="fas fa-eye"></i> View All | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <div class="card-body"> | ||||
|                     <!-- Add Subscriber Form --> | ||||
|                     <div class="add-subscriber"> | ||||
|                         <input type="email" id="newEmail" placeholder="Add new subscriber email..." /> | ||||
|                         <button onclick="addSubscriber()" class="btn btn-success"> | ||||
|                             <i class="fas fa-plus"></i> Add | ||||
|                         </button> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <!-- Search Box --> | ||||
|                     <div class="search-box"> | ||||
|                         <i class="fas fa-search search-icon"></i> | ||||
|                         <input type="text" class="search-input" placeholder="Search subscribers..."  | ||||
|                                value="{{ search }}" onkeyup="filterSubscribers(this.value)"> | ||||
|                     </div> | ||||
| 
 | ||||
|                     {% if subscribers %} | ||||
|                     <div class="table-container"> | ||||
|                         <table class="table"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th>Email Address</th> | ||||
|                                     <th>Joined</th> | ||||
|                                     <th>Source</th> | ||||
|                                     <th>Actions</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 {% for subscriber in subscribers %} | ||||
|                                 <tr> | ||||
|                                     <td>{{ subscriber.email }}</td> | ||||
|                                     <td>{{ subscriber.subscribed_at.strftime('%b %d, %Y') if subscriber.subscribed_at else 'N/A' }}</td> | ||||
|                                     <td> | ||||
|                                         <span class="badge success">{{ subscriber.source or 'manual' }}</span> | ||||
|                                     </td> | ||||
|                                     <td> | ||||
|                                         <button onclick="removeSubscriber('{{ subscriber.email }}')"  | ||||
|                                                 class="btn btn-error btn-small"> | ||||
|                                             <i class="fas fa-trash"></i> | ||||
|                                         </button> | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                                 {% endfor %} | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <!-- Pagination --> | ||||
|                     {% if pagination.total_pages > 1 %} | ||||
|                     <div class="pagination"> | ||||
|                         {% if pagination.page > 1 %} | ||||
|                         <a href="?page={{ pagination.page - 1 }}{% if search %}&search={{ search }}{% endif %}"> | ||||
|                             <i class="fas fa-chevron-left"></i> Previous | ||||
|                         </a> | ||||
|                         {% endif %} | ||||
| 
 | ||||
|                         {% for page_num in range(1, pagination.total_pages + 1) %} | ||||
|                         {% if page_num == pagination.page %} | ||||
|                         <span class="current">{{ page_num }}</span> | ||||
|                         {% else %} | ||||
|                         <a href="?page={{ page_num }}{% if search %}&search={{ search }}{% endif %}">{{ page_num }}</a> | ||||
|                         {% endif %} | ||||
|                         {% endfor %} | ||||
| 
 | ||||
|                         {% if pagination.page < pagination.total_pages %} | ||||
|                         <a href="?page={{ pagination.page + 1 }}{% if search %}&search={{ search }}{% endif %}"> | ||||
|                             Next <i class="fas fa-chevron-right"></i> | ||||
|                         </a> | ||||
|                         {% endif %} | ||||
|                     </div> | ||||
|                     {% endif %} | ||||
|                     {% else %} | ||||
|                     <div class="empty-state"> | ||||
|                         <i class="fas fa-users"></i> | ||||
|                         <h3>No subscribers yet</h3> | ||||
|                         <p>Start building your subscriber list!</p> | ||||
|                     </div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Recent Newsletters --> | ||||
|             <div class="card"> | ||||
|                 <div class="card-header"> | ||||
|                     <h2 class="card-title"> | ||||
|                         <i class="fas fa-history"></i> | ||||
|                         Recent Newsletters | ||||
|                     </h2> | ||||
|                     <a href="/send_newsletter" class="btn btn-primary btn-small"> | ||||
|                         <i class="fas fa-plus"></i> New | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <div class="card-body"> | ||||
|                     {% if recent_newsletters %} | ||||
|                     <div class="recent-newsletters"> | ||||
|                         {% for newsletter in recent_newsletters %} | ||||
|                         <div class="newsletter-item"> | ||||
|                             <div class="newsletter-subject">{{ newsletter.subject }}</div> | ||||
|                             <div class="newsletter-meta"> | ||||
|                                 <span><i class="fas fa-calendar"></i> {{ newsletter.sent_at.strftime('%b %d, %Y at %H:%M') if newsletter.sent_at else 'N/A' }}</span> | ||||
|                                 <span><i class="fas fa-user"></i> {{ newsletter.sent_by or 'System' }}</span> | ||||
|                                 {% if newsletter.success_count is not none %} | ||||
|                                 <span><i class="fas fa-check"></i> {{ newsletter.success_count }} sent</span> | ||||
|                                 {% endif %} | ||||
|                                 {% if newsletter.failure_count and newsletter.failure_count > 0 %} | ||||
|                                 <span><i class="fas fa-times"></i> {{ newsletter.failure_count }} failed</span> | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         {% endfor %} | ||||
|                     </div> | ||||
|                     {% else %} | ||||
|                     <div class="empty-state"> | ||||
|                         <i class="fas fa-paper-plane"></i> | ||||
|                         <h3>No newsletters sent yet</h3> | ||||
|                         <p>Send your first newsletter to get started!</p> | ||||
|                         <a href="/send_newsletter" class="btn btn-primary"> | ||||
|                             <i class="fas fa-plus"></i> Create Newsletter | ||||
|                         </a> | ||||
|                     </div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <script> | ||||
|         async function addSubscriber() { | ||||
|             const emailInput = document.getElementById('newEmail'); | ||||
|             const email = emailInput.value.trim(); | ||||
| 
 | ||||
|             if (!email) { | ||||
|                 alert('Please enter an email address'); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!isValidEmail(email)) { | ||||
|                 alert('Please enter a valid email address'); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 const response = await fetch('/add_subscriber', { | ||||
|                     method: 'POST', | ||||
|                     headers: { | ||||
|                         'Content-Type': 'application/json', | ||||
|                     }, | ||||
|                     body: JSON.stringify({ email: email }) | ||||
|                 }); | ||||
| 
 | ||||
|                 const result = await response.json(); | ||||
| 
 | ||||
|                 if (result.success) { | ||||
|                     showFlash('success', result.message); | ||||
|                     emailInput.value = ''; | ||||
|                     setTimeout(() => location.reload(), 1000); | ||||
|                 } else { | ||||
|                     showFlash('error', result.message); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 showFlash('error', 'An error occurred while adding the subscriber'); | ||||
|                 console.error('Error:', error); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         async function removeSubscriber(email) { | ||||
|             if (!confirm(`Are you sure you want to unsubscribe ${email}?`)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 const response = await fetch('/remove_subscriber', { | ||||
|                     method: 'POST', | ||||
|                     headers: { | ||||
|                         'Content-Type': 'application/json', | ||||
|                     }, | ||||
|                     body: JSON.stringify({ email: email }) | ||||
|                 }); | ||||
| 
 | ||||
|                 const result = await response.json(); | ||||
| 
 | ||||
|                 if (result.success) { | ||||
|                     showFlash('success', result.message); | ||||
|                     setTimeout(() => location.reload(), 1000); | ||||
|                 } else { | ||||
|                     showFlash('error', result.message); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 showFlash('error', 'An error occurred while removing the subscriber'); | ||||
|                 console.error('Error:', error); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         function filterSubscribers(searchTerm) { | ||||
|             if (searchTerm.length > 2 || searchTerm.length === 0) { | ||||
|                 const url = new URL(window.location); | ||||
|                 if (searchTerm) { | ||||
|                     url.searchParams.set('search', searchTerm); | ||||
|                 } else { | ||||
|                     url.searchParams.delete('search'); | ||||
|                 } | ||||
|                 url.searchParams.set('page', '1'); | ||||
|                  | ||||
|                 // Debounce the search | ||||
|                 clearTimeout(window.searchTimeout); | ||||
|                 window.searchTimeout = setTimeout(() => { | ||||
|                     window.location = url; | ||||
|                 }, 500); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         function isValidEmail(email) { | ||||
|             const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||||
|             return emailRegex.test(email); | ||||
|         } | ||||
| 
 | ||||
|         function showFlash(type, message) { | ||||
|             const flashContainer = document.querySelector('.flash-messages') || createFlashContainer(); | ||||
|             const flash = document.createElement('div'); | ||||
|             flash.className = `flash ${type}`; | ||||
|              | ||||
|             const icon = type === 'success' ? 'check-circle' :  | ||||
|                         type === 'error' ? 'exclamation-circle' :  | ||||
|                         type === 'warning' ? 'exclamation-triangle' : 'info-circle'; | ||||
|              | ||||
|             flash.innerHTML = `<i class="fas fa-${icon}"></i> ${message}`; | ||||
|             flashContainer.appendChild(flash); | ||||
| 
 | ||||
|             setTimeout(() => { | ||||
|                 flash.remove(); | ||||
|             }, 5000); | ||||
|         } | ||||
| 
 | ||||
|         function createFlashContainer() { | ||||
|             const container = document.createElement('div'); | ||||
|             container.className = 'flash-messages'; | ||||
|             document.querySelector('.container').insertBefore(container, document.querySelector('.stats-grid')); | ||||
|             return container; | ||||
|         } | ||||
| 
 | ||||
|         // Handle Enter key for add subscriber | ||||
|         document.getElementById('newEmail').addEventListener('keypress', function(e) { | ||||
|             if (e.key === 'Enter') { | ||||
|                 addSubscriber(); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Auto-refresh stats every 30 seconds | ||||
|         setInterval(async () => { | ||||
|             try { | ||||
|                 const response = await fetch('/api/stats'); | ||||
|                 const stats = await response.json(); | ||||
|                  | ||||
|                 document.querySelector('.stat-card.success .stat-number').textContent = stats.total_active; | ||||
|                 document.querySelector('.stat-card.info .stat-number').textContent = stats.recent_signups; | ||||
|                 document.querySelector('.stat-card.primary .stat-number').textContent = stats.newsletters_sent; | ||||
|                 document.querySelector('.stat-card.warning .stat-number').textContent = stats.total_unsubscribed; | ||||
|             } catch (error) { | ||||
|                 console.error('Error updating stats:', error); | ||||
|             } | ||||
|         }, 30000); | ||||
|     </script> | ||||
| </body> | ||||
| </html> | ||||
| </html> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Cipher Vance
						Cipher Vance