← back to Norma

app/pulse/profile/page.tsx

376 lines

'use client';

import React, { useEffect, useState } from 'react';
import {
  User, Calendar, FileText, Bookmark, Save,
  Check, AlertCircle, Loader2,
} from 'lucide-react';

/* ────────────────────────── Types ────────────────────────── */

interface ProfileData {
  username: string;
  displayName: string;
  role: string;
  createdAt: string;
  signedCount: number;
  trackedCount: number;
}

/* ────────────────────────── Colors ────────────────────────── */

const C = {
  darkGreen: '#1B4332',
  gold: '#DAA520',
  text: '#1a1a1a',
  textMuted: '#6b7280',
  border: '#e5e7eb',
};

/* ────────────────────────── Helpers ────────────────────────── */

function formatDate(dateStr: string): string {
  return new Date(dateStr).toLocaleDateString('en-US', {
    year: 'numeric', month: 'long', day: 'numeric',
    timeZone: 'America/Los_Angeles',
  });
}

function roleLabel(role: string): string {
  switch (role) {
    case 'admin': return 'Administrator';
    case 'staff': return 'Staff Member';
    case 'pulse': return 'Pulse User';
    default: return role;
  }
}

function roleColor(role: string): { bg: string; text: string } {
  switch (role) {
    case 'admin': return { bg: 'rgba(99,102,241,0.1)', text: '#6366f1' };
    case 'staff': return { bg: 'rgba(14,165,233,0.1)', text: '#0ea5e9' };
    default: return { bg: 'rgba(218,165,32,0.1)', text: '#b8860b' };
  }
}

/* ────────────────────────── Page ────────────────────────── */

export default function PulseProfilePage() {
  const [profile, setProfile] = useState<ProfileData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  // Edit state
  const [editDisplayName, setEditDisplayName] = useState('');
  const [saving, setSaving] = useState(false);
  const [saveSuccess, setSaveSuccess] = useState(false);
  const [saveError, setSaveError] = useState('');

  useEffect(() => {
    async function loadProfile() {
      try {
        // Fetch session, stats in parallel
        const [sessionRes, activityRes, trackedRes] = await Promise.all([
          fetch('/api/auth/session', { credentials: 'include' }),
          fetch('/api/pulse/activity?page=1&limit=1', { credentials: 'include' }),
          fetch('/api/pulse/tracked?page=1&limit=1', { credentials: 'include' }),
        ]);

        if (!sessionRes.ok) {
          setError('Please sign in to view your profile.');
          return;
        }

        const sessionData = await sessionRes.json();
        if (!sessionData.authenticated) {
          setError('Please sign in to view your profile.');
          return;
        }

        const activityData = activityRes.ok ? await activityRes.json() : { total: 0 };
        const trackedData = trackedRes.ok ? await trackedRes.json() : { total: 0 };

        // Fetch full profile from a profile API (or build from session data)
        const profileRes = await fetch('/api/pulse/profile', { credentials: 'include' });
        let displayName = sessionData.displayName || sessionData.user;
        let createdAt = new Date().toISOString();

        if (profileRes.ok) {
          const profileData = await profileRes.json();
          displayName = profileData.display_name || displayName;
          createdAt = profileData.created_at || createdAt;
        }

        const p: ProfileData = {
          username: sessionData.user,
          displayName,
          role: sessionData.role,
          createdAt,
          signedCount: activityData.total || 0,
          trackedCount: trackedData.total || 0,
        };

        setProfile(p);
        setEditDisplayName(p.displayName);
      } catch {
        setError('Failed to load profile. Please try again.');
      } finally {
        setLoading(false);
      }
    }
    loadProfile();
  }, []);

  async function handleSaveDisplayName() {
    if (!editDisplayName.trim() || editDisplayName.trim() === profile?.displayName) return;
    setSaving(true);
    setSaveError('');
    setSaveSuccess(false);

    try {
      const res = await fetch('/api/pulse/profile', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ display_name: editDisplayName.trim() }),
      });

      if (!res.ok) {
        const data = await res.json();
        throw new Error(data.error || 'Failed to save');
      }

      setProfile(prev => prev ? { ...prev, displayName: editDisplayName.trim() } : prev);
      setSaveSuccess(true);
      setTimeout(() => setSaveSuccess(false), 3000);
    } catch (err) {
      setSaveError((err as Error).message);
    } finally {
      setSaving(false);
    }
  }

  if (loading) {
    return (
      <div style={{ maxWidth: 700, margin: '0 auto', padding: '40px 24px 80px' }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
          {[1, 2, 3].map(i => (
            <div key={i} style={{
              backgroundColor: '#fff', borderRadius: 16, padding: 32,
              border: '1px solid ' + C.border,
            }}>
              <div style={{ height: 20, backgroundColor: '#e5e7eb', borderRadius: 7, width: '40%', marginBottom: 16 }} />
              <div style={{ height: 14, backgroundColor: '#e5e7eb', borderRadius: 7, width: '70%' }} />
            </div>
          ))}
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div style={{ maxWidth: 700, margin: '0 auto', padding: '40px 24px 80px' }}>
        <div style={{
          backgroundColor: '#fef2f2', border: '1px solid #fecaca', borderRadius: 12,
          padding: 24, textAlign: 'center', color: '#991b1b',
          fontFamily: 'system-ui, sans-serif',
        }}>
          {error}
        </div>
      </div>
    );
  }

  if (!profile) return null;

  const rc = roleColor(profile.role);

  return (
    <div style={{ maxWidth: 700, margin: '0 auto', padding: '40px 24px 80px' }}>
      {/* Header */}
      <div style={{ marginBottom: 32 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
          <div style={{
            width: 44, height: 44, borderRadius: 12,
            backgroundColor: 'rgba(27,67,50,0.08)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <User size={22} style={{ color: C.darkGreen }} />
          </div>
          <h1 style={{ fontSize: 28, fontWeight: 'bold', color: C.text, margin: 0, fontFamily: 'Georgia, serif' }}>
            My Profile
          </h1>
        </div>
      </div>

      {/* Profile Card */}
      <div style={{
        backgroundColor: '#fff', borderRadius: 16, overflow: 'hidden',
        border: '1px solid ' + C.border, marginBottom: 24,
      }}>
        {/* Profile header with avatar */}
        <div style={{
          backgroundColor: C.darkGreen, padding: '32px 32px 40px',
          display: 'flex', alignItems: 'center', gap: 20,
        }}>
          <div style={{
            width: 72, height: 72, borderRadius: '50%',
            backgroundColor: 'rgba(218,165,32,0.2)', border: '3px solid ' + C.gold,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontFamily: 'Georgia, serif', fontSize: 28, fontWeight: 'bold',
            color: C.gold,
          }}>
            {(profile.displayName || profile.username).charAt(0).toUpperCase()}
          </div>
          <div>
            <h2 style={{ fontSize: 24, fontWeight: 'bold', color: '#fff', margin: 0, fontFamily: 'Georgia, serif' }}>
              {profile.displayName}
            </h2>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 14, color: 'rgba(255,255,255,0.7)', fontFamily: 'system-ui, sans-serif' }}>
                @{profile.username}
              </span>
              <span style={{
                display: 'inline-block', fontSize: 12, fontWeight: 600,
                padding: '3px 12px', borderRadius: 9999,
                backgroundColor: 'rgba(218,165,32,0.2)', color: C.gold,
                fontFamily: 'system-ui, sans-serif',
              }}>
                {roleLabel(profile.role)}
              </span>
            </div>
          </div>
        </div>

        {/* Stats row */}
        <div style={{ display: 'flex', borderBottom: '1px solid ' + C.border }}>
          <div style={{ flex: 1, padding: '20px 32px', textAlign: 'center', borderRight: '1px solid ' + C.border }}>
            <div style={{ fontSize: 28, fontWeight: 'bold', color: C.text, fontFamily: 'Georgia, serif' }}>
              {profile.signedCount}
            </div>
            <div style={{ fontSize: 13, color: C.textMuted, fontFamily: 'system-ui, sans-serif', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, marginTop: 4 }}>
              <FileText size={14} />
              Petitions Signed
            </div>
          </div>
          <div style={{ flex: 1, padding: '20px 32px', textAlign: 'center', borderRight: '1px solid ' + C.border }}>
            <div style={{ fontSize: 28, fontWeight: 'bold', color: C.text, fontFamily: 'Georgia, serif' }}>
              {profile.trackedCount}
            </div>
            <div style={{ fontSize: 13, color: C.textMuted, fontFamily: 'system-ui, sans-serif', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, marginTop: 4 }}>
              <Bookmark size={14} />
              Petitions Tracked
            </div>
          </div>
          <div style={{ flex: 1, padding: '20px 32px', textAlign: 'center' }}>
            <div style={{ fontSize: 14, fontWeight: 600, color: C.text, fontFamily: 'system-ui, sans-serif' }}>
              {formatDate(profile.createdAt)}
            </div>
            <div style={{ fontSize: 13, color: C.textMuted, fontFamily: 'system-ui, sans-serif', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, marginTop: 4 }}>
              <Calendar size={14} />
              Member Since
            </div>
          </div>
        </div>

        {/* Edit Display Name */}
        <div style={{ padding: 32 }}>
          <h3 style={{ fontSize: 17, fontWeight: 600, color: C.text, margin: '0 0 16px', fontFamily: 'Georgia, serif' }}>
            Edit Display Name
          </h3>
          <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
            <div style={{ flex: 1 }}>
              <input
                type="text"
                value={editDisplayName}
                onChange={(e) => setEditDisplayName(e.target.value)}
                maxLength={50}
                placeholder="Your display name"
                style={{
                  width: '100%', padding: '10px 14px', borderRadius: 8, fontSize: 15,
                  border: '1px solid ' + C.border, backgroundColor: '#f9fafb',
                  color: C.text, fontFamily: 'system-ui, sans-serif',
                  outline: 'none', transition: 'border-color 150ms',
                  boxSizing: 'border-box',
                }}
                onFocus={(e) => (e.currentTarget.style.borderColor = C.darkGreen)}
                onBlur={(e) => (e.currentTarget.style.borderColor = C.border)}
              />
              <p style={{ fontSize: 12, color: C.textMuted, margin: '6px 0 0', fontFamily: 'system-ui, sans-serif' }}>
                This name will be displayed publicly on your activity.
              </p>
            </div>
            <button
              onClick={handleSaveDisplayName}
              disabled={saving || !editDisplayName.trim() || editDisplayName.trim() === profile.displayName}
              style={{
                display: 'inline-flex', alignItems: 'center', gap: 6,
                padding: '10px 20px', borderRadius: 8, fontSize: 14, fontWeight: 600,
                backgroundColor: C.darkGreen, color: '#fff', border: 'none',
                cursor: (saving || !editDisplayName.trim() || editDisplayName.trim() === profile.displayName) ? 'default' : 'pointer',
                fontFamily: 'system-ui, sans-serif', transition: 'all 150ms',
                opacity: (saving || !editDisplayName.trim() || editDisplayName.trim() === profile.displayName) ? 0.5 : 1,
              }}
            >
              {saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
              {saving ? 'Saving...' : 'Save'}
            </button>
          </div>

          {/* Save feedback */}
          {saveSuccess && (
            <div style={{
              display: 'flex', alignItems: 'center', gap: 6, marginTop: 12,
              fontSize: 14, color: '#16a34a', fontFamily: 'system-ui, sans-serif',
            }}>
              <Check size={16} />
              Display name updated successfully.
            </div>
          )}
          {saveError && (
            <div style={{
              display: 'flex', alignItems: 'center', gap: 6, marginTop: 12,
              fontSize: 14, color: '#dc2626', fontFamily: 'system-ui, sans-serif',
            }}>
              <AlertCircle size={16} />
              {saveError}
            </div>
          )}
        </div>
      </div>

      {/* Account Info */}
      <div style={{
        backgroundColor: '#fff', borderRadius: 16, padding: 32,
        border: '1px solid ' + C.border,
      }}>
        <h3 style={{ fontSize: 17, fontWeight: 600, color: C.text, margin: '0 0 20px', fontFamily: 'Georgia, serif' }}>
          Account Details
        </h3>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', borderBottom: '1px solid #f3f4f6' }}>
            <span style={{ fontSize: 14, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>Username</span>
            <span style={{ fontSize: 14, fontWeight: 600, color: C.text, fontFamily: 'system-ui, monospace' }}>@{profile.username}</span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', borderBottom: '1px solid #f3f4f6' }}>
            <span style={{ fontSize: 14, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>Role</span>
            <span style={{
              display: 'inline-block', fontSize: 13, fontWeight: 600,
              padding: '4px 12px', borderRadius: 9999,
              backgroundColor: rc.bg, color: rc.text,
              fontFamily: 'system-ui, sans-serif',
            }}>
              {roleLabel(profile.role)}
            </span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
            <span style={{ fontSize: 14, color: C.textMuted, fontFamily: 'system-ui, sans-serif' }}>Joined</span>
            <span style={{ fontSize: 14, fontWeight: 500, color: C.text, fontFamily: 'system-ui, sans-serif' }}>{formatDate(profile.createdAt)}</span>
          </div>
        </div>
      </div>
    </div>
  );
}