mlstocks / frontend /src /components /IndicatorsModal.vue
github-actions[bot]
Deploy to Hugging Face Space
abf702c
<script setup>
import {
X,
ArrowLeft,
LineChart,
TrendingUp,
Activity,
BarChart3,
Layers,
ChevronDown,
ChevronUp,
RotateCcw,
Trash2,
Save,
Check
} from 'lucide-vue-next';
import { ref, onMounted, computed } from 'vue';
import { api } from '../services/api';
const props = defineProps({
isOpen: Boolean
});
const emit = defineEmits(['close']);
const categories = ref([
{
id: 'trend',
name: 'Trend Signals',
icon: TrendingUp,
color: 'text-blue-400',
indicators: [
{ id: 'sma', name: 'SMA (Moving Average)' },
{ id: 'ema', name: 'EMA (Exponential MA)' },
{ id: 'bb', name: 'Bollinger Bands' },
{ id: 'macd', name: 'MACD Convergence' },
{ id: 'ichimoku', name: 'Ichimoku Cloud' }
],
isOpen: true
},
{
id: 'momentum',
name: 'Momentum Logic',
icon: Activity,
color: 'text-emerald-400',
indicators: [
{ id: 'rsi', name: 'RSI Strength' },
{ id: 'stoch', name: 'Stochastic Osc' },
{ id: 'cci', name: 'Commodity Channel' },
{ id: 'williams', name: 'Williams %R' }
],
isOpen: false
},
{
id: 'volume',
name: 'Volume Matrices',
icon: BarChart3,
color: 'text-amber-400',
indicators: [
{ id: 'volume', name: 'Market Volume' },
{ id: 'obv', name: 'OBV Flow' },
{ id: 'vwap', name: 'VWAP Price' },
{ id: 'cmf', name: 'Chaikin Flow' }
],
isOpen: false
},
{
id: 'volatility',
name: 'Volatility Scope',
icon: Layers,
color: 'text-rose-400',
indicators: [
{ id: 'atr', name: 'ATR Range' },
{ id: 'keltner', name: 'Keltner Logic' },
{ id: 'donchian', name: 'Donchian Channels' }
],
isOpen: false
}
]);
const selectedIndicators = ref(new Set());
const isLoading = ref(false);
const isSaving = ref(false);
const fetchUserIndicators = async () => {
isLoading.value = true;
try {
const data = await api.get('/user/indicators');
selectedIndicators.value = new Set(data);
} catch (error) {
console.error('Error fetching indicators:', error);
} finally {
isLoading.value = false;
}
};
const saveIndicators = async () => {
isSaving.value = true;
try {
await api.post('/user/indicators', {
indicators: Array.from(selectedIndicators.value)
});
emit('close');
} catch (error) {
console.error('Error saving indicators:', error);
} finally {
isSaving.value = false;
}
};
const toggleIndicator = (id) => {
if (selectedIndicators.value.has(id)) {
selectedIndicators.value.delete(id);
} else {
selectedIndicators.value.add(id);
}
};
const resetToDefault = () => {
selectedIndicators.value = new Set(['sma', 'ema', 'rsi', 'volume']);
};
const clearAll = () => {
selectedIndicators.value.clear();
};
const getActiveCount = (category) => {
return category.indicators.filter(ind => selectedIndicators.value.has(ind.id)).length;
};
const totalActive = computed(() => selectedIndicators.value.size);
const totalAvailable = computed(() => {
return categories.value.reduce((acc, cat) => acc + cat.indicators.length, 0);
});
onMounted(() => {
fetchUserIndicators();
});
const toggleCategory = (catId) => {
const cat = categories.value.find(c => c.id === catId);
if (cat) cat.isOpen = !cat.isOpen;
};
</script>
<template>
<div v-if="isOpen" class="fixed inset-0 z-[100] flex items-center justify-center md:p-4 overflow-hidden">
<!-- Backdrop -->
<div class="absolute inset-0 bg-slate-950/95 backdrop-blur-md" @click="$emit('close')"></div>
<!-- Modal Container -->
<div class="relative w-full h-full md:h-auto md:max-w-2xl bg-[#0b1120] border-0 md:border md:border-slate-800 md:rounded-[2rem] shadow-2xl overflow-hidden flex flex-col md:max-h-[90vh]">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-5 md:py-8 border-b border-slate-800 bg-slate-900/40">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-blue-600/10 rounded-xl flex items-center justify-center border border-blue-500/20 md:hidden">
<Activity class="w-5 h-5 text-blue-500" />
</div>
<div>
<h2 class="text-xl md:text-2xl font-black text-white tracking-tight">Feature Vault</h2>
<p class="text-[9px] md:text-[10px] text-slate-500 font-bold uppercase tracking-[0.2em] mt-0.5">Vector Analysis Preferences</p>
</div>
</div>
<button @click="$emit('close')" class="p-2.5 bg-slate-800 hover:bg-slate-700 rounded-xl transition-all">
<X class="w-5 h-5 text-slate-300" />
</button>
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-5 md:p-10 space-y-8 md:space-y-12">
<!-- Dashboard Summary -->
<div class="grid grid-cols-2 gap-4">
<div class="bg-indigo-600/5 border border-indigo-500/20 rounded-2xl p-5 md:p-6 group relative overflow-hidden">
<div class="absolute -right-2 -bottom-2 opacity-5 group-hover:opacity-10 transition-opacity">
<LineChart class="w-16 h-16 text-indigo-500" />
</div>
<div class="text-[9px] font-black text-indigo-400 uppercase tracking-widest mb-1">Active Vectors</div>
<div class="text-2xl font-black text-white leading-none">{{ totalActive }} <span class="text-xs text-slate-600">/ {{ totalAvailable }}</span></div>
</div>
<div class="bg-emerald-600/5 border border-emerald-500/20 rounded-2xl p-5 md:p-6 group relative overflow-hidden">
<div class="absolute -right-2 -bottom-2 opacity-5 group-hover:opacity-10 transition-opacity">
<Check class="w-16 h-16 text-emerald-500" />
</div>
<div class="text-[9px] font-black text-emerald-400 uppercase tracking-widest mb-1">Config Status</div>
<div class="text-2xl font-black text-white leading-none">{{ Math.round((totalActive/totalAvailable)*100) }}% <span class="text-xs text-slate-600">Enabled</span></div>
</div>
</div>
<!-- Scrollable Category Area -->
<div class="space-y-4">
<div v-for="cat in categories" :key="cat.id" class="border border-slate-800 rounded-2xl bg-slate-900/40 transition-all overflow-hidden" :class="cat.isOpen ? 'shadow-xl shadow-slate-950/50' : ''">
<div
@click="toggleCategory(cat.id)"
class="flex items-center justify-between p-4 md:p-6 cursor-pointer hover:bg-white/[0.02] transition-colors"
>
<div class="flex items-center gap-4">
<div :class="['w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-slate-950 shadow-inner', cat.color]">
<component :is="cat.icon" class="w-5 h-5" />
</div>
<div>
<span class="font-black text-sm md:text-lg text-white tracking-tight">{{ cat.name }}</span>
<div v-if="getActiveCount(cat) > 0" class="text-[8px] font-black text-emerald-500 uppercase tracking-[0.2em] mt-0.5">Synced</div>
</div>
</div>
<div class="flex items-center gap-4">
<div v-if="getActiveCount(cat) > 0" class="w-6 h-6 rounded-full bg-emerald-500/10 flex items-center justify-center border border-emerald-500/30">
<span class="text-[10px] font-bold text-emerald-400">{{ getActiveCount(cat) }}</span>
</div>
<ChevronDown class="w-5 h-5 text-slate-600 transition-transform duration-300" :class="cat.isOpen ? 'rotate-180' : ''" />
</div>
</div>
<div v-if="cat.isOpen" class="px-2 pb-2 grid grid-cols-1 sm:grid-cols-2 gap-1 animate-in fade-in transition-all">
<div
v-for="ind in cat.indicators"
:key="ind.id"
@click="toggleIndicator(ind.id)"
class="flex items-center justify-between p-4 rounded-xl cursor-pointer transition-all border-2"
:class="selectedIndicators.has(ind.id) ? 'bg-blue-600/10 border-blue-500/50' : 'bg-transparent border-transparent hover:bg-white/5'"
>
<span class="text-xs md:text-sm font-bold" :class="selectedIndicators.has(ind.id) ? 'text-white' : 'text-slate-500'">
{{ ind.name }}
</span>
<div class="w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all"
:class="selectedIndicators.has(ind.id) ? 'bg-blue-600 border-blue-500 shadow-lg shadow-blue-500/20' : 'border-slate-800 bg-slate-950 opacity-40'">
<Check v-if="selectedIndicators.has(ind.id)" class="w-4 h-4 text-white stroke-[3px]" />
</div>
</div>
</div>
</div>
</div>
<!-- Global Actions -->
<div class="flex flex-col sm:flex-row gap-3">
<button
@click="resetToDefault"
class="flex-1 flex items-center justify-center gap-3 py-3 md:py-4 bg-slate-900 hover:bg-slate-800 text-slate-400 font-black uppercase text-[10px] md:text-xs tracking-widest rounded-2xl transition-all border border-slate-800"
>
<RotateCcw class="w-4 h-4" /> Reset Config
</button>
<button
@click="clearAll"
class="flex-1 flex items-center justify-center gap-3 py-3 md:py-4 bg-slate-900 hover:bg-slate-800 text-slate-400 font-black uppercase text-[10px] md:text-xs tracking-widest rounded-2xl transition-all border border-slate-800"
>
<Trash2 class="w-4 h-4" /> Purge Vectors
</button>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-6 md:px-10 md:py-10 border-t border-slate-800 bg-slate-900/60">
<button
@click="saveIndicators"
:disabled="isSaving"
class="w-full flex items-center justify-center gap-4 py-4 md:py-6 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-base md:text-xl font-black rounded-2xl md:rounded-3xl transition-all shadow-xl shadow-blue-900/40 active:scale-95"
>
<Save v-if="!isSaving" class="w-5 h-5" />
<RotateCcw v-else class="w-6 h-6 animate-spin" />
{{ isSaving ? 'Committing Changes...' : 'Synchronize Vault' }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 10px; }
.animate-in {
animation-duration: 0.3s;
animation-fill-mode: both;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in { animation-name: fadeIn; }
</style>