Files
innotexBoard/frontend/src/views/HomelabView.vue
2026-01-16 18:40:39 +01:00

270 lines
9.7 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
<!-- Header -->
<div class="sticky top-0 z-40 backdrop-blur-xl bg-gray-900/80 border-b border-gray-700/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-4xl font-bold text-white mb-2">
<span class="bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
InnotexBoard
</span>
</h1>
<p class="text-gray-400">Votre Homelab Personnel</p>
</div>
<div class="flex items-center space-x-4">
<span v-if="authStore.isAuthenticated" class="text-sm text-gray-300 bg-gray-800 px-3 py-1 rounded-full">
{{ authStore.username }}
</span>
<button
v-if="authStore.isAuthenticated"
@click="goToAdmin"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition text-sm font-medium"
>
Gérer
</button>
<button
v-if="!authStore.isAuthenticated"
@click="goToLogin"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition text-sm font-medium"
>
Connexion
</button>
<button
v-else
@click="handleLogout"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition text-sm font-medium"
>
Déconnexion
</button>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Quick Stats (if authenticated) -->
<div v-if="authStore.isAuthenticated" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-12">
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">Total Services</div>
<div class="text-3xl font-bold text-white">{{ shortcuts.length }}</div>
</div>
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">Catégories</div>
<div class="text-3xl font-bold text-white">{{ categories.size }}</div>
</div>
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">État</div>
<div class="text-3xl font-bold text-green-400"> Actif</div>
</div>
<div class="bg-gray-800/50 backdrop-blur border border-gray-700/50 rounded-xl p-4">
<div class="text-gray-400 text-sm mb-1">Dernier Update</div>
<div class="text-lg font-bold text-white">{{ lastUpdate }}</div>
</div>
</div>
<!-- Shortcuts Grid by Category -->
<div v-if="shortcuts.length > 0">
<div v-for="(categoryShortcuts, category) in groupedShortcuts" :key="category" class="mb-12">
<div class="flex items-center mb-6">
<div class="h-1 w-8 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mr-3"></div>
<h2 class="text-2xl font-bold text-white capitalize">{{ getCategoryTitle(category) }}</h2>
<span class="ml-3 px-3 py-1 bg-gray-700/50 text-gray-300 text-sm rounded-full">
{{ categoryShortcuts.length }}
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<a
v-for="shortcut in categoryShortcuts"
:key="shortcut.id"
:href="shortcut.url"
target="_blank"
rel="noopener noreferrer"
class="group relative bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700/50 hover:border-gray-600 rounded-xl p-6 transition-all duration-300 hover:shadow-2xl hover:shadow-blue-500/20 overflow-hidden"
:style="{ borderLeftColor: shortcut.color || '#3b82f6', borderLeftWidth: '4px' }"
>
<!-- Background gradient on hover -->
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Content -->
<div class="relative z-10">
<!-- Icon -->
<div class="text-4xl mb-4 inline-block p-3 bg-gray-700/50 group-hover:bg-gray-600/50 rounded-lg transition">
{{ shortcut.icon }}
</div>
<!-- Title -->
<h3 class="font-bold text-white text-lg mb-2 group-hover:text-blue-400 transition truncate">
{{ shortcut.name }}
</h3>
<!-- Description -->
<p v-if="shortcut.description" class="text-gray-400 text-sm mb-4 line-clamp-2">
{{ shortcut.description }}
</p>
<!-- URL -->
<div class="flex items-center text-gray-500 text-xs group-hover:text-gray-300 transition">
<span class="truncate">{{ getHostname(shortcut.url) }}</span>
<svg class="w-4 h-4 ml-2 opacity-0 group-hover:opacity-100 transition transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-13.5-2.5H21m0 0l-3-3m3 3l-3 3" />
</svg>
</div>
</div>
<!-- Delete button (if authenticated) -->
<button
v-if="authStore.isAuthenticated"
@click.prevent.stop="deleteShortcut(shortcut.id)"
class="absolute top-2 right-2 p-2 bg-red-500/0 hover:bg-red-500 text-red-400 hover:text-white rounded-lg transition opacity-0 group-hover:opacity-100 duration-300"
title="Supprimer"
>
</button>
</a>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="flex flex-col items-center justify-center py-20">
<div class="text-6xl mb-4">🔗</div>
<h3 class="text-2xl font-bold text-white mb-2">Aucun Service Configuré</h3>
<p class="text-gray-400 mb-4">
<span v-if="authStore.isAuthenticated">
Ajoutez vos premiers services pour les afficher ici
</span>
<span v-else>
Connectez-vous pour gérer vos services
</span>
</p>
<button
v-if="authStore.isAuthenticated"
@click="goToAdmin"
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition font-medium"
>
Ajouter un Service
</button>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-gray-800 mt-20 py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-500 text-sm">
<p>InnotexBoard v1.0 Votre centre de contrôle homelab personnel</p>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import api from '../api'
const router = useRouter()
const authStore = useAuthStore()
const shortcuts = ref([])
const loading = ref(true)
const lastUpdate = ref('À l\'instant')
const categories = computed(() => {
return new Set(shortcuts.value.map(s => s.category || 'Autres'))
})
const groupedShortcuts = computed(() => {
const grouped = {}
shortcuts.value.forEach(shortcut => {
const cat = shortcut.category || 'Autres'
if (!grouped[cat]) {
grouped[cat] = []
}
grouped[cat].push(shortcut)
})
return grouped
})
const fetchShortcuts = async () => {
try {
loading.value = true
const response = await api.get('/shortcuts/')
shortcuts.value = response.data || []
lastUpdate.value = new Date().toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit'
})
} catch (error) {
console.error('Erreur lors du chargement des raccourcis:', error)
shortcuts.value = []
} finally {
loading.value = false
}
}
const deleteShortcut = async (shortcutId) => {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce raccourci ?')) return
try {
await api.delete(`/shortcuts/${shortcutId}`)
shortcuts.value = shortcuts.value.filter(s => s.id !== shortcutId)
} catch (error) {
console.error('Erreur lors de la suppression:', error)
alert('Erreur lors de la suppression du raccourci')
}
}
const getHostname = (url) => {
try {
const u = new URL(url)
return u.hostname
} catch {
return url
}
}
const getCategoryTitle = (category) => {
const titles = {
'media': '🎬 Médias',
'storage': '💾 Stockage',
'tools': '🔧 Outils',
'monitoring': '📊 Monitoring',
'security': '🔒 Sécurité',
'other': '📌 Autres'
}
return titles[category.toLowerCase()] || category
}
const goToLogin = () => {
router.push('/login')
}
const goToAdmin = () => {
router.push('/shortcuts')
}
const handleLogout = () => {
authStore.logout()
router.push('/')
}
// Auto-refresh shortcuts every 30 seconds
onMounted(() => {
fetchShortcuts()
const interval = setInterval(fetchShortcuts, 30000)
return () => clearInterval(interval)
})
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>