Initial commit
This commit is contained in:
269
frontend/src/views/HomelabView.vue
Normal file
269
frontend/src/views/HomelabView.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user