From c25de2a19116c396cc449381f752f450190746e0 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 9 Sep 2025 16:29:24 -0500 Subject: [PATCH] Add AccountInfoModal component and integrate account information display in M3UProfiles --- .../src/components/forms/AccountInfoModal.jsx | 345 ++++++++++++++++++ frontend/src/components/forms/M3UProfiles.jsx | 222 ++++++++--- 2 files changed, 517 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/forms/AccountInfoModal.jsx 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" + > + + + + )} + + + + + ); + })}