initiales bauen mit mistral
This commit is contained in:
1 parent
7e2e8bdf59
commit
c74bca1e13
22 files changed
+5642
No files matched your search
@@ -0,0 +1,357 @@
|
||||
/* =============================================================================
|
||||
Formeleon - Multi-tenant Form Generator
|
||||
Author: Björn Runden <br@runden.net>
|
||||
Script: style.css
|
||||
Description: Custom styles for Formeleon admin interface.
|
||||
=============================================================================
|
||||
*/
|
||||
|
||||
/* Global Styles */
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Card Styles */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
/* Badge Styles */
|
||||
.badge {
|
||||
font-weight: 500;
|
||||
padding: 0.35em 0.65em;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-outline-* {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
/* Form Control Styles */
|
||||
.form-control, .form-select {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 0.75rem 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Dev Mode Banner */
|
||||
.dev-mode-banner {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(90deg, #ffc107, #ff8c00);
|
||||
color: #000;
|
||||
padding: 12px 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dev-mode-banner i {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* NavBar Styles */
|
||||
.navbar {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
border: none;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.5rem 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-item:hover, .dropdown-item:focus {
|
||||
background-color: #f8f9fa;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Input Group Styles */
|
||||
.input-group-text {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* Alert Styles */
|
||||
.alert {
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Code Blocks */
|
||||
code {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #dee2e6;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 3rem !important;
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: calc(1.525rem + 3.3vw);
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-collapse {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: static;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.dev-mode-banner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Form Builder Specific Styles */
|
||||
.form-builder {
|
||||
background: #fff;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
border: 1px dashed #ced4da;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-field:hover {
|
||||
border-color: #80bdff;
|
||||
background-color: rgba(0, 123, 255, 0.05);
|
||||
}
|
||||
|
||||
.form-field-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* JSON Editor Styles */
|
||||
.json-editor {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Status Badge Colors */
|
||||
.bg-success {
|
||||
background-color: #198754 !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #ffc107 !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: #0dcaf0 !important;
|
||||
}
|
||||
|
||||
/* Toolbar Styles */
|
||||
.toolbar {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-content {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
/* Pagination Styles */
|
||||
.pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: #0d6efd;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/** =============================================================================
|
||||
* Formeleon - Multi-tenant Form Generator
|
||||
* Author: Björn Runden <br@runden.net>
|
||||
* Script: main.js
|
||||
* Description: Main JavaScript for Formeleon admin interface.
|
||||
* Handles common functionality, AJAX requests, and UI interactions.
|
||||
* =============================================================================
|
||||
*/
|
||||
|
||||
// Wait for DOM to be ready
|
||||
$(document).ready(function() {
|
||||
// Initialize tooltips
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
|
||||
// Initialize popovers
|
||||
$('[data-bs-toggle="popover"]').popover();
|
||||
|
||||
// Handle auto-dismiss alerts
|
||||
setTimeout(function() {
|
||||
$('.alert').alert('close');
|
||||
}, 5000);
|
||||
|
||||
// Form field management (for form builder)
|
||||
initFormBuilder();
|
||||
|
||||
// JSON editor initialization
|
||||
initJsonEditor();
|
||||
|
||||
// Search functionality
|
||||
initSearch();
|
||||
|
||||
// Confirmation for delete actions
|
||||
initDeleteConfirmation();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize form builder functionality
|
||||
*/
|
||||
function initFormBuilder() {
|
||||
// Add field button
|
||||
$(document).on('click', '.add-field', function(e) {
|
||||
e.preventDefault();
|
||||
const container = $(this).data('container');
|
||||
addFieldToContainer(container);
|
||||
});
|
||||
|
||||
// Remove field button
|
||||
$(document).on('click', '.remove-field', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).closest('.form-field').remove();
|
||||
});
|
||||
|
||||
// Field type change handler
|
||||
$(document).on('change', '.field-type-select', function() {
|
||||
const fieldDiv = $(this).closest('.form-field');
|
||||
const fieldType = $(this).val();
|
||||
updateFieldOptions(fieldDiv, fieldType);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new field to the form builder
|
||||
* @param {string} container - CSS selector for the container
|
||||
*/
|
||||
function addFieldToContainer(container) {
|
||||
const containerEl = $(container);
|
||||
const fieldCount = containerEl.find('.form-field').length + 1;
|
||||
|
||||
const fieldHtml = `
|
||||
<div class="form-field border p-3 mb-3 rounded">
|
||||
<div class="form-field-header d-flex justify-content-between mb-2">
|
||||
<h5>Field ${fieldCount}</h5>
|
||||
<div class="form-field-actions">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-field">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Field Name</label>
|
||||
<input type="text" class="form-control" name="fields[${fieldCount}][name]" placeholder="field_name">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Type</label>
|
||||
<select class="form-select field-type-select" name="fields[${fieldCount}][type]">
|
||||
<option value="text">Text</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="textarea">Textarea</option>
|
||||
<option value="select">Select</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
<option value="radio">Radio</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="file">File</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" class="form-control" name="fields[${fieldCount}][label]" placeholder="Field Label">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" name="fields[${fieldCount}][required]" value="true">
|
||||
<label class="form-check-label">Required</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Options (for select/radio/checkbox)</label>
|
||||
<input type="text" class="form-control" name="fields[${fieldCount}][options]" placeholder="option1,option2,option3">
|
||||
<div class="form-text">Comma-separated list of options</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
containerEl.append(fieldHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update field options based on field type
|
||||
* @param {jQuery} fieldDiv - The field div element
|
||||
* @param {string} fieldType - The selected field type
|
||||
*/
|
||||
function updateFieldOptions(fieldDiv, fieldType) {
|
||||
const optionsGroup = fieldDiv.find('.field-options');
|
||||
|
||||
// Remove existing options
|
||||
optionsGroup.remove();
|
||||
|
||||
// Add type-specific options
|
||||
switch(fieldType) {
|
||||
case 'select':
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
const optionsHtml = `
|
||||
<div class="field-options row mt-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Options</label>
|
||||
<textarea class="form-control" name="fields[][options]" placeholder="option1\noption2\noption3"></textarea>
|
||||
<div class="form-text">One option per line</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
fieldDiv.append(optionsHtml);
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
const textareaHtml = `
|
||||
<div class="field-options row mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Rows</label>
|
||||
<input type="number" class="form-control" name="fields[][rows]" value="3">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control" name="fields[][placeholder]" placeholder="Enter text here...">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
fieldDiv.append(textareaHtml);
|
||||
break;
|
||||
|
||||
default:
|
||||
// No additional options for basic field types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize JSON editor for form structure
|
||||
*/
|
||||
function initJsonEditor() {
|
||||
// Pretty print JSON in textarea
|
||||
$('.json-editor').each(function() {
|
||||
const textarea = $(this);
|
||||
try {
|
||||
const json = JSON.parse(textarea.val());
|
||||
textarea.val(JSON.stringify(json, null, 2));
|
||||
} catch (e) {
|
||||
// Not valid JSON, leave as is
|
||||
}
|
||||
});
|
||||
|
||||
// Format JSON button
|
||||
$(document).on('click', '.format-json', function(e) {
|
||||
e.preventDefault();
|
||||
const target = $(this).data('target');
|
||||
const textarea = $(target);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(textarea.val());
|
||||
textarea.val(JSON.stringify(json, null, 2));
|
||||
} catch (e) {
|
||||
alert('Invalid JSON: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Minify JSON button
|
||||
$(document).on('click', '.minify-json', function(e) {
|
||||
e.preventDefault();
|
||||
const target = $(this).data('target');
|
||||
const textarea = $(target);
|
||||
|
||||
try {
|
||||
const json = JSON.parse(textarea.val());
|
||||
textarea.val(JSON.stringify(json));
|
||||
} catch (e) {
|
||||
alert('Invalid JSON: ' + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize search functionality
|
||||
*/
|
||||
function initSearch() {
|
||||
// Table search
|
||||
$('#searchInput').on('input', function() {
|
||||
const searchTerm = $(this).val().toLowerCase();
|
||||
const table = $(this).closest('.card').find('table tbody tr');
|
||||
|
||||
table.each(function() {
|
||||
const text = $(this).text().toLowerCase();
|
||||
const visible = text.includes(searchTerm);
|
||||
$(this).toggle(visible);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize delete confirmation dialogs
|
||||
*/
|
||||
function initDeleteConfirmation() {
|
||||
// Already handled inline with onsubmit="return confirm(...)"
|
||||
// This is a fallback for more complex confirmations
|
||||
$(document).on('click', '.confirm-delete', function(e) {
|
||||
e.preventDefault();
|
||||
const message = $(this).data('confirm-message') || 'Are you sure you want to delete this item?';
|
||||
const form = $(this).closest('form');
|
||||
|
||||
if (confirm(message)) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX helper function
|
||||
* @param {string} url - URL to request
|
||||
* @param {string} method - HTTP method (GET, POST, PUT, DELETE)
|
||||
* @param {object} data - Data to send
|
||||
* @param {function} success - Success callback
|
||||
* @param {function} error - Error callback
|
||||
*/
|
||||
function ajaxRequest(url, method, data, success, error) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
method: method,
|
||||
data: data,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
success: function(response) {
|
||||
if (typeof success === 'function') {
|
||||
success(response);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, err) {
|
||||
if (typeof error === 'function') {
|
||||
error(xhr, status, err);
|
||||
} else {
|
||||
alert('Error: ' + (xhr.responseJSON?.message || err));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSRF token from meta tag or cookie
|
||||
* @return {string} CSRF token
|
||||
*/
|
||||
function getCsrfToken() {
|
||||
return $('meta[name="csrf-token"]').attr('content') ||
|
||||
getCookie('csrftoken') ||
|
||||
getCookie('csrf_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cookie by name
|
||||
* @param {string} name - Cookie name
|
||||
* @return {string} Cookie value or null
|
||||
*/
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading spinner
|
||||
* @param {string|jQuery} element - Element or selector to add spinner to
|
||||
*/
|
||||
function showLoading(element) {
|
||||
const el = $(element);
|
||||
el.append('<span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"></span>');
|
||||
el.attr('disabled', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading spinner
|
||||
* @param {string|jQuery} element - Element or selector to remove spinner from
|
||||
*/
|
||||
function hideLoading(element) {
|
||||
const el = $(element);
|
||||
el.find('.spinner-border').remove();
|
||||
el.removeAttr('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
* @param {string} dateString - ISO date string
|
||||
* @return {string} Formatted date
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes for display
|
||||
* @param {number} bytes - Number of bytes
|
||||
* @return {string} Formatted size
|
||||
*/
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random ID
|
||||
* @param {number} length - Length of ID (default: 8)
|
||||
* @return {string} Random ID
|
||||
*/
|
||||
function generateId(length = 8) {
|
||||
return Math.random().toString(36).substring(2, 2 + length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
* @param {function} callback - Optional callback
|
||||
*/
|
||||
function copyToClipboard(text, callback) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
if (typeof callback === 'function') {
|
||||
callback(true);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(false, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* @param {string} message - Message to display
|
||||
* @param {string} type - Type (success, error, warning, info)
|
||||
*/
|
||||
function showToast(message, type = 'info') {
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create toast container if it doesn't exist
|
||||
let container = $('.toast-container');
|
||||
if (container.length === 0) {
|
||||
container = $('<div class="toast-container position-fixed bottom-0 end-0 p-3"></div>');
|
||||
$('body').append(container);
|
||||
}
|
||||
|
||||
container.append(toastHtml);
|
||||
|
||||
const toastElement = $('#' + toastId);
|
||||
const toast = new bootstrap.Toast(toastElement[0], { autohide: true, delay: 3000 });
|
||||
toast.show();
|
||||
|
||||
// Remove toast after it's hidden
|
||||
toastElement.on('hidden.bs.toast', function() {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Export functions for use in other scripts
|
||||
window.Formeleon = {
|
||||
ajaxRequest: ajaxRequest,
|
||||
showLoading: showLoading,
|
||||
hideLoading: hideLoading,
|
||||
formatDate: formatDate,
|
||||
formatBytes: formatBytes,
|
||||
generateId: generateId,
|
||||
copyToClipboard: copyToClipboard,
|
||||
showToast: showToast,
|
||||
getCsrfToken: getCsrfToken
|
||||
};
|
||||
Reference in new issue
Block a user