Created toggle for enable / disable user signup flow with user role

Fixed numbers mismatching in host cards
Fixed issues with the settings file
Fixed layouts on hosts/packages/repos
Added ability to delete multiple hosts at once
Fixed Dark mode styling in areas
Removed console debugging messages
Done some other stuff ...
This commit is contained in:
Muhammad Ibrahim
2025-09-22 01:01:50 +01:00
parent a268f6b8f1
commit 797be20c45
29 changed files with 940 additions and 4015 deletions

View File

@@ -28,9 +28,11 @@ import {
Settings as SettingsIcon
} from 'lucide-react';
import { dashboardPreferencesAPI } from '../utils/api';
import { useTheme } from '../contexts/ThemeContext';
// Sortable Card Item Component
const SortableCardItem = ({ card, onToggle }) => {
const { isDark } = useTheme();
const {
attributes,
listeners,
@@ -50,7 +52,7 @@ const SortableCardItem = ({ card, onToggle }) => {
<div
ref={setNodeRef}
style={style}
className={`flex items-center justify-between p-3 bg-white border border-secondary-200 rounded-lg ${
className={`flex items-center justify-between p-3 bg-white dark:bg-secondary-800 border border-secondary-200 dark:border-secondary-600 rounded-lg ${
isDragging ? 'shadow-lg' : 'shadow-sm'
}`}
>
@@ -58,12 +60,12 @@ const SortableCardItem = ({ card, onToggle }) => {
<button
{...attributes}
{...listeners}
className="text-secondary-400 hover:text-secondary-600 cursor-grab active:cursor-grabbing"
className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300 cursor-grab active:cursor-grabbing"
>
<GripVertical className="h-4 w-4" />
</button>
<div className="flex items-center gap-2">
<div className="text-sm font-medium text-secondary-900">
<div className="text-sm font-medium text-secondary-900 dark:text-white">
{card.title}
</div>
</div>
@@ -73,8 +75,8 @@ const SortableCardItem = ({ card, onToggle }) => {
onClick={() => onToggle(card.cardId)}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors ${
card.enabled
? 'bg-green-100 text-green-800 hover:bg-green-200'
: 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200'
? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800'
: 'bg-secondary-100 dark:bg-secondary-700 text-secondary-600 dark:text-secondary-300 hover:bg-secondary-200 dark:hover:bg-secondary-600'
}`}
>
{card.enabled ? (
@@ -97,6 +99,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
const [cards, setCards] = useState([]);
const [hasChanges, setHasChanges] = useState(false);
const queryClient = useQueryClient();
const { isDark } = useTheme();
const sensors = useSensors(
useSensor(PointerSensor),
@@ -212,24 +215,24 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-secondary-500 bg-opacity-75 transition-opacity" onClick={onClose} />
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="inline-block align-bottom bg-white dark:bg-secondary-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white dark:bg-secondary-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<SettingsIcon className="h-5 w-5 text-primary-600" />
<h3 className="text-lg font-medium text-secondary-900">
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
Dashboard Settings
</h3>
</div>
<button
onClick={onClose}
className="text-secondary-400 hover:text-secondary-600"
className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
>
<X className="h-5 w-5" />
</button>
</div>
<p className="text-sm text-secondary-600 mb-6">
<p className="text-sm text-secondary-600 dark:text-secondary-400 mb-6">
Customize your dashboard by reordering cards and toggling their visibility.
Drag cards to reorder them, and click the visibility toggle to show/hide cards.
</p>
@@ -259,7 +262,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
)}
</div>
<div className="bg-secondary-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<div className="bg-secondary-50 dark:bg-secondary-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
onClick={handleSave}
disabled={!hasChanges || updatePreferencesMutation.isPending}
@@ -284,7 +287,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
<button
onClick={handleReset}
className="mt-3 w-full inline-flex justify-center rounded-md border border-secondary-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-secondary-700 hover:bg-secondary-50 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
className="mt-3 w-full inline-flex justify-center rounded-md border border-secondary-300 dark:border-secondary-600 shadow-sm px-4 py-2 bg-white dark:bg-secondary-800 text-base font-medium text-secondary-700 dark:text-secondary-200 hover:bg-secondary-50 dark:hover:bg-secondary-700 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
<RotateCcw className="h-4 w-4 mr-2" />
Reset to Defaults
@@ -292,7 +295,7 @@ const DashboardSettingsModal = ({ isOpen, onClose }) => {
<button
onClick={onClose}
className="mt-3 w-full inline-flex justify-center rounded-md border border-secondary-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-secondary-700 hover:bg-secondary-50 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
className="mt-3 w-full inline-flex justify-center rounded-md border border-secondary-300 dark:border-secondary-600 shadow-sm px-4 py-2 bg-white dark:bg-secondary-800 text-base font-medium text-secondary-700 dark:text-secondary-200 hover:bg-secondary-50 dark:hover:bg-secondary-700 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>

View File

@@ -30,15 +30,19 @@ const FirstTimeAdminSetup = () => {
return false
}
if (!formData.email.trim()) {
setError('Email is required')
setError('Email address is required')
return false
}
if (!formData.email.includes('@')) {
setError('Please enter a valid email address')
// Enhanced email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(formData.email.trim())) {
setError('Please enter a valid email address (e.g., user@example.com)')
return false
}
if (formData.password.length < 6) {
setError('Password must be at least 6 characters')
if (formData.password.length < 8) {
setError('Password must be at least 8 characters for security')
return false
}
if (formData.password !== formData.confirmPassword) {
@@ -186,7 +190,7 @@ const FirstTimeAdminSetup = () => {
value={formData.password}
onChange={handleInputChange}
className="input w-full"
placeholder="Enter your password (min 6 characters)"
placeholder="Enter your password (min 8 characters)"
required
disabled={isLoading}
/>

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Edit2, Check, X, ChevronDown } from 'lucide-react';
const InlineGroupEdit = ({
@@ -88,11 +88,8 @@ const InlineGroupEdit = ({
const handleSave = async () => {
if (disabled || isLoading) return;
console.log('handleSave called:', { selectedValue, originalValue: value, changed: selectedValue !== value });
// Check if value actually changed
if (selectedValue === value) {
console.log('No change detected, closing edit mode');
setIsEditing(false);
setIsOpen(false);
return;
@@ -102,15 +99,12 @@ const InlineGroupEdit = ({
setError('');
try {
console.log('Calling onSave with:', selectedValue);
await onSave(selectedValue);
console.log('Save successful');
// Update the local value to match the saved value
setSelectedValue(selectedValue);
setIsEditing(false);
setIsOpen(false);
} catch (err) {
console.error('Save failed:', err);
setError(err.message || 'Failed to save');
} finally {
setIsLoading(false);
@@ -127,22 +121,23 @@ const InlineGroupEdit = ({
}
};
const getDisplayValue = () => {
console.log('getDisplayValue called with:', { value, options });
const displayValue = useMemo(() => {
if (!value) {
console.log('No value, returning Ungrouped');
return 'Ungrouped';
}
const option = options.find(opt => opt.id === value);
console.log('Found option:', option);
return option ? option.name : 'Unknown Group';
};
}, [value, options]);
const getDisplayColor = () => {
const displayColor = useMemo(() => {
if (!value) return 'bg-secondary-100 text-secondary-800';
const option = options.find(opt => opt.id === value);
return option ? `text-white` : 'bg-secondary-100 text-secondary-800';
};
}, [value, options]);
const selectedOption = useMemo(() => {
return options.find(opt => opt.id === value);
}, [value, options]);
if (isEditing) {
return (
@@ -241,10 +236,10 @@ const InlineGroupEdit = ({
return (
<div className={`flex items-center gap-2 group ${className}`}>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getDisplayColor()}`}
style={value ? { backgroundColor: options.find(opt => opt.id === value)?.color } : {}}
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${displayColor}`}
style={value ? { backgroundColor: selectedOption?.color } : {}}
>
{getDisplayValue()}
{displayValue}
</span>
{!disabled && (
<button

View File

@@ -23,7 +23,12 @@ import {
Plus,
Activity,
Cog,
FileText
FileText,
Github,
MessageCircle,
Mail,
Star,
Globe
} from 'lucide-react'
import { useState, useEffect, useRef } from 'react'
import { useQuery } from '@tanstack/react-query'
@@ -40,6 +45,7 @@ const Layout = ({ children }) => {
return saved ? JSON.parse(saved) : false
})
const [userMenuOpen, setUserMenuOpen] = useState(false)
const [githubStars, setGithubStars] = useState(null)
const location = useLocation()
const { user, logout, canViewHosts, canManageHosts, canViewPackages, canViewUsers, canManageUsers, canManageSettings } = useAuth()
const { updateAvailable } = useUpdateNotification()
@@ -133,10 +139,56 @@ const Layout = ({ children }) => {
window.location.href = '/hosts?action=add'
}
const copyEmailToClipboard = async () => {
const email = 'support@patchmon.net'
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(email)
} else {
// Fallback for non-secure contexts
const textArea = document.createElement('textarea')
textArea.value = email
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
document.execCommand('copy')
textArea.remove()
}
// You could add a toast notification here if you have one
} catch (err) {
console.error('Failed to copy email:', err)
// Fallback: show email in prompt
prompt('Copy this email address:', email)
}
}
// Fetch GitHub stars count
const fetchGitHubStars = async () => {
try {
const response = await fetch('https://api.github.com/repos/9technologygroup/patchmon.net')
if (response.ok) {
const data = await response.json()
setGithubStars(data.stargazers_count)
}
} catch (error) {
console.error('Failed to fetch GitHub stars:', error)
}
}
// Short format for navigation area
const formatRelativeTimeShort = (date) => {
if (!date) return 'Never'
const now = new Date()
const diff = now - new Date(date)
const dateObj = new Date(date)
// Check if date is valid
if (isNaN(dateObj.getTime())) return 'Invalid date'
const diff = now - dateObj
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
@@ -167,6 +219,11 @@ const Layout = ({ children }) => {
}
}, [])
// Fetch GitHub stars on component mount
useEffect(() => {
fetchGitHubStars()
}, [])
return (
<div className="min-h-screen bg-secondary-50">
{/* Mobile sidebar */}
@@ -425,6 +482,7 @@ const Layout = ({ children }) => {
</ul>
</nav>
{/* Profile Section - Bottom of Sidebar */}
<div className="border-t border-secondary-200 dark:border-secondary-600">
{!sidebarCollapsed ? (
@@ -560,20 +618,54 @@ const Layout = ({ children }) => {
</h2>
</div>
<div className="flex items-center gap-x-4 lg:gap-x-6">
{/* Customize Dashboard Button - Only show on Dashboard page */}
{location.pathname === '/' && (
<button
onClick={() => {
// This will be handled by the Dashboard component
const event = new CustomEvent('openDashboardSettings');
window.dispatchEvent(event);
}}
className="btn-outline flex items-center gap-2"
{/* External Links */}
<div className="flex items-center gap-2">
<a
href="https://github.com/9technologygroup/patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1.5 px-3 py-2 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm group relative"
title="⭐ Star us on GitHub! Click to open repository"
>
<Settings className="h-4 w-4" />
Customize Dashboard
<Github className="h-5 w-5 flex-shrink-0" />
{githubStars !== null && (
<div className="flex items-center gap-0.5">
<Star className="h-3 w-3 fill-current text-yellow-500" />
<span className="text-sm font-medium">{githubStars}</span>
</div>
)}
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-sm rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-50">
Star us on GitHub!
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-100"></div>
</div>
</a>
<a
href="https://discord.gg/DDKQeW6mnq"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm"
title="Discord"
>
<MessageCircle className="h-5 w-5" />
</a>
<button
onClick={copyEmailToClipboard}
className="flex items-center justify-center w-10 h-10 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm"
title="Copy support@patchmon.net"
>
<Mail className="h-5 w-5" />
</button>
)}
<a
href="https://patchmon.net"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-10 h-10 bg-gray-50 dark:bg-gray-800 text-secondary-600 dark:text-secondary-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm"
title="Visit patchmon.net"
>
<Globe className="h-5 w-5" />
</a>
</div>
</div>
</div>
</div>