Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
262
static/js/shared-components.js
Normal file
262
static/js/shared-components.js
Normal file
@@ -0,0 +1,262 @@
|
||||
// Shared UI Components and Functionality
|
||||
// This file contains reusable Vue composition functions and utilities
|
||||
// that can be used across multiple pages (index, inquire, admin, etc.)
|
||||
|
||||
// Dark Mode Composition
|
||||
function useDarkMode() {
|
||||
const isDarkMode = Vue.ref(false);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
isDarkMode.value = !isDarkMode.value;
|
||||
if (isDarkMode.value) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('darkMode', 'true');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('darkMode', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
const initializeDarkMode = () => {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const savedMode = localStorage.getItem('darkMode');
|
||||
if (savedMode === 'true' || (savedMode === null && prefersDark)) {
|
||||
isDarkMode.value = true;
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
isDarkMode.value = false;
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isDarkMode,
|
||||
toggleDarkMode,
|
||||
initializeDarkMode
|
||||
};
|
||||
}
|
||||
|
||||
// Color Scheme Composition
|
||||
function useColorScheme() {
|
||||
const showColorSchemeModal = Vue.ref(false);
|
||||
const currentColorScheme = Vue.ref('blue');
|
||||
const isDarkMode = Vue.ref(false);
|
||||
|
||||
const colorSchemes = {
|
||||
light: [
|
||||
{ id: 'blue', name: 'Ocean Blue', description: 'Classic blue theme with professional appeal', accent: '#3b82f6', hover: '#2563eb' },
|
||||
{ id: 'emerald', name: 'Forest Emerald', description: 'Fresh green theme for a natural feel', accent: '#10b981', hover: '#059669' },
|
||||
{ id: 'purple', name: 'Royal Purple', description: 'Elegant purple theme with sophistication', accent: '#8b5cf6', hover: '#7c3aed' },
|
||||
{ id: 'rose', name: 'Sunset Rose', description: 'Warm pink theme with gentle energy', accent: '#f43f5e', hover: '#e11d48' },
|
||||
{ id: 'amber', name: 'Golden Amber', description: 'Warm yellow theme for brightness', accent: '#f59e0b', hover: '#d97706' },
|
||||
{ id: 'teal', name: 'Ocean Teal', description: 'Cool teal theme for tranquility', accent: '#06b6d4', hover: '#0891b2' }
|
||||
],
|
||||
dark: [
|
||||
{ id: 'blue', name: 'Midnight Blue', description: 'Deep blue for focused night work', accent: '#60a5fa', hover: '#3b82f6' },
|
||||
{ id: 'emerald', name: 'Emerald Night', description: 'Rich green for comfortable viewing', accent: '#34d399', hover: '#10b981' },
|
||||
{ id: 'purple', name: 'Deep Purple', description: 'Luxurious purple for creative sessions', accent: '#a78bfa', hover: '#8b5cf6' },
|
||||
{ id: 'rose', name: 'Crimson', description: 'Bold red-pink for energetic work', accent: '#fb7185', hover: '#f43f5e' },
|
||||
{ id: 'amber', name: 'Golden Hour', description: 'Warm amber for reduced eye strain', accent: '#fbbf24', hover: '#f59e0b' },
|
||||
{ id: 'teal', name: 'Electric Cyan', description: 'Vibrant cyan for modern aesthetics', accent: '#22d3ee', hover: '#06b6d4' }
|
||||
]
|
||||
};
|
||||
|
||||
const applyColorScheme = (schemeId) => {
|
||||
const schemes = isDarkMode.value ? colorSchemes.dark : colorSchemes.light;
|
||||
const scheme = schemes.find(s => s.id === schemeId);
|
||||
if (scheme) {
|
||||
// Remove all theme classes
|
||||
const allThemeClasses = [
|
||||
...colorSchemes.light.map(s => `theme-light-${s.id}`),
|
||||
...colorSchemes.dark.map(s => `theme-dark-${s.id}`)
|
||||
].filter(c => !c.includes('blue')); // blue is the default, no class needed
|
||||
|
||||
document.documentElement.classList.remove(...allThemeClasses);
|
||||
|
||||
// Apply new theme class if not blue (default)
|
||||
if (schemeId !== 'blue') {
|
||||
const themeClass = `theme-${isDarkMode.value ? 'dark' : 'light'}-${schemeId}`;
|
||||
document.documentElement.classList.add(themeClass);
|
||||
}
|
||||
|
||||
// Don't set CSS variables - let the theme classes handle all colors
|
||||
localStorage.setItem('colorScheme', schemeId);
|
||||
currentColorScheme.value = schemeId;
|
||||
}
|
||||
};
|
||||
|
||||
const initializeColorScheme = (darkMode) => {
|
||||
isDarkMode.value = darkMode;
|
||||
const savedScheme = localStorage.getItem('colorScheme') || 'blue';
|
||||
currentColorScheme.value = savedScheme;
|
||||
applyColorScheme(savedScheme);
|
||||
};
|
||||
|
||||
// Watch for dark mode changes and reapply color scheme
|
||||
Vue.watch(() => isDarkMode.value, (newValue) => {
|
||||
applyColorScheme(currentColorScheme.value);
|
||||
});
|
||||
|
||||
const openColorSchemeModal = () => {
|
||||
showColorSchemeModal.value = true;
|
||||
};
|
||||
|
||||
const closeColorSchemeModal = () => {
|
||||
showColorSchemeModal.value = false;
|
||||
};
|
||||
|
||||
const selectColorScheme = (schemeId) => {
|
||||
applyColorScheme(schemeId);
|
||||
const scheme = colorSchemes[isDarkMode.value ? 'dark' : 'light'].find(s => s.id === schemeId);
|
||||
if (window.showToast && scheme) {
|
||||
window.showToast(`Applied ${scheme.name} theme`, 'fa-palette');
|
||||
}
|
||||
};
|
||||
|
||||
const resetColorScheme = () => {
|
||||
applyColorScheme('blue');
|
||||
if (window.showToast) {
|
||||
const defaultScheme = colorSchemes[isDarkMode.value ? 'dark' : 'light'].find(s => s.id === 'blue');
|
||||
window.showToast(`Reset to default ${defaultScheme?.name || 'Ocean Blue'} theme`, 'fa-undo');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
showColorSchemeModal,
|
||||
currentColorScheme,
|
||||
colorSchemes,
|
||||
openColorSchemeModal,
|
||||
closeColorSchemeModal,
|
||||
selectColorScheme,
|
||||
resetColorScheme,
|
||||
applyColorScheme,
|
||||
initializeColorScheme
|
||||
};
|
||||
}
|
||||
|
||||
// Shared Transcripts Modal Composition
|
||||
function useSharesModal() {
|
||||
const showSharesListModal = Vue.ref(false);
|
||||
const userShares = Vue.ref([]);
|
||||
const isLoadingShares = Vue.ref(false);
|
||||
|
||||
const openSharesList = async () => {
|
||||
isLoadingShares.value = true;
|
||||
showSharesListModal.value = true;
|
||||
try {
|
||||
const response = await fetch('/api/shares');
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to load shared items');
|
||||
userShares.value = data;
|
||||
} catch (error) {
|
||||
if (window.setGlobalError) {
|
||||
window.setGlobalError(`Failed to load shared items: ${error.message}`);
|
||||
} else {
|
||||
console.error('Failed to load shared items:', error);
|
||||
}
|
||||
} finally {
|
||||
isLoadingShares.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeSharesList = () => {
|
||||
showSharesListModal.value = false;
|
||||
};
|
||||
|
||||
const copyShareLink = async (shareId) => {
|
||||
const url = `${window.location.origin}/share/${shareId}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
if (window.showToast) {
|
||||
window.showToast('Share link copied to clipboard', 'fa-link');
|
||||
}
|
||||
} catch (err) {
|
||||
if (window.setGlobalError) {
|
||||
window.setGlobalError('Failed to copy link to clipboard');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteShare = async (shareId) => {
|
||||
if (!confirm('Are you sure you want to delete this share?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/shares/${shareId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to delete share');
|
||||
}
|
||||
|
||||
userShares.value = userShares.value.filter(share => share.id !== shareId);
|
||||
if (window.showToast) {
|
||||
window.showToast('Share deleted successfully', 'fa-trash');
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.setGlobalError) {
|
||||
window.setGlobalError(`Failed to delete share: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
showSharesListModal,
|
||||
userShares,
|
||||
isLoadingShares,
|
||||
openSharesList,
|
||||
closeSharesList,
|
||||
copyShareLink,
|
||||
deleteShare
|
||||
};
|
||||
}
|
||||
|
||||
// User Menu Composition
|
||||
function useUserMenu() {
|
||||
const isUserMenuOpen = Vue.ref(false);
|
||||
|
||||
const toggleUserMenu = () => {
|
||||
isUserMenuOpen.value = !isUserMenuOpen.value;
|
||||
};
|
||||
|
||||
const closeUserMenu = () => {
|
||||
isUserMenuOpen.value = false;
|
||||
};
|
||||
|
||||
// Close menu when clicking outside
|
||||
Vue.onMounted(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
const userMenuButton = e.target.closest('button[class*="flex items-center gap"]');
|
||||
const userMenuDropdown = e.target.closest('div[class*="absolute right-0"]');
|
||||
const isUserMenuButtonClick = userMenuButton && userMenuButton.querySelector('i.fa-user-circle');
|
||||
|
||||
if (!isUserMenuButtonClick && !userMenuDropdown) {
|
||||
isUserMenuOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
Vue.onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
isUserMenuOpen,
|
||||
toggleUserMenu,
|
||||
closeUserMenu
|
||||
};
|
||||
}
|
||||
|
||||
// Export for use in Vue components
|
||||
window.SharedComponents = {
|
||||
useDarkMode,
|
||||
useColorScheme,
|
||||
useSharesModal,
|
||||
useUserMenu
|
||||
};
|
||||
Reference in New Issue
Block a user