diff --git a/frontend/src/components/forms/AccountInfoModal.jsx b/frontend/src/components/forms/AccountInfoModal.jsx
new file mode 100644
index 00000000..4c0fba39
--- /dev/null
+++ b/frontend/src/components/forms/AccountInfoModal.jsx
@@ -0,0 +1,345 @@
+import React from 'react';
+import {
+ Modal,
+ Text,
+ Box,
+ Group,
+ Badge,
+ Table,
+ Stack,
+ Divider,
+ Alert,
+ Loader,
+ Center,
+} from '@mantine/core';
+import {
+ Info,
+ Clock,
+ Users,
+ CheckCircle,
+ XCircle,
+ AlertTriangle,
+} from 'lucide-react';
+
+const AccountInfoModal = ({ isOpen, onClose, profile }) => {
+ if (!profile || !profile.custom_properties) {
+ return (
+
+
+
+
+ No account information available
+
+ Account information will be available after the next refresh for
+ XtreamCodes accounts.
+
+
+
+
+ );
+ }
+
+ const { user_info, server_info, last_refresh } = profile.custom_properties;
+
+ // Helper function to format timestamps
+ const formatTimestamp = (timestamp) => {
+ if (!timestamp) return 'Unknown';
+ try {
+ const date =
+ typeof timestamp === 'string' && timestamp.includes('T')
+ ? new Date(timestamp)
+ : new Date(parseInt(timestamp) * 1000);
+ return date.toLocaleString();
+ } catch {
+ return 'Invalid date';
+ }
+ };
+
+ // Helper function to get time remaining
+ const getTimeRemaining = (expTimestamp) => {
+ if (!expTimestamp) return null;
+ try {
+ const expDate = new Date(parseInt(expTimestamp) * 1000);
+ const now = new Date();
+ const diffMs = expDate - now;
+
+ if (diffMs <= 0) return 'Expired';
+
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+ const hours = Math.floor(
+ (diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
+ );
+
+ if (days > 0) {
+ return `${days} day${days !== 1 ? 's' : ''} ${hours} hour${hours !== 1 ? 's' : ''}`;
+ } else {
+ return `${hours} hour${hours !== 1 ? 's' : ''}`;
+ }
+ } catch {
+ return 'Unknown';
+ }
+ };
+
+ // Helper function to get status badge
+ const getStatusBadge = (status) => {
+ const statusConfig = {
+ Active: { color: 'green', icon: CheckCircle },
+ Expired: { color: 'red', icon: XCircle },
+ Disabled: { color: 'red', icon: XCircle },
+ Banned: { color: 'red', icon: XCircle },
+ };
+
+ const config = statusConfig[status] || {
+ color: 'gray',
+ icon: AlertTriangle,
+ };
+ const Icon = config.icon;
+
+ return (
+ }
+ >
+ {status || 'Unknown'}
+
+ );
+ };
+
+ const timeRemaining = user_info?.exp_date
+ ? getTimeRemaining(user_info.exp_date)
+ : null;
+ const isExpired = timeRemaining === 'Expired';
+
+ return (
+
+
+
+ Account Information - {profile.name}
+
+
+ }
+ size="lg"
+ >
+
+ {/* Account Status Overview */}
+
+
+
+ Account Status
+
+ {getStatusBadge(user_info?.status)}
+
+
+ {isExpired && (
+ }
+ color="red"
+ variant="light"
+ mb="md"
+ >
+ This account has expired!
+
+ )}
+
+
+
+
+ {/* Key Information Cards */}
+
+
+
+
+ Expires
+
+
+ {user_info?.exp_date
+ ? formatTimestamp(user_info.exp_date)
+ : 'Unknown'}
+
+ {timeRemaining && (
+
+ {timeRemaining === 'Expired'
+ ? 'Expired'
+ : `${timeRemaining} remaining`}
+
+ )}
+
+
+
+
+
+ Connections
+
+
+ {user_info?.active_cons || '0'} /{' '}
+ {user_info?.max_connections || 'Unknown'}
+
+
+ Active / Max
+
+
+
+
+
+
+ {/* Detailed Information Table */}
+
+
+ Account Details
+
+
+
+
+
+ Username
+
+ {user_info?.username || 'Unknown'}
+
+
+ Account Created
+
+ {user_info?.created_at
+ ? formatTimestamp(user_info.created_at)
+ : 'Unknown'}
+
+
+
+ Trial Account
+
+
+ {user_info?.is_trial === '1' ? 'Yes' : 'No'}
+
+
+
+ {user_info?.allowed_output_formats &&
+ user_info.allowed_output_formats.length > 0 && (
+
+ Allowed Formats
+
+
+ {user_info.allowed_output_formats.map(
+ (format, index) => (
+
+ {format.toUpperCase()}
+
+ )
+ )}
+
+
+
+ )}
+
+
+
+
+ {/* Server Information */}
+ {server_info && Object.keys(server_info).length > 0 && (
+ <>
+
+
+
+ Server Information
+
+
+
+ {server_info.url && (
+
+
+ Server URL
+
+
+
+ {server_info.url}
+
+
+
+ )}
+ {server_info.port && (
+
+ Port
+
+
+ {server_info.port}
+
+
+
+ )}
+ {server_info.https_port && (
+
+ HTTPS Port
+
+
+ {server_info.https_port}
+
+
+
+ )}
+ {server_info.timezone && (
+
+ Timezone
+ {server_info.timezone}
+
+ )}
+
+
+
+ >
+ )}
+
+ {/* Last Refresh Info */}
+
+
+
+
+ Last Updated:
+
+
+ {last_refresh ? formatTimestamp(last_refresh) : 'Never'}
+
+
+
+
+
+ );
+};
+
+export default AccountInfoModal;
diff --git a/frontend/src/components/forms/M3UProfiles.jsx b/frontend/src/components/forms/M3UProfiles.jsx
index 455be473..0371b9cc 100644
--- a/frontend/src/components/forms/M3UProfiles.jsx
+++ b/frontend/src/components/forms/M3UProfiles.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import API from '../../api';
import M3UProfile from './M3UProfile';
+import AccountInfoModal from './AccountInfoModal';
import usePlaylistsStore from '../../store/playlists';
import ConfirmationDialog from '../ConfirmationDialog';
import useWarningsStore from '../../store/warnings';
@@ -18,8 +19,10 @@ import {
Center,
Group,
Switch,
+ Badge,
+ Stack,
} from '@mantine/core';
-import { SquareMinus, SquarePen } from 'lucide-react';
+import { SquareMinus, SquarePen, Info } from 'lucide-react';
const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
const theme = useMantineTheme();
@@ -34,6 +37,8 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
const [profileToDelete, setProfileToDelete] = useState(null);
+ const [accountInfoOpen, setAccountInfoOpen] = useState(false);
+ const [selectedProfileForInfo, setSelectedProfileForInfo] = useState(null);
useEffect(() => {
try {
@@ -113,6 +118,59 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
setProfileEditorOpen(false);
};
+ const showAccountInfo = (profile) => {
+ setSelectedProfileForInfo(profile);
+ setAccountInfoOpen(true);
+ };
+
+ const closeAccountInfo = () => {
+ setSelectedProfileForInfo(null);
+ setAccountInfoOpen(false);
+ };
+
+ // Helper function to get account status from profile
+ const getAccountStatus = (profile) => {
+ if (!profile.custom_properties?.user_info) return null;
+ return profile.custom_properties.user_info.status;
+ };
+
+ // Helper function to check if account is expired
+ const isAccountExpired = (profile) => {
+ if (!profile.custom_properties?.user_info?.exp_date) return false;
+ try {
+ const expDate = new Date(
+ parseInt(profile.custom_properties.user_info.exp_date) * 1000
+ );
+ return expDate < new Date();
+ } catch {
+ return false;
+ }
+ };
+
+ // Helper function to get account expiration info
+ const getExpirationInfo = (profile) => {
+ if (!profile.custom_properties?.user_info?.exp_date) return null;
+ try {
+ const expDate = new Date(
+ parseInt(profile.custom_properties.user_info.exp_date) * 1000
+ );
+ const now = new Date();
+ const diffMs = expDate - now;
+
+ if (diffMs <= 0) return { text: 'Expired', color: 'red' };
+
+ const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+ if (days > 30) return { text: `${days} days`, color: 'green' };
+ if (days > 7) return { text: `${days} days`, color: 'yellow' };
+ if (days > 0) return { text: `${days} days`, color: 'orange' };
+
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
+ return { text: `${hours}h`, color: 'red' };
+ } catch {
+ return null;
+ }
+ };
+
// Don't render if modal is not open, or if playlist data is invalid
if (!isOpen || !playlist || !playlist.id) {
return <>>;
@@ -132,57 +190,116 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
// Sort remaining profiles alphabetically by name
return a.name.localeCompare(b.name);
})
- .map((item) => (
-
-
-
- {item.name}
- toggleActive(item)}
- disabled={item.is_default}
- style={{ paddingTop: 6 }}
- />
-
+ .map((item) => {
+ const accountStatus = getAccountStatus(item);
+ const expirationInfo = getExpirationInfo(item);
+ const expired = isAccountExpired(item);
-
- modifyMaxStreams(value, item)}
- style={{ flex: 1 }}
- />
-
- {!item.is_default && (
-
- editProfile(item)}
- >
-
-
-
- deleteProfile(item.id)}
- size="small"
- variant="transparent"
- >
-
-
+ return (
+
+
+ {/* Header with name and status badges */}
+
+
+ {item.name}
+ {playlist?.account_type === 'XC' &&
+ item.custom_properties && (
+
+ {/* Account status badge */}
+ {accountStatus && (
+
+ {accountStatus}
+
+ )}
+ {/* Expiration badge */}
+ {expirationInfo && (
+
+ {expirationInfo.text}
+
+ )}
+ {/* Info button next to badges */}
+ showAccountInfo(item)}
+ title="View account information"
+ style={{
+ backgroundColor: 'rgba(34, 139, 230, 0.1)',
+ color: '#228be6',
+ }}
+ >
+
+
+
+ )}
- )}
-
-
-
- ))}
+
+
+ {/* Max Streams and Actions */}
+
+ modifyMaxStreams(value, item)}
+ style={{ flex: 1 }}
+ />
+
+
+ {/* Toggle switch */}
+ toggleActive(item)}
+ disabled={item.is_default}
+ label="Active"
+ labelPosition="left"
+ size="sm"
+ />
+
+ {!item.is_default && (
+ <>
+ editProfile(item)}
+ title="Edit profile"
+ >
+
+
+
+ deleteProfile(item.id)}
+ size="small"
+ variant="transparent"
+ title="Delete profile"
+ >
+
+
+ >
+ )}
+
+
+
+
+ );
+ })}
+
>
);
};