2025-01-09 10:21:26 +03:30
|
|
|
import './bootstrap.js';
|
|
|
|
|
/*
|
|
|
|
|
* Welcome to your app's main JavaScript file!
|
|
|
|
|
*
|
|
|
|
|
* We recommend including the built version of this JavaScript file
|
|
|
|
|
* (and its CSS file) in your base layout (base.html.twig).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// any CSS you import will output into a single css file (app.css in this case)
|
2025-09-05 11:52:08 +03:30
|
|
|
import './styles/tailwind.css';
|
2025-01-09 10:21:26 +03:30
|
|
|
import './styles/app.css';
|
|
|
|
|
|
2025-09-05 11:52:08 +03:30
|
|
|
// Notification System
|
|
|
|
|
class NotificationSystem {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.container = null;
|
|
|
|
|
this.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
// Create notification container if it doesn't exist
|
|
|
|
|
this.container = document.getElementById('notification-container');
|
|
|
|
|
if (!this.container) {
|
|
|
|
|
this.container = document.createElement('div');
|
|
|
|
|
this.container.id = 'notification-container';
|
|
|
|
|
this.container.className = 'notification-container';
|
|
|
|
|
document.body.appendChild(this.container);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
show(message, type = 'info', title = '', duration = 5000) {
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
notification.className = `notification ${type}`;
|
|
|
|
|
|
|
|
|
|
const icon = this.getIcon(type);
|
|
|
|
|
const closeIcon = this.getCloseIcon();
|
|
|
|
|
|
|
|
|
|
notification.innerHTML = `
|
|
|
|
|
<div class="notification-icon">${icon}</div>
|
|
|
|
|
<div class="notification-content">
|
|
|
|
|
${title ? `<div class="notification-title">${title}</div>` : ''}
|
|
|
|
|
<div class="notification-message">${message}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="notification-close" onclick="this.parentElement.remove()">${closeIcon}</button>
|
|
|
|
|
<div class="notification-progress" style="width: 100%; animation: progress ${duration}ms linear forwards;"></div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Add progress bar animation
|
|
|
|
|
const style = document.createElement('style');
|
|
|
|
|
style.textContent = `
|
|
|
|
|
@keyframes progress {
|
|
|
|
|
from { width: 100%; }
|
|
|
|
|
to { width: 0%; }
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
|
|
|
|
this.container.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
// Trigger animation
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
notification.classList.add('show');
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
|
|
// Auto remove
|
|
|
|
|
if (duration > 0) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.remove(notification);
|
|
|
|
|
}, duration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return notification;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getIcon(type) {
|
|
|
|
|
const icons = {
|
|
|
|
|
success: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
|
|
|
</svg>`,
|
|
|
|
|
error: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
|
|
|
</svg>`,
|
|
|
|
|
warning: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
|
|
|
</svg>`,
|
|
|
|
|
info: `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
|
|
|
</svg>`
|
|
|
|
|
};
|
|
|
|
|
return icons[type] || icons.info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCloseIcon() {
|
|
|
|
|
return `<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
|
|
|
</svg>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remove(notification) {
|
|
|
|
|
if (notification && notification.parentElement) {
|
|
|
|
|
notification.classList.remove('show');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (notification.parentElement) {
|
|
|
|
|
notification.remove();
|
|
|
|
|
}
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
success(message, title = 'موفقیت') {
|
|
|
|
|
return this.show(message, 'success', title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error(message, title = 'خطا') {
|
|
|
|
|
return this.show(message, 'error', title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warning(message, title = 'هشدار') {
|
|
|
|
|
return this.show(message, 'warning', title);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info(message, title = 'اطلاعات') {
|
|
|
|
|
return this.show(message, 'info', title);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize notification system
|
|
|
|
|
const notification = new NotificationSystem();
|
|
|
|
|
|
|
|
|
|
// Make notification available globally
|
|
|
|
|
window.notification = notification;
|
|
|
|
|
|
|
|
|
|
// Test notification system
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (window.notification) {
|
|
|
|
|
console.log('Notification system initialized successfully');
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
// Blog functionality
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
// Blog search enhancement
|
|
|
|
|
const searchInput = document.querySelector('input[name="search"]');
|
|
|
|
|
if (searchInput) {
|
|
|
|
|
searchInput.addEventListener('focus', function() {
|
|
|
|
|
this.classList.add('blog-search-input');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
searchInput.addEventListener('blur', function() {
|
|
|
|
|
this.classList.remove('blog-search-input');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Blog card hover effects
|
|
|
|
|
const blogCards = document.querySelectorAll('.group.bg-white.rounded-2xl');
|
|
|
|
|
blogCards.forEach(card => {
|
|
|
|
|
card.classList.add('blog-card-hover');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Pagination hover effects
|
|
|
|
|
const paginationItems = document.querySelectorAll('nav a, nav span');
|
|
|
|
|
paginationItems.forEach(item => {
|
|
|
|
|
item.classList.add('pagination-item');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Smooth scroll for anchor links
|
|
|
|
|
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
|
|
|
|
anchorLinks.forEach(link => {
|
|
|
|
|
link.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const target = document.querySelector(this.getAttribute('href'));
|
|
|
|
|
if (target) {
|
|
|
|
|
target.scrollIntoView({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
block: 'start'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Lazy loading for images
|
|
|
|
|
const images = document.querySelectorAll('img[data-src]');
|
|
|
|
|
const imageObserver = new IntersectionObserver((entries, observer) => {
|
|
|
|
|
entries.forEach(entry => {
|
|
|
|
|
if (entry.isIntersecting) {
|
|
|
|
|
const img = entry.target;
|
|
|
|
|
img.src = img.dataset.src;
|
|
|
|
|
img.classList.remove('blog-loading');
|
|
|
|
|
img.classList.add('opacity-100');
|
|
|
|
|
observer.unobserve(img);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
images.forEach(img => {
|
|
|
|
|
img.classList.add('blog-loading', 'opacity-0');
|
|
|
|
|
imageObserver.observe(img);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reading progress bar for blog posts
|
|
|
|
|
const blogPost = document.querySelector('main.min-h-screen.bg-gray-50');
|
|
|
|
|
if (blogPost) {
|
|
|
|
|
const progressBar = document.createElement('div');
|
|
|
|
|
progressBar.className = 'fixed top-0 left-0 w-0 h-1 bg-gradient-to-r from-blue-500 to-purple-500 z-50 transition-all duration-300';
|
|
|
|
|
document.body.appendChild(progressBar);
|
|
|
|
|
|
|
|
|
|
window.addEventListener('scroll', function() {
|
|
|
|
|
const scrollTop = window.pageYOffset;
|
|
|
|
|
const docHeight = document.body.scrollHeight - window.innerHeight;
|
|
|
|
|
const scrollPercent = (scrollTop / docHeight) * 100;
|
|
|
|
|
progressBar.style.width = scrollPercent + '%';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy code blocks functionality
|
|
|
|
|
const codeBlocks = document.querySelectorAll('pre code');
|
|
|
|
|
codeBlocks.forEach(block => {
|
|
|
|
|
const copyButton = document.createElement('button');
|
|
|
|
|
copyButton.className = 'absolute top-2 left-2 px-2 py-1 text-xs bg-gray-800 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200';
|
|
|
|
|
copyButton.textContent = 'کپی';
|
|
|
|
|
|
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
|
wrapper.className = 'relative group';
|
|
|
|
|
wrapper.style.position = 'relative';
|
|
|
|
|
|
|
|
|
|
block.parentNode.insertBefore(wrapper, block);
|
|
|
|
|
wrapper.appendChild(block);
|
|
|
|
|
wrapper.appendChild(copyButton);
|
|
|
|
|
|
|
|
|
|
copyButton.addEventListener('click', function() {
|
|
|
|
|
navigator.clipboard.writeText(block.textContent).then(() => {
|
|
|
|
|
copyButton.textContent = 'کپی شد!';
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
copyButton.textContent = 'کپی';
|
|
|
|
|
}, 2000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Q&A functionality
|
|
|
|
|
// Add hover effects to Q&A cards
|
|
|
|
|
const qaCards = document.querySelectorAll('.bg-white.rounded-2xl.shadow-soft');
|
|
|
|
|
qaCards.forEach(card => {
|
|
|
|
|
card.classList.add('qa-card-hover');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add hover effects to Q&A tags
|
|
|
|
|
const qaTags = document.querySelectorAll('.inline-flex.items-center.px-3.py-1');
|
|
|
|
|
qaTags.forEach(tag => {
|
|
|
|
|
tag.classList.add('qa-tag-hover');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add focus effects to Q&A form inputs
|
|
|
|
|
const qaInputs = document.querySelectorAll('input, textarea, select');
|
|
|
|
|
qaInputs.forEach(input => {
|
|
|
|
|
input.classList.add('qa-form-focus');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add hover effects to Q&A pagination
|
|
|
|
|
const qaPaginationItems = document.querySelectorAll('nav a, nav span');
|
|
|
|
|
qaPaginationItems.forEach(item => {
|
|
|
|
|
item.classList.add('qa-pagination-item');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add stagger animation to Q&A cards
|
|
|
|
|
qaCards.forEach((card, index) => {
|
|
|
|
|
card.classList.add('qa-fade-in-up');
|
|
|
|
|
if (index < 4) {
|
|
|
|
|
card.classList.add(`qa-stagger-${index + 1}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Q&A search enhancement
|
|
|
|
|
const qaSearchInput = document.querySelector('input[name="search"]');
|
|
|
|
|
if (qaSearchInput) {
|
|
|
|
|
qaSearchInput.addEventListener('focus', function() {
|
|
|
|
|
this.parentElement.classList.add('ring-2', 'ring-blue-500');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
qaSearchInput.addEventListener('blur', function() {
|
|
|
|
|
this.parentElement.classList.remove('ring-2', 'ring-blue-500');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Q&A filter enhancement
|
|
|
|
|
const qaFilterSelect = document.querySelector('select[name="filter"]');
|
|
|
|
|
if (qaFilterSelect) {
|
|
|
|
|
qaFilterSelect.addEventListener('change', function() {
|
|
|
|
|
this.classList.add('bg-blue-50', 'border-blue-300');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.classList.remove('bg-blue-50', 'border-blue-300');
|
|
|
|
|
}, 300);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Q&A vote button enhancement
|
|
|
|
|
const qaVoteButtons = document.querySelectorAll('.vote-btn');
|
|
|
|
|
qaVoteButtons.forEach(button => {
|
|
|
|
|
button.classList.add('qa-vote-btn');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Q&A tag selection enhancement
|
|
|
|
|
const qaTagSuggestions = document.querySelectorAll('.tag-suggestion');
|
|
|
|
|
qaTagSuggestions.forEach(tag => {
|
|
|
|
|
tag.addEventListener('click', function() {
|
|
|
|
|
this.classList.toggle('qa-tag-selected');
|
|
|
|
|
this.classList.toggle('qa-tag-unselected');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Q&A answer acceptance enhancement
|
|
|
|
|
const qaAcceptButtons = document.querySelectorAll('.accept-answer-btn');
|
|
|
|
|
qaAcceptButtons.forEach(button => {
|
|
|
|
|
button.addEventListener('click', function() {
|
|
|
|
|
this.classList.add('bg-green-200', 'text-green-800');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.classList.remove('bg-green-200', 'text-green-800');
|
|
|
|
|
}, 1000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Q&A loading states
|
|
|
|
|
const qaForms = document.querySelectorAll('form');
|
|
|
|
|
qaForms.forEach(form => {
|
|
|
|
|
form.addEventListener('submit', function() {
|
|
|
|
|
const submitButton = this.querySelector('button[type="submit"]');
|
|
|
|
|
if (submitButton) {
|
|
|
|
|
submitButton.classList.add('qa-loading-shimmer');
|
|
|
|
|
submitButton.disabled = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|