T.ME/BIBIL_0DAY
CasperSecurity


Server : Apache/2
System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User : gositeme ( 1004)
PHP Version : 8.2.29
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Directory :  /home/gositeme/domains/lavocat.ca/private_html/src/pages/admin/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/lavocat.ca/private_html/src/pages/admin/notifications.tsx
import React, { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '../../components/LayoutWithSidebar';
import { motion } from 'framer-motion';
import toast from 'react-hot-toast';

interface NotificationCampaign {
  id: string;
  type: 'banner' | 'toast' | 'modal' | 'exit-intent';
  title: string;
  message: string;
  actionText?: string;
  actionUrl?: string;
  priority: 'low' | 'medium' | 'high' | 'urgent';
  placement: 'top' | 'bottom' | 'center';
  targetPages?: string[];
  targetRegion?: string;
  language: 'en' | 'fr' | 'both';
  isActive: boolean;
  expiresAt?: string;
  showAfterSeconds?: number;
  maxViews?: number;
  createdAt: string;
}

const AdminNotifications: React.FC = () => {
  const { data: session, status } = useSession();
  const router = useRouter();
  const [campaigns, setCampaigns] = useState<NotificationCampaign[]>([]);
  const [showCreateForm, setShowCreateForm] = useState(false);
  const [editingCampaign, setEditingCampaign] = useState<NotificationCampaign | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // Form state
  const [formData, setFormData] = useState<{
    type: 'banner' | 'toast' | 'modal' | 'exit-intent';
    title: string;
    message: string;
    actionText: string;
    actionUrl: string;
    priority: 'low' | 'medium' | 'high' | 'urgent';
    placement: 'top' | 'bottom' | 'center';
    targetPages: string;
    targetRegion: string;
    language: 'en' | 'fr' | 'both';
    isActive: boolean;
    expiresAt: string;
    showAfterSeconds: number;
    maxViews: number;
  }>({
    type: 'banner',
    title: '',
    message: '',
    actionText: '',
    actionUrl: '',
    priority: 'medium',
    placement: 'top',
    targetPages: '',
    targetRegion: '',
    language: 'both',
    isActive: true,
    expiresAt: '',
    showAfterSeconds: 0,
    maxViews: 5
  });

  useEffect(() => {
    if (status === 'unauthenticated') {
      router.push('/auth/login');
      return;
    }

    if (session?.user?.role !== 'ADMIN') {
      router.push('/');
      return;
    }

    fetchCampaigns();
  }, [session, status, router]);

  const fetchCampaigns = async () => {
    try {
      setIsLoading(true);
      const response = await fetch('/api/public/notifications/campaigns');
      if (response.ok) {
        const data = await response.json();
        setCampaigns(data);
      }
    } catch (error) {
      console.error('Error fetching campaigns:', error);
      toast.error('Failed to load notification campaigns');
    } finally {
      setIsLoading(false);
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!formData.title || !formData.message) {
      toast.error('Title and message are required');
      return;
    }

    try {
      const campaignData = {
        ...formData,
        id: editingCampaign?.id || `campaign_${Date.now()}`,
        targetPages: formData.targetPages ? formData.targetPages.split(',').map(p => p.trim()) : undefined,
        createdAt: editingCampaign?.createdAt || new Date().toISOString()
      };

      if (editingCampaign) {
        setCampaigns(prev => prev.map(c => c.id === editingCampaign.id ? campaignData : c));
        toast.success('Campaign updated successfully');
      } else {
        setCampaigns(prev => [...prev, campaignData]);
        toast.success('Campaign created successfully');
      }

      resetForm();
    } catch (error) {
      toast.error('Failed to save campaign');
    }
  };

  const resetForm = () => {
    setFormData({
      type: 'banner',
      title: '',
      message: '',
      actionText: '',
      actionUrl: '',
      priority: 'medium',
      placement: 'top',
      targetPages: '',
      targetRegion: '',
      language: 'both',
      isActive: true,
      expiresAt: '',
      showAfterSeconds: 0,
      maxViews: 5
    });
    setEditingCampaign(null);
    setShowCreateForm(false);
  };

  const handleEdit = (campaign: NotificationCampaign) => {
    setEditingCampaign(campaign);
    setFormData({
      type: campaign.type,
      title: campaign.title,
      message: campaign.message,
      actionText: campaign.actionText || '',
      actionUrl: campaign.actionUrl || '',
      priority: campaign.priority,
      placement: campaign.placement,
      targetPages: campaign.targetPages?.join(', ') || '',
      targetRegion: campaign.targetRegion || '',
      language: campaign.language,
      isActive: campaign.isActive,
      expiresAt: campaign.expiresAt ? campaign.expiresAt.slice(0, 16) : '',
      showAfterSeconds: campaign.showAfterSeconds || 0,
      maxViews: campaign.maxViews || 5
    });
    setShowCreateForm(true);
  };

  const handleToggleActive = (campaignId: string) => {
    setCampaigns(prev => prev.map(c => 
      c.id === campaignId ? { ...c, isActive: !c.isActive } : c
    ));
    toast.success('Campaign status updated');
  };

  const handleDelete = (campaignId: string) => {
    if (confirm('Are you sure you want to delete this campaign?')) {
      setCampaigns(prev => prev.filter(c => c.id !== campaignId));
      toast.success('Campaign deleted');
    }
  };

  const getPriorityColor = (priority: string) => {
    switch (priority) {
      case 'urgent': return 'text-red-600 bg-red-100';
      case 'high': return 'text-orange-600 bg-orange-100';
      case 'medium': return 'text-blue-600 bg-blue-100';
      default: return 'text-gray-600 bg-gray-100';
    }
  };

  if (status === 'loading') {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
        <span className="ml-3 text-gray-600">Loading...</span>
      </div>
    );
  }

  return (
    <LayoutWithSidebar>
      <div className="space-y-6">
        <div className="flex justify-between items-center">
          <div>
            <h1 className="text-3xl font-bold text-gray-900">Public Notifications</h1>
            <p className="text-gray-600">Manage banners, toasts, and engagement prompts for public users</p>
          </div>
          <button
            onClick={() => setShowCreateForm(true)}
            className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition-colors"
          >
            Create Campaign
          </button>
        </div>

        {/* Campaign Stats */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
          <div className="bg-white p-4 rounded-lg shadow">
            <div className="text-2xl font-bold text-primary">{campaigns.length}</div>
            <div className="text-sm text-gray-600">Total Campaigns</div>
          </div>
          <div className="bg-white p-4 rounded-lg shadow">
            <div className="text-2xl font-bold text-green-600">{campaigns.filter(c => c.isActive).length}</div>
            <div className="text-sm text-gray-600">Active</div>
          </div>
          <div className="bg-white p-4 rounded-lg shadow">
            <div className="text-2xl font-bold text-red-600">{campaigns.filter(c => c.priority === 'urgent').length}</div>
            <div className="text-sm text-gray-600">Urgent</div>
          </div>
          <div className="bg-white p-4 rounded-lg shadow">
            <div className="text-2xl font-bold text-blue-600">{campaigns.filter(c => c.type === 'banner').length}</div>
            <div className="text-sm text-gray-600">Banners</div>
          </div>
        </div>

        {/* Create/Edit Form */}
        {showCreateForm && (
          <motion.div
            initial={{ opacity: 0, y: -20 }}
            animate={{ opacity: 1, y: 0 }}
            className="bg-white rounded-lg shadow-lg p-6"
          >
            <h2 className="text-xl font-bold mb-4">
              {editingCampaign ? 'Edit Campaign' : 'Create New Campaign'}
            </h2>

            <form onSubmit={handleSubmit} className="space-y-4">
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Campaign Type
                  </label>
                  <select
                    value={formData.type}
                    onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value as any }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                  >
                    <option value="banner">Banner</option>
                    <option value="toast">Toast</option>
                    <option value="modal">Modal</option>
                    <option value="exit-intent">Exit Intent</option>
                  </select>
                </div>

                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Priority
                  </label>
                  <select
                    value={formData.priority}
                    onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as any }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                  >
                    <option value="low">Low</option>
                    <option value="medium">Medium</option>
                    <option value="high">High</option>
                    <option value="urgent">Urgent</option>
                  </select>
                </div>
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Title
                </label>
                <input
                  type="text"
                  value={formData.title}
                  onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
                  className="w-full border border-gray-300 rounded-md px-3 py-2"
                  required
                />
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Message
                </label>
                <textarea
                  value={formData.message}
                  onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
                  className="w-full border border-gray-300 rounded-md px-3 py-2 h-24"
                  required
                />
              </div>

              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Action Text (Optional)
                  </label>
                  <input
                    type="text"
                    value={formData.actionText}
                    onChange={(e) => setFormData(prev => ({ ...prev, actionText: e.target.value }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                  />
                </div>

                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Action URL (Optional)
                  </label>
                  <input
                    type="text"
                    value={formData.actionUrl}
                    onChange={(e) => setFormData(prev => ({ ...prev, actionUrl: e.target.value }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                  />
                </div>
              </div>

              <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Language
                  </label>
                  <select
                    value={formData.language}
                    onChange={(e) => setFormData(prev => ({ ...prev, language: e.target.value as any }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                  >
                    <option value="both">Both Languages</option>
                    <option value="en">English Only</option>
                    <option value="fr">French Only</option>
                  </select>
                </div>

                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Max Views
                  </label>
                  <input
                    type="number"
                    value={formData.maxViews}
                    onChange={(e) => setFormData(prev => ({ ...prev, maxViews: parseInt(e.target.value) }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                    min="1"
                  />
                </div>

                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-1">
                    Expires At (Optional)
                  </label>
                  <input
                    type="datetime-local"
                    value={formData.expiresAt}
                    onChange={(e) => setFormData(prev => ({ ...prev, expiresAt: e.target.value }))}
                    className="w-full border border-gray-300 rounded-md px-3 py-2"
                  />
                </div>
              </div>

              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Target Pages (comma-separated, optional)
                </label>
                <input
                  type="text"
                  value={formData.targetPages}
                  onChange={(e) => setFormData(prev => ({ ...prev, targetPages: e.target.value }))}
                  className="w-full border border-gray-300 rounded-md px-3 py-2"
                  placeholder="/, /class-action, /faq"
                />
              </div>

              <div className="flex items-center">
                <input
                  type="checkbox"
                  id="isActive"
                  checked={formData.isActive}
                  onChange={(e) => setFormData(prev => ({ ...prev, isActive: e.target.checked }))}
                  className="mr-2"
                />
                <label htmlFor="isActive" className="text-sm font-medium text-gray-700">
                  Active
                </label>
              </div>

              <div className="flex space-x-4">
                <button
                  type="submit"
                  className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition-colors"
                >
                  {editingCampaign ? 'Update Campaign' : 'Create Campaign'}
                </button>
                <button
                  type="button"
                  onClick={resetForm}
                  className="bg-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-400 transition-colors"
                >
                  Cancel
                </button>
              </div>
            </form>
          </motion.div>
        )}

        {/* Campaigns List */}
        <div className="bg-white rounded-lg shadow overflow-hidden">
          <div className="px-6 py-4 border-b border-gray-200">
            <h3 className="text-lg font-medium text-gray-900">Active Campaigns</h3>
          </div>
          
          <div className="overflow-x-auto">
            <table className="min-w-full divide-y divide-gray-200">
              <thead className="bg-gray-50">
                <tr>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    Campaign
                  </th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    Type
                  </th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    Priority
                  </th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    Status
                  </th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    Expires
                  </th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    Actions
                  </th>
                </tr>
              </thead>
              <tbody className="bg-white divide-y divide-gray-200">
                {campaigns.map((campaign) => (
                  <tr key={campaign.id}>
                    <td className="px-6 py-4 whitespace-nowrap">
                      <div>
                        <div className="text-sm font-medium text-gray-900">{campaign.title}</div>
                        <div className="text-sm text-gray-500">{campaign.message.slice(0, 60)}...</div>
                      </div>
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap">
                      <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
                        {campaign.type}
                      </span>
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap">
                      <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPriorityColor(campaign.priority)}`}>
                        {campaign.priority}
                      </span>
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap">
                      <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
                        campaign.isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
                      }`}>
                        {campaign.isActive ? 'Active' : 'Inactive'}
                      </span>
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                      {campaign.expiresAt 
                        ? new Date(campaign.expiresAt).toLocaleDateString()
                        : 'Never'
                      }
                    </td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
                      <button
                        onClick={() => handleEdit(campaign)}
                        className="text-primary hover:text-primary-dark"
                      >
                        Edit
                      </button>
                      <button
                        onClick={() => handleToggleActive(campaign.id)}
                        className={campaign.isActive ? 'text-red-600 hover:text-red-900' : 'text-green-600 hover:text-green-900'}
                      >
                        {campaign.isActive ? 'Deactivate' : 'Activate'}
                      </button>
                      <button
                        onClick={() => handleDelete(campaign.id)}
                        className="text-red-600 hover:text-red-900"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>

          {campaigns.length === 0 && (
            <div className="text-center py-12">
              <div className="text-gray-500">No notification campaigns found</div>
              <button
                onClick={() => setShowCreateForm(true)}
                className="mt-4 bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition-colors"
              >
                Create Your First Campaign
              </button>
            </div>
          )}
        </div>
      </div>
    </LayoutWithSidebar>
  );
};

export default AdminNotifications; 

CasperSecurity Mini