joklihuhiuyuiyhuiyyiuyiuj
This commit is contained in:
parent
e551b48c8b
commit
156125e1db
508
api_service/dashboard_web/css/components.css
Normal file
508
api_service/dashboard_web/css/components.css
Normal file
@ -0,0 +1,508 @@
|
||||
/* Components CSS file for the Discord Bot Dashboard */
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading-spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--spacing-6);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(88, 101, 242, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary-color);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-spinner-sm {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.loading-spinner-lg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-width: 6px;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Button with loading state */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: calc(50% - 8px);
|
||||
left: calc(50% - 8px);
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--card-bg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
min-width: 300px;
|
||||
max-width: 450px;
|
||||
animation: slideIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.toast.toast-success {
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
.toast.toast-error {
|
||||
border-left: 4px solid var(--danger-color);
|
||||
}
|
||||
|
||||
.toast.toast-warning {
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.toast.toast-info {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.25rem;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast.toast-hiding {
|
||||
animation: slideOut 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25em 0.6em;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: var(--warning-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-light {
|
||||
background-color: #e2e8f0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
animation: scaleIn 0.3s;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: var(--spacing-4) var(--spacing-6);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--spacing-6);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: var(--spacing-4) var(--spacing-6);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dropdowns */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
min-width: 180px;
|
||||
padding: var(--spacing-2) 0;
|
||||
margin: var(--spacing-1) 0 0;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
animation: fadeIn 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: var(--text-primary);
|
||||
text-align: inherit;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 0;
|
||||
margin: var(--spacing-2) 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
position: relative;
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-4);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #0f5132;
|
||||
background-color: #d1e7dd;
|
||||
border-color: #badbcc;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #842029;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c2c7;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #664d03;
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffecb5;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #055160;
|
||||
background-color: #cff4fc;
|
||||
border-color: #b6effb;
|
||||
}
|
||||
|
||||
.alert-dismissible {
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.alert-dismissible .close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: var(--spacing-4);
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress {
|
||||
display: flex;
|
||||
height: 0.75rem;
|
||||
overflow: hidden;
|
||||
font-size: 0.75rem;
|
||||
background-color: #e9ecef;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background-color: var(--primary-color);
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-text {
|
||||
visibility: hidden;
|
||||
width: 120px;
|
||||
background-color: var(--secondary-color);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-2);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
margin-left: -60px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-text::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: var(--secondary-color) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
290
api_service/dashboard_web/css/layout.css
Normal file
290
api_service/dashboard_web/css/layout.css
Normal file
@ -0,0 +1,290 @@
|
||||
/* Layout CSS file for the Discord Bot Dashboard */
|
||||
|
||||
/* Grid System */
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.col {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.col-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
|
||||
.col-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
|
||||
.col-3 { flex: 0 0 25%; max-width: 25%; }
|
||||
.col-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
|
||||
.col-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
|
||||
.col-6 { flex: 0 0 50%; max-width: 50%; }
|
||||
.col-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
|
||||
.col-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
|
||||
.col-9 { flex: 0 0 75%; max-width: 75%; }
|
||||
.col-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
|
||||
.col-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
|
||||
.col-12 { flex: 0 0 100%; max-width: 100%; }
|
||||
|
||||
/* Responsive Breakpoints */
|
||||
@media (max-width: 576px) {
|
||||
.col-sm-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
|
||||
.col-sm-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
|
||||
.col-sm-3 { flex: 0 0 25%; max-width: 25%; }
|
||||
.col-sm-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
|
||||
.col-sm-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
|
||||
.col-sm-6 { flex: 0 0 50%; max-width: 50%; }
|
||||
.col-sm-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
|
||||
.col-sm-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
|
||||
.col-sm-9 { flex: 0 0 75%; max-width: 75%; }
|
||||
.col-sm-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
|
||||
.col-sm-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
|
||||
.col-sm-12 { flex: 0 0 100%; max-width: 100%; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-md-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
|
||||
.col-md-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
|
||||
.col-md-3 { flex: 0 0 25%; max-width: 25%; }
|
||||
.col-md-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
|
||||
.col-md-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
|
||||
.col-md-6 { flex: 0 0 50%; max-width: 50%; }
|
||||
.col-md-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
|
||||
.col-md-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
|
||||
.col-md-9 { flex: 0 0 75%; max-width: 75%; }
|
||||
.col-md-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
|
||||
.col-md-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
|
||||
.col-md-12 { flex: 0 0 100%; max-width: 100%; }
|
||||
|
||||
/* Mobile sidebar adjustments */
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
left: 0;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.col-lg-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
|
||||
.col-lg-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
|
||||
.col-lg-3 { flex: 0 0 25%; max-width: 25%; }
|
||||
.col-lg-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
|
||||
.col-lg-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
|
||||
.col-lg-6 { flex: 0 0 50%; max-width: 50%; }
|
||||
.col-lg-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
|
||||
.col-lg-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
|
||||
.col-lg-9 { flex: 0 0 75%; max-width: 75%; }
|
||||
.col-lg-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
|
||||
.col-lg-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
|
||||
.col-lg-12 { flex: 0 0 100%; max-width: 100%; }
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.col-xl-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
|
||||
.col-xl-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
|
||||
.col-xl-3 { flex: 0 0 25%; max-width: 25%; }
|
||||
.col-xl-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
|
||||
.col-xl-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
|
||||
.col-xl-6 { flex: 0 0 50%; max-width: 50%; }
|
||||
.col-xl-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
|
||||
.col-xl-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
|
||||
.col-xl-9 { flex: 0 0 75%; max-width: 75%; }
|
||||
.col-xl-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
|
||||
.col-xl-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
|
||||
.col-xl-12 { flex: 0 0 100%; max-width: 100%; }
|
||||
}
|
||||
|
||||
/* Sidebar Toggle Button (hidden by default, shown on mobile) */
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
padding: 0;
|
||||
margin-right: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* Responsive Containers */
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Flexbox Utilities */
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
.flex-nowrap {
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
.justify-content-start {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.justify-content-end {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.justify-content-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.justify-content-around {
|
||||
justify-content: space-around !important;
|
||||
}
|
||||
|
||||
.align-items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.align-items-end {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.align-items-baseline {
|
||||
align-items: baseline !important;
|
||||
}
|
||||
|
||||
.align-items-stretch {
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
/* Spacing Utilities */
|
||||
.m-0 { margin: 0 !important; }
|
||||
.mt-0 { margin-top: 0 !important; }
|
||||
.mr-0 { margin-right: 0 !important; }
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.ml-0 { margin-left: 0 !important; }
|
||||
|
||||
.p-0 { padding: 0 !important; }
|
||||
.pt-0 { padding-top: 0 !important; }
|
||||
.pr-0 { padding-right: 0 !important; }
|
||||
.pb-0 { padding-bottom: 0 !important; }
|
||||
.pl-0 { padding-left: 0 !important; }
|
||||
|
||||
/* Display Utilities */
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.d-inline {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
.d-inline-block {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.d-sm-none { display: none !important; }
|
||||
.d-sm-inline { display: inline !important; }
|
||||
.d-sm-inline-block { display: inline-block !important; }
|
||||
.d-sm-block { display: block !important; }
|
||||
.d-sm-flex { display: flex !important; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.d-md-none { display: none !important; }
|
||||
.d-md-inline { display: inline !important; }
|
||||
.d-md-inline-block { display: inline-block !important; }
|
||||
.d-md-block { display: block !important; }
|
||||
.d-md-flex { display: flex !important; }
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-none { display: none !important; }
|
||||
.d-lg-inline { display: inline !important; }
|
||||
.d-lg-inline-block { display: inline-block !important; }
|
||||
.d-lg-block { display: block !important; }
|
||||
.d-lg-flex { display: flex !important; }
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.d-xl-none { display: none !important; }
|
||||
.d-xl-inline { display: inline !important; }
|
||||
.d-xl-inline-block { display: inline-block !important; }
|
||||
.d-xl-block { display: block !important; }
|
||||
.d-xl-flex { display: flex !important; }
|
||||
}
|
470
api_service/dashboard_web/css/main.css
Normal file
470
api_service/dashboard_web/css/main.css
Normal file
@ -0,0 +1,470 @@
|
||||
/* Main CSS file for the Discord Bot Dashboard */
|
||||
|
||||
:root {
|
||||
/* Color variables */
|
||||
--primary-color: #5865F2; /* Discord blue */
|
||||
--primary-hover: #4752C4;
|
||||
--secondary-color: #2D3748;
|
||||
--accent-color: #7289DA;
|
||||
--success-color: #48BB78;
|
||||
--warning-color: #F6AD55;
|
||||
--danger-color: #F56565;
|
||||
--light-bg: #F7FAFC;
|
||||
--dark-bg: #1A202C;
|
||||
--card-bg: #FFFFFF;
|
||||
--text-primary: #2D3748;
|
||||
--text-secondary: #718096;
|
||||
--border-color: #E2E8F0;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
|
||||
/* Spacing variables */
|
||||
--spacing-1: 0.25rem;
|
||||
--spacing-2: 0.5rem;
|
||||
--spacing-3: 0.75rem;
|
||||
--spacing-4: 1rem;
|
||||
--spacing-6: 1.5rem;
|
||||
--spacing-8: 2rem;
|
||||
--spacing-12: 3rem;
|
||||
--spacing-16: 4rem;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background-color: var(--light-bg);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin-bottom: var(--spacing-4);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-4);
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: var(--spacing-6);
|
||||
margin-left: 250px; /* Width of the sidebar */
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background-color: var(--card-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: var(--spacing-4) var(--spacing-6);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 250px; /* Width of the sidebar */
|
||||
height: 64px;
|
||||
z-index: 10;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--spacing-6);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
padding: var(--spacing-4) 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: var(--spacing-3) var(--spacing-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: var(--spacing-4) var(--spacing-6);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
input[type="search"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: white;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.2);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
.checkbox-label,
|
||||
.radio-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #3a4a63;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #3da066;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: var(--warning-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #e09c49;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #e04c4c;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
font-size: 0.875rem;
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 1.125rem;
|
||||
padding: var(--spacing-3) var(--spacing-6);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mt-1 { margin-top: var(--spacing-1); }
|
||||
.mt-2 { margin-top: var(--spacing-2); }
|
||||
.mt-4 { margin-top: var(--spacing-4); }
|
||||
.mt-6 { margin-top: var(--spacing-6); }
|
||||
|
||||
.mb-1 { margin-bottom: var(--spacing-1); }
|
||||
.mb-2 { margin-bottom: var(--spacing-2); }
|
||||
.mb-4 { margin-bottom: var(--spacing-4); }
|
||||
.mb-6 { margin-bottom: var(--spacing-6); }
|
||||
|
||||
.ml-1 { margin-left: var(--spacing-1); }
|
||||
.ml-2 { margin-left: var(--spacing-2); }
|
||||
.ml-4 { margin-left: var(--spacing-4); }
|
||||
.ml-6 { margin-left: var(--spacing-6); }
|
||||
|
||||
.mr-1 { margin-right: var(--spacing-1); }
|
||||
.mr-2 { margin-right: var(--spacing-2); }
|
||||
.mr-4 { margin-right: var(--spacing-4); }
|
||||
.mr-6 { margin-right: var(--spacing-6); }
|
||||
|
||||
.p-1 { padding: var(--spacing-1); }
|
||||
.p-2 { padding: var(--spacing-2); }
|
||||
.p-4 { padding: var(--spacing-4); }
|
||||
.p-6 { padding: var(--spacing-6); }
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
}
|
@ -4,49 +4,114 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bot Dashboard</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<!-- Preload fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- CSS files -->
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Discord Bot Dashboard</h1>
|
||||
|
||||
<div id="auth-section">
|
||||
<button id="login-button">Login with Discord</button>
|
||||
<!-- Auth Section -->
|
||||
<div id="auth-section" class="container">
|
||||
<div class="card mt-6" style="max-width: 500px; margin: 100px auto; text-align: center;">
|
||||
<h1>Discord Bot Dashboard</h1>
|
||||
<p class="mb-4">Manage your Discord bot settings and configurations</p>
|
||||
<button id="login-button" class="btn btn-primary btn-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||
Login with Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-section" style="display: none;">
|
||||
<h2>Welcome, <span id="username">User</span>!</h2>
|
||||
<button id="logout-button">Logout</button>
|
||||
|
||||
<div class="dashboard-nav">
|
||||
<button id="nav-server-settings" class="nav-button active">Server Settings</button>
|
||||
<button id="nav-ai-settings" class="nav-button">AI Settings</button>
|
||||
<button id="nav-conversations" class="nav-button">Conversations</button>
|
||||
<!-- Dashboard Section -->
|
||||
<div id="dashboard-container" class="dashboard-container" style="display: none;">
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="#" class="sidebar-logo">Bot Dashboard</a>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<a href="#server-settings" class="nav-item active" data-section="server-settings-section">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
|
||||
Server Settings
|
||||
</a>
|
||||
<a href="#ai-settings" class="nav-item" data-section="ai-settings-section">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M12 2a10 10 0 1 0 10 10H12V2z"></path><path d="M20 12a8 8 0 1 0-16 0"></path><path d="M12 12v10"></path></svg>
|
||||
AI Settings
|
||||
</a>
|
||||
<a href="#conversations" class="nav-item" data-section="conversations-section">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||
Conversations
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<button id="logout-button" class="btn btn-danger w-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<button id="sidebar-toggle" class="sidebar-toggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
||||
</button>
|
||||
<h1 class="header-title">Discord Bot Dashboard</h1>
|
||||
<div class="user-info">
|
||||
<div id="user-avatar" class="user-avatar">U</div>
|
||||
<span id="username" class="user-name">User</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content Container -->
|
||||
<div class="content-container" style="margin-top: 80px;">
|
||||
|
||||
<!-- Server Settings Section -->
|
||||
<div id="server-settings-section" class="dashboard-section">
|
||||
<h3>Manage Server Settings</h3>
|
||||
<label for="guild-select">Select Server:</label>
|
||||
<select name="guilds" id="guild-select">
|
||||
<option value="">--Please choose a server--</option>
|
||||
</select>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Manage Server Settings</h2>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="guild-select">Select Server:</label>
|
||||
<select name="guilds" id="guild-select" class="w-full">
|
||||
<option value="">--Please choose a server--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-form" style="display: none;">
|
||||
<h4>Prefix</h4>
|
||||
<label for="prefix-input">Command Prefix:</label>
|
||||
<input type="text" id="prefix-input" name="prefix" maxlength="10">
|
||||
<button id="save-prefix-button">Save Prefix</button>
|
||||
<p id="prefix-feedback"></p>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Prefix Settings</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="prefix-input">Command Prefix:</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="prefix-input" name="prefix" maxlength="10" class="w-full">
|
||||
<button id="save-prefix-button" class="btn btn-primary">
|
||||
Save Prefix
|
||||
</button>
|
||||
</div>
|
||||
<p id="prefix-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Welcome Messages</h4>
|
||||
<div class="settings-card">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Welcome Messages</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="welcome-channel">Welcome Channel:</label>
|
||||
<div class="channel-select-container">
|
||||
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID">
|
||||
<select id="welcome-channel-select" class="channel-dropdown">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID" class="w-full">
|
||||
<select id="welcome-channel-select" class="w-full">
|
||||
<option value="">-- Select Channel --</option>
|
||||
<!-- Will be populated by JS -->
|
||||
</select>
|
||||
@ -54,22 +119,24 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="welcome-message">Welcome Message Template:</label>
|
||||
<textarea id="welcome-message" name="welcome_message" rows="4" cols="50" placeholder="Use {user} for mention, {username} for name, {server} for server name."></textarea>
|
||||
<textarea id="welcome-message" name="welcome_message" rows="4" placeholder="Use {user} for mention, {username} for name, {server} for server name." class="w-full"></textarea>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-welcome-button">Save Welcome Settings</button>
|
||||
<button id="disable-welcome-button">Disable Welcome</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-welcome-button" class="btn btn-primary">Save Welcome Settings</button>
|
||||
<button id="disable-welcome-button" class="btn btn-warning">Disable Welcome</button>
|
||||
</div>
|
||||
<p id="welcome-feedback"></p>
|
||||
<p id="welcome-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<h4>Goodbye Messages</h4>
|
||||
<div class="settings-card">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Goodbye Messages</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="goodbye-channel">Goodbye Channel:</label>
|
||||
<div class="channel-select-container">
|
||||
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID">
|
||||
<select id="goodbye-channel-select" class="channel-dropdown">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID" class="w-full">
|
||||
<select id="goodbye-channel-select" class="w-full">
|
||||
<option value="">-- Select Channel --</option>
|
||||
<!-- Will be populated by JS -->
|
||||
</select>
|
||||
@ -77,64 +144,79 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="goodbye-message">Goodbye Message Template:</label>
|
||||
<textarea id="goodbye-message" name="goodbye_message" rows="4" cols="50" placeholder="Use {username} for name, {server} for server name."></textarea>
|
||||
<textarea id="goodbye-message" name="goodbye_message" rows="4" placeholder="Use {username} for name, {server} for server name." class="w-full"></textarea>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-goodbye-button">Save Goodbye Settings</button>
|
||||
<button id="disable-goodbye-button">Disable Goodbye</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-goodbye-button" class="btn btn-primary">Save Goodbye Settings</button>
|
||||
<button id="disable-goodbye-button" class="btn btn-warning">Disable Goodbye</button>
|
||||
</div>
|
||||
<p id="goodbye-feedback"></p>
|
||||
<p id="goodbye-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<h4>Enabled Modules (Cogs)</h4>
|
||||
<div class="settings-card">
|
||||
<div class="cogs-container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Enabled Modules (Cogs)</h3>
|
||||
</div>
|
||||
<div class="cogs-container p-4 border rounded mb-4">
|
||||
<div id="cogs-list">
|
||||
<!-- Cog checkboxes will be populated by JS -->
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-cogs-button">Save Module Settings</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-cogs-button" class="btn btn-primary">Save Module Settings</button>
|
||||
</div>
|
||||
<p id="cogs-feedback"></p>
|
||||
<p id="cogs-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<h4>Command Permissions</h4>
|
||||
<div class="settings-card">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Command Permissions</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="command-select">Command:</label>
|
||||
<select id="command-select">
|
||||
<select id="command-select" class="w-full">
|
||||
<!-- Will be populated by JS -->
|
||||
<option value="">-- Select Command --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="role-select">Role:</label>
|
||||
<select id="role-select">
|
||||
<select id="role-select" class="w-full">
|
||||
<!-- Will be populated by JS -->
|
||||
<option value="">-- Select Role --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="add-perm-button">Allow Role</button>
|
||||
<button id="remove-perm-button">Disallow Role</button>
|
||||
<div class="btn-group">
|
||||
<button id="add-perm-button" class="btn btn-success">Allow Role</button>
|
||||
<button id="remove-perm-button" class="btn btn-danger">Disallow Role</button>
|
||||
</div>
|
||||
<div id="current-perms">
|
||||
<div id="current-perms" class="mt-4 p-4 border rounded">
|
||||
<!-- Current permissions will be listed here -->
|
||||
<div class="text-gray">No permissions set yet.</div>
|
||||
</div>
|
||||
<p id="perms-feedback"></p>
|
||||
<p id="perms-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Settings Section -->
|
||||
<div id="ai-settings-section" class="dashboard-section" style="display: none;">
|
||||
<h3>AI Settings</h3>
|
||||
<div class="settings-card">
|
||||
<h4>General AI Settings</h4>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">AI Settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">General AI Settings</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ai-model-select">AI Model:</label>
|
||||
<select id="ai-model-select">
|
||||
<select id="ai-model-select" class="w-full">
|
||||
<option value="openai/gpt-3.5-turbo">GPT-3.5 Turbo</option>
|
||||
<option value="openai/gpt-4">GPT-4</option>
|
||||
<option value="anthropic/claude-3-opus">Claude 3 Opus</option>
|
||||
@ -146,131 +228,196 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ai-temperature">Temperature: <span id="temperature-value">0.7</span></label>
|
||||
<input type="range" id="ai-temperature" min="0" max="2" step="0.1" value="0.7">
|
||||
<input type="range" id="ai-temperature" min="0" max="2" step="0.1" value="0.7" class="w-full">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ai-max-tokens">Max Tokens:</label>
|
||||
<input type="number" id="ai-max-tokens" min="100" max="8000" step="100" value="1000">
|
||||
<input type="number" id="ai-max-tokens" min="100" max="8000" step="100" value="1000" class="w-full">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="ai-reasoning-enabled">
|
||||
Enable Reasoning
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" id="reasoning-effort-group">
|
||||
<label for="ai-reasoning-effort">Reasoning Effort:</label>
|
||||
<select id="ai-reasoning-effort">
|
||||
<select id="ai-reasoning-effort" class="w-full">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="ai-web-search-enabled">
|
||||
Enable Web Search
|
||||
</label>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-ai-settings-button">Save AI Settings</button>
|
||||
<button id="reset-ai-settings-button">Reset to Defaults</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-ai-settings-button" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
||||
Save AI Settings
|
||||
</button>
|
||||
<button id="reset-ai-settings-button" class="btn btn-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
<p id="ai-settings-feedback"></p>
|
||||
<p id="ai-settings-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h4>Character Settings</h4>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Character Settings</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ai-character">Character Name:</label>
|
||||
<input type="text" id="ai-character" placeholder="e.g., Kasane Teto">
|
||||
<input type="text" id="ai-character" placeholder="e.g., Kasane Teto" class="w-full">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ai-character-info">Character Information:</label>
|
||||
<textarea id="ai-character-info" rows="4" placeholder="Describe the character's personality, background, etc."></textarea>
|
||||
<textarea id="ai-character-info" rows="4" placeholder="Describe the character's personality, background, etc." class="w-full"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="ai-character-breakdown">
|
||||
Enable Character Breakdown
|
||||
</label>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-character-settings-button">Save Character Settings</button>
|
||||
<button id="clear-character-settings-button">Clear Character</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-character-settings-button" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
||||
Save Character Settings
|
||||
</button>
|
||||
<button id="clear-character-settings-button" class="btn btn-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
Clear Character
|
||||
</button>
|
||||
</div>
|
||||
<p id="character-settings-feedback"></p>
|
||||
<p id="character-settings-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h4>System Prompt</h4>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">System Prompt</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea id="ai-system-prompt" rows="6" placeholder="Enter a system prompt to guide the AI's behavior..."></textarea>
|
||||
<textarea id="ai-system-prompt" rows="6" placeholder="Enter a system prompt to guide the AI's behavior..." class="w-full"></textarea>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-system-prompt-button">Save System Prompt</button>
|
||||
<button id="reset-system-prompt-button">Reset to Default</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-system-prompt-button" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
||||
Save System Prompt
|
||||
</button>
|
||||
<button id="reset-system-prompt-button" class="btn btn-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
|
||||
Reset to Default
|
||||
</button>
|
||||
</div>
|
||||
<p id="system-prompt-feedback"></p>
|
||||
<p id="system-prompt-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h4>Custom Instructions</h4>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Custom Instructions</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea id="ai-custom-instructions" rows="6" placeholder="Enter custom instructions for the AI..."></textarea>
|
||||
<textarea id="ai-custom-instructions" rows="6" placeholder="Enter custom instructions for the AI..." class="w-full"></textarea>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="save-custom-instructions-button">Save Custom Instructions</button>
|
||||
<button id="clear-custom-instructions-button">Clear Instructions</button>
|
||||
<div class="btn-group">
|
||||
<button id="save-custom-instructions-button" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
||||
Save Custom Instructions
|
||||
</button>
|
||||
<button id="clear-custom-instructions-button" class="btn btn-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
||||
Clear Instructions
|
||||
</button>
|
||||
</div>
|
||||
<p id="custom-instructions-feedback"></p>
|
||||
<p id="custom-instructions-feedback" class="mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversations Section -->
|
||||
<div id="conversations-section" class="dashboard-section" style="display: none;">
|
||||
<h3>AI Conversations</h3>
|
||||
|
||||
<div class="conversations-header">
|
||||
<div class="search-container">
|
||||
<input type="text" id="conversation-search" placeholder="Search conversations...">
|
||||
</div>
|
||||
<button id="new-conversation-button">New Conversation</button>
|
||||
</div>
|
||||
|
||||
<div class="conversations-list-container">
|
||||
<div id="conversations-list">
|
||||
<!-- Will be populated by JS -->
|
||||
<div class="no-conversations">No conversations found. Start a new conversation!</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">AI Conversations</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="conversation-detail" style="display: none;">
|
||||
<div class="conversation-header">
|
||||
<h4 id="conversation-title">Conversation Title</h4>
|
||||
<div class="conversation-actions">
|
||||
<button id="rename-conversation-button">Rename</button>
|
||||
<button id="delete-conversation-button">Delete</button>
|
||||
<button id="export-conversation-button">Export</button>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h3 class="card-title mb-0">Your Conversations</h3>
|
||||
<button id="new-conversation-button" class="btn btn-primary btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
New Conversation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<div class="search-container">
|
||||
<input type="text" id="conversation-search" placeholder="Search conversations..." class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversation-messages" id="conversation-messages">
|
||||
<!-- Will be populated by JS -->
|
||||
<div class="conversations-list-container border rounded p-0 mb-4" style="max-height: 300px; overflow-y: auto;">
|
||||
<div id="conversations-list">
|
||||
<!-- Will be populated by JS -->
|
||||
<div class="no-conversations p-4 text-center text-gray">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="margin: 0 auto 1rem;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||
<p>No conversations found.</p>
|
||||
<button id="start-conversation-button" class="btn btn-primary btn-sm mt-2">Start a new conversation</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="conversation-detail" class="card" style="display: none;">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h3 id="conversation-title" class="card-title mb-0">Conversation Title</h3>
|
||||
<div class="conversation-actions">
|
||||
<button id="rename-conversation-button" class="btn btn-secondary btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
|
||||
</button>
|
||||
<button id="export-conversation-button" class="btn btn-secondary btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
</button>
|
||||
<button id="delete-conversation-button" class="btn btn-danger btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversation-messages p-4" id="conversation-messages" style="max-height: 500px; overflow-y: auto;">
|
||||
<!-- Will be populated by JS -->
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for renaming conversations -->
|
||||
<div id="rename-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal">×</span>
|
||||
<h4>Rename Conversation</h4>
|
||||
<input type="text" id="new-conversation-title" placeholder="Enter new title">
|
||||
<div class="button-group">
|
||||
<button id="confirm-rename-button">Rename</button>
|
||||
<button id="cancel-rename-button">Cancel</button>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Rename Conversation</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="new-conversation-title">New Title:</label>
|
||||
<input type="text" id="new-conversation-title" placeholder="Enter new title" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="cancel-rename-button" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirm-rename-button" class="btn btn-primary">Rename</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -278,16 +425,42 @@
|
||||
<!-- Modal for creating new conversations -->
|
||||
<div id="new-conversation-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal">×</span>
|
||||
<h4>Create New Conversation</h4>
|
||||
<input type="text" id="new-conversation-name" placeholder="Enter conversation title">
|
||||
<div class="button-group">
|
||||
<button id="create-conversation-button">Create</button>
|
||||
<button id="cancel-create-button">Cancel</button>
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Create New Conversation</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="new-conversation-name">Conversation Title:</label>
|
||||
<input type="text" id="new-conversation-name" placeholder="Enter conversation title" class="w-full">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="cancel-create-button" class="btn btn-secondary">Cancel</button>
|
||||
<button id="create-conversation-button" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
<!-- Delete confirmation modal -->
|
||||
<div id="delete-confirm-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Delete Conversation</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this conversation? This action cannot be undone.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="cancel-delete-button" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirm-delete-button" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript files -->
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
723
api_service/dashboard_web/js/main.js
Normal file
723
api_service/dashboard_web/js/main.js
Normal file
@ -0,0 +1,723 @@
|
||||
/**
|
||||
* Main JavaScript file for the Discord Bot Dashboard
|
||||
*/
|
||||
|
||||
// Initialize components when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize modals
|
||||
Modal.init();
|
||||
|
||||
// Initialize sidebar toggle
|
||||
initSidebar();
|
||||
|
||||
// Initialize authentication
|
||||
initAuth();
|
||||
|
||||
// Initialize tabs
|
||||
initTabs();
|
||||
|
||||
// Initialize dropdowns
|
||||
initDropdowns();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize sidebar functionality
|
||||
*/
|
||||
function initSidebar() {
|
||||
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('show');
|
||||
});
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', (event) => {
|
||||
if (window.innerWidth <= 768 &&
|
||||
sidebar.classList.contains('show') &&
|
||||
!sidebar.contains(event.target) &&
|
||||
event.target !== sidebarToggle) {
|
||||
sidebar.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set active nav item based on current page
|
||||
const currentPath = window.location.pathname;
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
const href = item.getAttribute('href');
|
||||
if (href && currentPath.includes(href)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize authentication
|
||||
*/
|
||||
function initAuth() {
|
||||
const loginButton = document.getElementById('login-button');
|
||||
const logoutButton = document.getElementById('logout-button');
|
||||
const authSection = document.getElementById('auth-section');
|
||||
const dashboardSection = document.getElementById('dashboard-container');
|
||||
|
||||
// Check authentication status
|
||||
checkAuthStatus();
|
||||
|
||||
// Login button event
|
||||
if (loginButton) {
|
||||
loginButton.addEventListener('click', () => {
|
||||
window.location.href = '/dashboard/api/auth/login';
|
||||
});
|
||||
}
|
||||
|
||||
// Logout button event
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
// Clear session
|
||||
fetch('/dashboard/api/auth/logout', { method: 'POST' })
|
||||
.then(() => {
|
||||
// Redirect to login page
|
||||
window.location.reload();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout error:', error);
|
||||
Toast.error('Failed to logout. Please try again.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
function checkAuthStatus() {
|
||||
fetch('/dashboard/api/auth/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.authenticated) {
|
||||
// User is authenticated, show dashboard
|
||||
if (authSection) authSection.style.display = 'none';
|
||||
if (dashboardSection) dashboardSection.style.display = 'block';
|
||||
|
||||
// Load user info
|
||||
loadUserInfo();
|
||||
|
||||
// Load initial data
|
||||
loadDashboardData();
|
||||
} else {
|
||||
// User is not authenticated, show login
|
||||
if (authSection) authSection.style.display = 'block';
|
||||
if (dashboardSection) dashboardSection.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Auth check error:', error);
|
||||
// Assume not authenticated on error
|
||||
if (authSection) authSection.style.display = 'block';
|
||||
if (dashboardSection) dashboardSection.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user information
|
||||
*/
|
||||
function loadUserInfo() {
|
||||
fetch('/dashboard/api/auth/user')
|
||||
.then(response => response.json())
|
||||
.then(user => {
|
||||
// Update username display
|
||||
const usernameSpan = document.getElementById('username');
|
||||
if (usernameSpan) {
|
||||
usernameSpan.textContent = user.username;
|
||||
}
|
||||
|
||||
// Update avatar if available
|
||||
const userAvatar = document.getElementById('user-avatar');
|
||||
if (userAvatar) {
|
||||
if (user.avatar) {
|
||||
userAvatar.style.backgroundImage = `url(https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png)`;
|
||||
userAvatar.textContent = '';
|
||||
} else {
|
||||
// Set initials as fallback
|
||||
userAvatar.textContent = user.username.substring(0, 1).toUpperCase();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading user info:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tab functionality
|
||||
*/
|
||||
function initTabs() {
|
||||
document.querySelectorAll('.tabs').forEach(tabContainer => {
|
||||
const tabs = tabContainer.querySelectorAll('.tab');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
// Get target content ID
|
||||
const target = tab.getAttribute('data-target');
|
||||
if (!target) return;
|
||||
|
||||
// Remove active class from all tabs
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked tab
|
||||
tab.classList.add('active');
|
||||
|
||||
// Hide all tab content
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
tabContents.forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show target content
|
||||
const targetContent = document.getElementById(target);
|
||||
if (targetContent) {
|
||||
targetContent.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize dropdown functionality
|
||||
*/
|
||||
function initDropdowns() {
|
||||
document.querySelectorAll('.dropdown-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const dropdown = toggle.closest('.dropdown');
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
// Close all other dropdowns
|
||||
document.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
|
||||
if (openMenu !== menu) {
|
||||
openMenu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle this dropdown
|
||||
menu.classList.toggle('show');
|
||||
});
|
||||
});
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.dropdown')) {
|
||||
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
|
||||
menu.classList.remove('show');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dashboard data
|
||||
*/
|
||||
function loadDashboardData() {
|
||||
// Load guilds for server select
|
||||
loadGuilds();
|
||||
|
||||
// Load global settings
|
||||
loadGlobalSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load guilds for server select dropdown
|
||||
*/
|
||||
function loadGuilds() {
|
||||
const guildSelect = document.getElementById('guild-select');
|
||||
if (!guildSelect) return;
|
||||
|
||||
// Show loading state
|
||||
guildSelect.disabled = true;
|
||||
guildSelect.innerHTML = '<option value="">Loading servers...</option>';
|
||||
|
||||
// Fetch guilds from API
|
||||
API.get('/dashboard/api/guilds')
|
||||
.then(guilds => {
|
||||
// Clear loading state
|
||||
guildSelect.innerHTML = '<option value="">--Please choose a server--</option>';
|
||||
|
||||
// Add guilds to select
|
||||
guilds.forEach(guild => {
|
||||
const option = document.createElement('option');
|
||||
option.value = guild.id;
|
||||
option.textContent = guild.name;
|
||||
guildSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Enable select
|
||||
guildSelect.disabled = false;
|
||||
|
||||
// Add change event
|
||||
guildSelect.addEventListener('change', () => {
|
||||
const guildId = guildSelect.value;
|
||||
if (guildId) {
|
||||
loadGuildSettings(guildId);
|
||||
} else {
|
||||
// Hide settings form if no guild selected
|
||||
const settingsForm = document.getElementById('settings-form');
|
||||
if (settingsForm) {
|
||||
settingsForm.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading guilds:', error);
|
||||
guildSelect.innerHTML = '<option value="">Error loading servers</option>';
|
||||
guildSelect.disabled = false;
|
||||
Toast.error('Failed to load servers. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings for a specific guild
|
||||
* @param {string} guildId - The guild ID
|
||||
*/
|
||||
function loadGuildSettings(guildId) {
|
||||
const settingsForm = document.getElementById('settings-form');
|
||||
if (!settingsForm) return;
|
||||
|
||||
// Show loading state
|
||||
const loadingContainer = document.createElement('div');
|
||||
loadingContainer.className = 'loading-container';
|
||||
loadingContainer.innerHTML = '<div class="loading-spinner"></div><p>Loading server settings...</p>';
|
||||
loadingContainer.style.textAlign = 'center';
|
||||
loadingContainer.style.padding = '2rem';
|
||||
|
||||
settingsForm.style.display = 'none';
|
||||
settingsForm.parentNode.insertBefore(loadingContainer, settingsForm);
|
||||
|
||||
// Fetch guild settings from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/settings`)
|
||||
.then(settings => {
|
||||
// Remove loading container
|
||||
loadingContainer.remove();
|
||||
|
||||
// Show settings form
|
||||
settingsForm.style.display = 'block';
|
||||
|
||||
// Populate form with settings
|
||||
populateGuildSettings(settings);
|
||||
|
||||
// Load additional data
|
||||
loadGuildChannels(guildId);
|
||||
loadGuildRoles(guildId);
|
||||
loadGuildCommands(guildId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading guild settings:', error);
|
||||
loadingContainer.innerHTML = '<p class="text-danger">Error loading server settings. Please try again.</p>';
|
||||
Toast.error('Failed to load server settings. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate guild settings form with data
|
||||
* @param {Object} settings - The guild settings
|
||||
*/
|
||||
function populateGuildSettings(settings) {
|
||||
// Prefix
|
||||
const prefixInput = document.getElementById('prefix-input');
|
||||
if (prefixInput) {
|
||||
prefixInput.value = settings.prefix || '!';
|
||||
}
|
||||
|
||||
// Welcome settings
|
||||
const welcomeChannel = document.getElementById('welcome-channel');
|
||||
const welcomeMessage = document.getElementById('welcome-message');
|
||||
|
||||
if (welcomeChannel && welcomeMessage) {
|
||||
welcomeChannel.value = settings.welcome_channel_id || '';
|
||||
welcomeMessage.value = settings.welcome_message || '';
|
||||
}
|
||||
|
||||
// Goodbye settings
|
||||
const goodbyeChannel = document.getElementById('goodbye-channel');
|
||||
const goodbyeMessage = document.getElementById('goodbye-message');
|
||||
|
||||
if (goodbyeChannel && goodbyeMessage) {
|
||||
goodbyeChannel.value = settings.goodbye_channel_id || '';
|
||||
goodbyeMessage.value = settings.goodbye_message || '';
|
||||
}
|
||||
|
||||
// Cogs (modules)
|
||||
const cogsList = document.getElementById('cogs-list');
|
||||
if (cogsList && settings.enabled_cogs) {
|
||||
cogsList.innerHTML = '';
|
||||
|
||||
Object.entries(settings.enabled_cogs).forEach(([cogName, enabled]) => {
|
||||
const cogDiv = document.createElement('div');
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.id = `cog-${cogName}`;
|
||||
checkbox.name = `cog-${cogName}`;
|
||||
checkbox.checked = enabled;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `cog-${cogName}`;
|
||||
label.textContent = cogName;
|
||||
|
||||
cogDiv.appendChild(checkbox);
|
||||
cogDiv.appendChild(label);
|
||||
cogsList.appendChild(cogDiv);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load channels for a guild
|
||||
* @param {string} guildId - The guild ID
|
||||
*/
|
||||
function loadGuildChannels(guildId) {
|
||||
const welcomeChannelSelect = document.getElementById('welcome-channel-select');
|
||||
const goodbyeChannelSelect = document.getElementById('goodbye-channel-select');
|
||||
|
||||
if (!welcomeChannelSelect && !goodbyeChannelSelect) return;
|
||||
|
||||
// Fetch channels from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/channels`)
|
||||
.then(channels => {
|
||||
// Filter text channels
|
||||
const textChannels = channels.filter(channel => channel.type === 0);
|
||||
|
||||
// Populate welcome channel select
|
||||
if (welcomeChannelSelect) {
|
||||
welcomeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
|
||||
|
||||
textChannels.forEach(channel => {
|
||||
const option = document.createElement('option');
|
||||
option.value = channel.id;
|
||||
option.textContent = `#${channel.name}`;
|
||||
welcomeChannelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set current value if available
|
||||
const welcomeChannelInput = document.getElementById('welcome-channel');
|
||||
if (welcomeChannelInput && welcomeChannelInput.value) {
|
||||
welcomeChannelSelect.value = welcomeChannelInput.value;
|
||||
}
|
||||
|
||||
// Add change event
|
||||
welcomeChannelSelect.addEventListener('change', () => {
|
||||
if (welcomeChannelInput) {
|
||||
welcomeChannelInput.value = welcomeChannelSelect.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Populate goodbye channel select
|
||||
if (goodbyeChannelSelect) {
|
||||
goodbyeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
|
||||
|
||||
textChannels.forEach(channel => {
|
||||
const option = document.createElement('option');
|
||||
option.value = channel.id;
|
||||
option.textContent = `#${channel.name}`;
|
||||
goodbyeChannelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set current value if available
|
||||
const goodbyeChannelInput = document.getElementById('goodbye-channel');
|
||||
if (goodbyeChannelInput && goodbyeChannelInput.value) {
|
||||
goodbyeChannelSelect.value = goodbyeChannelInput.value;
|
||||
}
|
||||
|
||||
// Add change event
|
||||
goodbyeChannelSelect.addEventListener('change', () => {
|
||||
if (goodbyeChannelInput) {
|
||||
goodbyeChannelInput.value = goodbyeChannelSelect.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading channels:', error);
|
||||
Toast.error('Failed to load channels. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load roles for a guild
|
||||
* @param {string} guildId - The guild ID
|
||||
*/
|
||||
function loadGuildRoles(guildId) {
|
||||
const roleSelect = document.getElementById('role-select');
|
||||
if (!roleSelect) return;
|
||||
|
||||
// Fetch roles from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/roles`)
|
||||
.then(roles => {
|
||||
roleSelect.innerHTML = '<option value="">-- Select Role --</option>';
|
||||
|
||||
roles.forEach(role => {
|
||||
const option = document.createElement('option');
|
||||
option.value = role.id;
|
||||
option.textContent = role.name;
|
||||
|
||||
// Set color if available
|
||||
if (role.color) {
|
||||
option.style.color = `#${role.color.toString(16).padStart(6, '0')}`;
|
||||
}
|
||||
|
||||
roleSelect.appendChild(option);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading roles:', error);
|
||||
Toast.error('Failed to load roles. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load commands for a guild
|
||||
* @param {string} guildId - The guild ID
|
||||
*/
|
||||
function loadGuildCommands(guildId) {
|
||||
const commandSelect = document.getElementById('command-select');
|
||||
if (!commandSelect) return;
|
||||
|
||||
// Fetch commands from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/commands`)
|
||||
.then(commands => {
|
||||
commandSelect.innerHTML = '<option value="">-- Select Command --</option>';
|
||||
|
||||
commands.forEach(command => {
|
||||
const option = document.createElement('option');
|
||||
option.value = command.name;
|
||||
option.textContent = command.name;
|
||||
|
||||
// Add description as title attribute
|
||||
if (command.description) {
|
||||
option.title = command.description;
|
||||
}
|
||||
|
||||
commandSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Load command permissions
|
||||
loadCommandPermissions(guildId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading commands:', error);
|
||||
Toast.error('Failed to load commands. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load command permissions for a guild
|
||||
* @param {string} guildId - The guild ID
|
||||
*/
|
||||
function loadCommandPermissions(guildId) {
|
||||
const currentPerms = document.getElementById('current-perms');
|
||||
if (!currentPerms) return;
|
||||
|
||||
// Show loading state
|
||||
currentPerms.innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner loading-spinner-sm"></div></div>';
|
||||
|
||||
// Fetch command permissions from API
|
||||
API.get(`/dashboard/api/guilds/${guildId}/command-permissions`)
|
||||
.then(permissions => {
|
||||
// Clear loading state
|
||||
currentPerms.innerHTML = '';
|
||||
|
||||
if (permissions.length === 0) {
|
||||
currentPerms.innerHTML = '<div class="text-gray">No custom permissions set.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group permissions by command
|
||||
const permsByCommand = {};
|
||||
|
||||
permissions.forEach(perm => {
|
||||
if (!permsByCommand[perm.command]) {
|
||||
permsByCommand[perm.command] = [];
|
||||
}
|
||||
|
||||
permsByCommand[perm.command].push(perm);
|
||||
});
|
||||
|
||||
// Create permission elements
|
||||
Object.entries(permsByCommand).forEach(([command, perms]) => {
|
||||
const commandDiv = document.createElement('div');
|
||||
commandDiv.className = 'command-perms';
|
||||
commandDiv.innerHTML = `<div class="command-name">${command}</div>`;
|
||||
|
||||
const rolesList = document.createElement('div');
|
||||
rolesList.className = 'roles-list';
|
||||
|
||||
perms.forEach(perm => {
|
||||
const roleSpan = document.createElement('span');
|
||||
roleSpan.className = 'role-badge';
|
||||
roleSpan.textContent = perm.role_name;
|
||||
roleSpan.dataset.roleId = perm.role_id;
|
||||
roleSpan.dataset.command = command;
|
||||
|
||||
// Add remove button
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'role-remove';
|
||||
removeBtn.innerHTML = '×';
|
||||
removeBtn.addEventListener('click', () => {
|
||||
removeCommandPermission(guildId, command, perm.role_id);
|
||||
});
|
||||
|
||||
roleSpan.appendChild(removeBtn);
|
||||
rolesList.appendChild(roleSpan);
|
||||
});
|
||||
|
||||
commandDiv.appendChild(rolesList);
|
||||
currentPerms.appendChild(commandDiv);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading command permissions:', error);
|
||||
currentPerms.innerHTML = '<div class="text-danger">Error loading permissions.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a command permission
|
||||
* @param {string} guildId - The guild ID
|
||||
* @param {string} command - The command name
|
||||
* @param {string} roleId - The role ID
|
||||
*/
|
||||
function addCommandPermission(guildId, command, roleId) {
|
||||
const addPermButton = document.getElementById('add-perm-button');
|
||||
const permsFeedback = document.getElementById('perms-feedback');
|
||||
|
||||
if (!command || !roleId) {
|
||||
permsFeedback.textContent = 'Please select both a command and a role.';
|
||||
permsFeedback.className = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
addPermButton.disabled = true;
|
||||
addPermButton.classList.add('btn-loading');
|
||||
|
||||
// Send request to API
|
||||
API.post(`/dashboard/api/guilds/${guildId}/command-permissions`, {
|
||||
command,
|
||||
role_id: roleId
|
||||
})
|
||||
.then(() => {
|
||||
// Show success message
|
||||
permsFeedback.textContent = 'Permission added successfully.';
|
||||
permsFeedback.className = '';
|
||||
|
||||
// Reload permissions
|
||||
loadCommandPermissions(guildId);
|
||||
|
||||
// Reset form
|
||||
document.getElementById('command-select').value = '';
|
||||
document.getElementById('role-select').value = '';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error adding permission:', error);
|
||||
permsFeedback.textContent = 'Error adding permission. Please try again.';
|
||||
permsFeedback.className = 'error';
|
||||
})
|
||||
.finally(() => {
|
||||
// Remove loading state
|
||||
addPermButton.disabled = false;
|
||||
addPermButton.classList.remove('btn-loading');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a command permission
|
||||
* @param {string} guildId - The guild ID
|
||||
* @param {string} command - The command name
|
||||
* @param {string} roleId - The role ID
|
||||
*/
|
||||
function removeCommandPermission(guildId, command, roleId) {
|
||||
// Send request to API
|
||||
API.delete(`/dashboard/api/guilds/${guildId}/command-permissions?command=${command}&role_id=${roleId}`)
|
||||
.then(() => {
|
||||
// Show success message
|
||||
Toast.success('Permission removed successfully.');
|
||||
|
||||
// Reload permissions
|
||||
loadCommandPermissions(guildId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error removing permission:', error);
|
||||
Toast.error('Error removing permission. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load global AI settings
|
||||
*/
|
||||
function loadGlobalSettings() {
|
||||
const aiSettingsSection = document.getElementById('ai-settings-section');
|
||||
if (!aiSettingsSection) return;
|
||||
|
||||
// Fetch global settings from API
|
||||
API.get('/dashboard/api/settings')
|
||||
.then(settings => {
|
||||
// Populate AI model select
|
||||
const modelSelect = document.getElementById('ai-model-select');
|
||||
if (modelSelect && settings.model) {
|
||||
modelSelect.value = settings.model;
|
||||
}
|
||||
|
||||
// Populate temperature slider
|
||||
const temperatureSlider = document.getElementById('ai-temperature');
|
||||
const temperatureValue = document.getElementById('temperature-value');
|
||||
if (temperatureSlider && temperatureValue && settings.temperature) {
|
||||
temperatureSlider.value = settings.temperature;
|
||||
temperatureValue.textContent = settings.temperature;
|
||||
|
||||
// Add input event for live update
|
||||
temperatureSlider.addEventListener('input', () => {
|
||||
temperatureValue.textContent = temperatureSlider.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Populate max tokens
|
||||
const maxTokensInput = document.getElementById('ai-max-tokens');
|
||||
if (maxTokensInput && settings.max_tokens) {
|
||||
maxTokensInput.value = settings.max_tokens;
|
||||
}
|
||||
|
||||
// Populate character settings
|
||||
const characterInput = document.getElementById('ai-character');
|
||||
const characterInfoInput = document.getElementById('ai-character-info');
|
||||
|
||||
if (characterInput && settings.character) {
|
||||
characterInput.value = settings.character;
|
||||
}
|
||||
|
||||
if (characterInfoInput && settings.character_info) {
|
||||
characterInfoInput.value = settings.character_info;
|
||||
}
|
||||
|
||||
// Populate system prompt
|
||||
const systemPromptInput = document.getElementById('ai-system-prompt');
|
||||
if (systemPromptInput && settings.system_message) {
|
||||
systemPromptInput.value = settings.system_message;
|
||||
}
|
||||
|
||||
// Populate custom instructions
|
||||
const customInstructionsInput = document.getElementById('ai-custom-instructions');
|
||||
if (customInstructionsInput && settings.custom_instructions) {
|
||||
customInstructionsInput.value = settings.custom_instructions;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading global settings:', error);
|
||||
Toast.error('Failed to load AI settings. Please try again.');
|
||||
});
|
||||
}
|
537
api_service/dashboard_web/js/utils.js
Normal file
537
api_service/dashboard_web/js/utils.js
Normal file
@ -0,0 +1,537 @@
|
||||
/**
|
||||
* Utility functions for the Discord Bot Dashboard
|
||||
*/
|
||||
|
||||
// Toast notification system
|
||||
const Toast = {
|
||||
container: null,
|
||||
|
||||
/**
|
||||
* Initialize the toast container
|
||||
*/
|
||||
init() {
|
||||
// Create toast container if it doesn't exist
|
||||
if (!this.container) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'toast-container';
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a toast notification
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} type - The type of toast (success, error, warning, info)
|
||||
* @param {string} title - Optional title for the toast
|
||||
* @param {number} duration - Duration in milliseconds before auto-hiding
|
||||
*/
|
||||
show(message, type = 'info', title = '', duration = 5000) {
|
||||
this.init();
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
// Create icon based on type
|
||||
let iconSvg = '';
|
||||
switch (type) {
|
||||
case 'success':
|
||||
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#48BB78" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>';
|
||||
break;
|
||||
case 'error':
|
||||
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F56565" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>';
|
||||
break;
|
||||
case 'warning':
|
||||
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F6AD55" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
|
||||
break;
|
||||
default: // info
|
||||
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5865F2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
|
||||
}
|
||||
|
||||
// Set toast content
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${iconSvg}</div>
|
||||
<div class="toast-content">
|
||||
${title ? `<div class="toast-title">${title}</div>` : ''}
|
||||
<div class="toast-message">${message}</div>
|
||||
</div>
|
||||
<button class="toast-close">×</button>
|
||||
`;
|
||||
|
||||
// Add to container
|
||||
this.container.appendChild(toast);
|
||||
|
||||
// Add close event
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => this.hide(toast));
|
||||
|
||||
// Auto-hide after duration
|
||||
if (duration) {
|
||||
setTimeout(() => this.hide(toast), duration);
|
||||
}
|
||||
|
||||
return toast;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a toast notification
|
||||
* @param {HTMLElement} toast - The toast element to hide
|
||||
*/
|
||||
hide(toast) {
|
||||
toast.classList.add('toast-hiding');
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 300); // Match animation duration
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a success toast
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} title - Optional title
|
||||
*/
|
||||
success(message, title = 'Success') {
|
||||
return this.show(message, 'success', title);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an error toast
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} title - Optional title
|
||||
*/
|
||||
error(message, title = 'Error') {
|
||||
return this.show(message, 'error', title);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a warning toast
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} title - Optional title
|
||||
*/
|
||||
warning(message, title = 'Warning') {
|
||||
return this.show(message, 'warning', title);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an info toast
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} title - Optional title
|
||||
*/
|
||||
info(message, title = 'Info') {
|
||||
return this.show(message, 'info', title);
|
||||
}
|
||||
};
|
||||
|
||||
// API utilities
|
||||
const API = {
|
||||
/**
|
||||
* Make an API request with loading state
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} options - Fetch options
|
||||
* @param {HTMLElement} loadingElement - Element to show loading state on
|
||||
* @returns {Promise} - The fetch promise
|
||||
*/
|
||||
async request(url, options = {}, loadingElement = null) {
|
||||
// Set default headers
|
||||
options.headers = options.headers || {};
|
||||
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
|
||||
|
||||
// Add loading state
|
||||
if (loadingElement) {
|
||||
if (loadingElement.tagName === 'BUTTON') {
|
||||
loadingElement.disabled = true;
|
||||
loadingElement.classList.add('btn-loading');
|
||||
} else {
|
||||
// Create or use existing loading overlay
|
||||
let overlay = loadingElement.querySelector('.loading-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'loading-overlay';
|
||||
overlay.innerHTML = '<div class="loading-spinner"></div>';
|
||||
|
||||
// Make sure the element has position relative for absolute positioning
|
||||
const computedStyle = window.getComputedStyle(loadingElement);
|
||||
if (computedStyle.position === 'static') {
|
||||
loadingElement.style.position = 'relative';
|
||||
}
|
||||
|
||||
loadingElement.appendChild(overlay);
|
||||
} else {
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
// Parse JSON response
|
||||
let data;
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
// Remove loading state
|
||||
if (loadingElement) {
|
||||
if (loadingElement.tagName === 'BUTTON') {
|
||||
loadingElement.disabled = false;
|
||||
loadingElement.classList.remove('btn-loading');
|
||||
} else {
|
||||
const overlay = loadingElement.querySelector('.loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
if (!response.ok) {
|
||||
const error = new Error(data.message || data.error || 'API request failed');
|
||||
error.status = response.status;
|
||||
error.data = data;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// Remove loading state
|
||||
if (loadingElement) {
|
||||
if (loadingElement.tagName === 'BUTTON') {
|
||||
loadingElement.disabled = false;
|
||||
loadingElement.classList.remove('btn-loading');
|
||||
} else {
|
||||
const overlay = loadingElement.querySelector('.loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error toast
|
||||
Toast.error(error.message || 'An error occurred');
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {HTMLElement} loadingElement - Element to show loading state on
|
||||
* @returns {Promise} - The fetch promise
|
||||
*/
|
||||
async get(url, loadingElement = null) {
|
||||
return this.request(url, { method: 'GET' }, loadingElement);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} data - The data to send
|
||||
* @param {HTMLElement} loadingElement - Element to show loading state on
|
||||
* @returns {Promise} - The fetch promise
|
||||
*/
|
||||
async post(url, data, loadingElement = null) {
|
||||
return this.request(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
},
|
||||
loadingElement
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {Object} data - The data to send
|
||||
* @param {HTMLElement} loadingElement - Element to show loading state on
|
||||
* @returns {Promise} - The fetch promise
|
||||
*/
|
||||
async put(url, data, loadingElement = null) {
|
||||
return this.request(
|
||||
url,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
},
|
||||
loadingElement
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
* @param {string} url - The API endpoint URL
|
||||
* @param {HTMLElement} loadingElement - Element to show loading state on
|
||||
* @returns {Promise} - The fetch promise
|
||||
*/
|
||||
async delete(url, loadingElement = null) {
|
||||
return this.request(url, { method: 'DELETE' }, loadingElement);
|
||||
}
|
||||
};
|
||||
|
||||
// Modal utilities
|
||||
const Modal = {
|
||||
/**
|
||||
* Open a modal
|
||||
* @param {string} modalId - The ID of the modal to open
|
||||
*/
|
||||
open(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden'; // Prevent scrolling
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close a modal
|
||||
* @param {string} modalId - The ID of the modal to close
|
||||
*/
|
||||
close(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = ''; // Restore scrolling
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize modal close buttons
|
||||
*/
|
||||
init() {
|
||||
// Close modal when clicking the close button
|
||||
document.querySelectorAll('.modal-close').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const modal = button.closest('.modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = ''; // Restore scrolling
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside the modal content
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.addEventListener('click', (event) => {
|
||||
if (event.target === modal) {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = ''; // Restore scrolling
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Form utilities
|
||||
const Form = {
|
||||
/**
|
||||
* Serialize form data to an object
|
||||
* @param {HTMLFormElement} form - The form element
|
||||
* @returns {Object} - The serialized form data
|
||||
*/
|
||||
serialize(form) {
|
||||
const formData = new FormData(form);
|
||||
const data = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
// Handle checkboxes
|
||||
if (form.elements[key].type === 'checkbox') {
|
||||
data[key] = value === 'on';
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Populate a form with data
|
||||
* @param {HTMLFormElement} form - The form element
|
||||
* @param {Object} data - The data to populate the form with
|
||||
*/
|
||||
populate(form, data) {
|
||||
for (const key in data) {
|
||||
if (form.elements[key]) {
|
||||
const element = form.elements[key];
|
||||
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = Boolean(data[key]);
|
||||
} else if (element.type === 'radio') {
|
||||
const radio = form.querySelector(`input[name="${key}"][value="${data[key]}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
}
|
||||
} else {
|
||||
element.value = data[key];
|
||||
}
|
||||
|
||||
// Trigger change event for elements like select
|
||||
const event = new Event('change');
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate a form
|
||||
* @param {HTMLFormElement} form - The form element
|
||||
* @returns {boolean} - Whether the form is valid
|
||||
*/
|
||||
validate(form) {
|
||||
let isValid = true;
|
||||
|
||||
// Remove existing error messages
|
||||
form.querySelectorAll('.form-error').forEach(error => {
|
||||
error.remove();
|
||||
});
|
||||
|
||||
// Check required fields
|
||||
form.querySelectorAll('[required]').forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
isValid = false;
|
||||
this.showError(field, 'This field is required');
|
||||
}
|
||||
});
|
||||
|
||||
// Check email fields
|
||||
form.querySelectorAll('input[type="email"]').forEach(field => {
|
||||
if (field.value && !this.isValidEmail(field.value)) {
|
||||
isValid = false;
|
||||
this.showError(field, 'Please enter a valid email address');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show an error message for a form field
|
||||
* @param {HTMLElement} field - The form field
|
||||
* @param {string} message - The error message
|
||||
*/
|
||||
showError(field, message) {
|
||||
// Create error message element
|
||||
const error = document.createElement('div');
|
||||
error.className = 'form-error';
|
||||
error.textContent = message;
|
||||
error.style.color = 'var(--danger-color)';
|
||||
error.style.fontSize = '0.875rem';
|
||||
error.style.marginTop = '0.25rem';
|
||||
|
||||
// Add error class to field
|
||||
field.classList.add('is-invalid');
|
||||
|
||||
// Insert error after field
|
||||
field.parentNode.insertBefore(error, field.nextSibling);
|
||||
|
||||
// Add event listener to remove error when field is changed
|
||||
field.addEventListener('input', () => {
|
||||
field.classList.remove('is-invalid');
|
||||
if (error.parentNode) {
|
||||
error.parentNode.removeChild(error);
|
||||
}
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an email is valid
|
||||
* @param {string} email - The email to check
|
||||
* @returns {boolean} - Whether the email is valid
|
||||
*/
|
||||
isValidEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
};
|
||||
|
||||
// DOM utilities
|
||||
const DOM = {
|
||||
/**
|
||||
* Create an element with attributes and children
|
||||
* @param {string} tag - The tag name
|
||||
* @param {Object} attrs - The attributes
|
||||
* @param {Array|string} children - The children
|
||||
* @returns {HTMLElement} - The created element
|
||||
*/
|
||||
createElement(tag, attrs = {}, children = []) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
// Set attributes
|
||||
for (const key in attrs) {
|
||||
if (key === 'className') {
|
||||
element.className = attrs[key];
|
||||
} else if (key === 'style' && typeof attrs[key] === 'object') {
|
||||
Object.assign(element.style, attrs[key]);
|
||||
} else if (key.startsWith('on') && typeof attrs[key] === 'function') {
|
||||
const eventName = key.substring(2).toLowerCase();
|
||||
element.addEventListener(eventName, attrs[key]);
|
||||
} else {
|
||||
element.setAttribute(key, attrs[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add children
|
||||
if (Array.isArray(children)) {
|
||||
children.forEach(child => {
|
||||
if (typeof child === 'string') {
|
||||
element.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
element.appendChild(child);
|
||||
}
|
||||
});
|
||||
} else if (typeof children === 'string') {
|
||||
element.textContent = children;
|
||||
}
|
||||
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a loading spinner
|
||||
* @param {string} size - The size of the spinner (sm, md, lg)
|
||||
* @returns {HTMLElement} - The spinner element
|
||||
*/
|
||||
createSpinner(size = 'md') {
|
||||
const spinner = document.createElement('div');
|
||||
spinner.className = `loading-spinner loading-spinner-${size}`;
|
||||
return spinner;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a loading spinner in a container
|
||||
* @param {HTMLElement} container - The container element
|
||||
* @param {string} size - The size of the spinner
|
||||
* @returns {HTMLElement} - The spinner container element
|
||||
*/
|
||||
showSpinner(container, size = 'md') {
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create spinner container
|
||||
const spinnerContainer = document.createElement('div');
|
||||
spinnerContainer.className = 'loading-spinner-container';
|
||||
|
||||
// Create spinner
|
||||
const spinner = this.createSpinner(size);
|
||||
spinnerContainer.appendChild(spinner);
|
||||
|
||||
// Add to container
|
||||
container.appendChild(spinnerContainer);
|
||||
|
||||
return spinnerContainer;
|
||||
}
|
||||
};
|
||||
|
||||
// Export utilities
|
||||
window.Toast = Toast;
|
||||
window.API = API;
|
||||
window.Modal = Modal;
|
||||
window.Form = Form;
|
||||
window.DOM = DOM;
|
@ -1,9 +1,22 @@
|
||||
// This file is kept for backward compatibility
|
||||
// It will load the new modular JS files
|
||||
|
||||
// Load the utility functions
|
||||
const utilsScript = document.createElement('script');
|
||||
utilsScript.src = 'js/utils.js';
|
||||
document.head.appendChild(utilsScript);
|
||||
|
||||
// Load the main script
|
||||
const mainScript = document.createElement('script');
|
||||
mainScript.src = 'js/main.js';
|
||||
document.head.appendChild(mainScript);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Auth elements
|
||||
const loginButton = document.getElementById('login-button');
|
||||
const logoutButton = document.getElementById('logout-button');
|
||||
const authSection = document.getElementById('auth-section');
|
||||
const dashboardSection = document.getElementById('dashboard-section');
|
||||
const dashboardSection = document.getElementById('dashboard-container');
|
||||
const usernameSpan = document.getElementById('username');
|
||||
|
||||
// Navigation elements
|
||||
|
@ -1,11 +1,20 @@
|
||||
/* This file is kept for backward compatibility */
|
||||
/* It imports the new modular CSS files */
|
||||
|
||||
@import url('css/main.css');
|
||||
@import url('css/components.css');
|
||||
@import url('css/layout.css');
|
||||
|
||||
/* Legacy styles below for backward compatibility */
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 2em;
|
||||
background-color: #f4f4f4;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
background-color: var(--light-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
color: #333;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
#dashboard-section {
|
||||
|
Loading…
x
Reference in New Issue
Block a user