admin firs step
This commit is contained in:
604
frontend/admin/pages/dashboard.vue
Normal file
604
frontend/admin/pages/dashboard.vue
Normal file
@@ -0,0 +1,604 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- App Bar -->
|
||||
<v-app-bar color="primary" prominent>
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
|
||||
<v-toolbar-title class="text-h5 font-weight-bold">
|
||||
<v-icon icon="mdi-rocket-launch" class="mr-2"></v-icon>
|
||||
{{ t('dashboard.title') }}
|
||||
<v-chip class="ml-2" :color="roleColor" size="small">
|
||||
{{ userRole }} • {{ scopeLevel }}
|
||||
</v-chip>
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon v-bind="props" class="mr-2">
|
||||
<v-icon icon="mdi-translate"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="locale = 'hu'">
|
||||
<v-list-item-title :class="{ 'font-weight-bold': locale === 'hu' }">
|
||||
🇭🇺 Magyar
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="locale = 'en'">
|
||||
<v-list-item-title :class="{ 'font-weight-bold': locale === 'en' }">
|
||||
🇬🇧 English
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<!-- User Menu -->
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon v-bind="props">
|
||||
<v-avatar size="40" color="secondary">
|
||||
<v-icon icon="mdi-account"></v-icon>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ userEmail }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Rank: {{ userRank }} • Scope ID: {{ scopeId }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item @click="navigateTo('/profile')">
|
||||
<v-list-item-title>
|
||||
<v-icon icon="mdi-account-cog" class="mr-2"></v-icon>
|
||||
{{ t('general.settings') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="logout">
|
||||
<v-list-item-title class="text-error">
|
||||
<v-icon icon="mdi-logout" class="mr-2"></v-icon>
|
||||
{{ t('navigation.logout') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- Navigation Drawer -->
|
||||
<v-navigation-drawer v-model="drawer" temporary>
|
||||
<v-list>
|
||||
<v-list-item prepend-icon="mdi-view-dashboard" :title="t('navigation.dashboard')" value="dashboard" @click="navigateTo('/dashboard')"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-cog" :title="t('navigation.settings')" value="settings" @click="navigateTo('/settings')"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-shield-account" :title="t('navigation.role_management')" value="roles" @click="navigateTo('/roles')"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-map" :title="t('navigation.geographical_scopes')" value="scopes" @click="navigateTo('/scopes')"></v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="text-caption text-disabled">
|
||||
Service Finder Admin v{{ appVersion }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Main Content -->
|
||||
<v-main>
|
||||
<v-container fluid class="pa-6">
|
||||
<!-- Welcome Header -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12">
|
||||
<v-card color="primary" variant="tonal" class="pa-4">
|
||||
<v-card-title class="text-h4 font-weight-bold">
|
||||
<v-icon icon="mdi-rocket" class="mr-2"></v-icon>
|
||||
{{ t('dashboard.welcome_title') }}
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-h6">
|
||||
{{ t('dashboard.welcome_subtitle', { scopeLevel }) }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-chip class="mr-2" color="success">
|
||||
<v-icon icon="mdi-check-circle" class="mr-1"></v-icon>
|
||||
Authenticated as {{ userRole }}
|
||||
</v-chip>
|
||||
<v-chip class="mr-2" color="info">
|
||||
<v-icon icon="mdi-map-marker" class="mr-1"></v-icon>
|
||||
Scope: {{ regionCode || 'Global' }}
|
||||
</v-chip>
|
||||
<v-chip color="warning">
|
||||
<v-icon icon="mdi-shield-star" class="mr-1"></v-icon>
|
||||
Rank: {{ userRank }}
|
||||
</v-chip>
|
||||
|
||||
<!-- Layout Controls -->
|
||||
<v-btn
|
||||
v-if="tileStore.isLayoutModified"
|
||||
class="ml-2"
|
||||
color="warning"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@click="restoreDefaultLayout"
|
||||
:loading="isRestoringLayout"
|
||||
>
|
||||
<v-icon icon="mdi-restore" class="mr-1"></v-icon>
|
||||
Restore Default Layout
|
||||
</v-btn>
|
||||
<v-tooltip v-else location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
class="ml-2"
|
||||
color="success"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-icon icon="mdi-check-all" class="mr-1"></v-icon>
|
||||
Default Layout Active
|
||||
</v-chip>
|
||||
</template>
|
||||
<span>Your dashboard layout matches the default configuration</span>
|
||||
</v-tooltip>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Launchpad Section -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<v-card-title class="text-h5 font-weight-bold pa-0">
|
||||
<v-icon icon="mdi-view-grid" class="mr-2"></v-icon>
|
||||
Launchpad
|
||||
</v-card-title>
|
||||
<v-btn variant="tonal" color="primary" prepend-icon="mdi-cog">
|
||||
Customize Tiles
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-card-subtitle class="pa-0">
|
||||
Role-based dashboard with {{ filteredTiles.length }} accessible tiles
|
||||
</v-card-subtitle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Dynamic Tiles Grid with Drag & Drop -->
|
||||
<Draggable
|
||||
v-model="draggableTiles"
|
||||
tag="v-row"
|
||||
item-key="id"
|
||||
class="drag-container"
|
||||
@end="onDragEnd"
|
||||
:component-data="{ class: 'drag-row' }"
|
||||
:animation="200"
|
||||
:ghost-class="'ghost-tile'"
|
||||
:chosen-class="'chosen-tile'"
|
||||
>
|
||||
<template #item="{ element: tile }">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="3"
|
||||
class="drag-col"
|
||||
>
|
||||
<TileWrapper :tile="tile" @click="handleTileClick">
|
||||
<template #default>
|
||||
<p class="text-body-2">{{ tile.description }}</p>
|
||||
<!-- Requirements Badges -->
|
||||
<div class="mt-2">
|
||||
<v-chip
|
||||
v-for="role in tile.requiredRole"
|
||||
:key="role"
|
||||
size="x-small"
|
||||
class="mr-1 mb-1"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ role }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="tile.minRank"
|
||||
size="x-small"
|
||||
class="mr-1 mb-1"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
>
|
||||
Rank {{ tile.minRank }}+
|
||||
</v-chip>
|
||||
</div>
|
||||
<!-- Scope Level Indicator -->
|
||||
<div v-if="tile.scopeLevel && tile.scopeLevel.length > 0" class="mt-2">
|
||||
<v-icon icon="mdi-map-marker" size="small" class="mr-1"></v-icon>
|
||||
<span class="text-caption">
|
||||
{{ tile.scopeLevel.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</TileWrapper>
|
||||
</v-col>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template #footer v-if="draggableTiles.length === 0">
|
||||
<v-col cols="12">
|
||||
<v-card class="pa-8 text-center">
|
||||
<v-icon icon="mdi-lock" size="64" class="mb-4 text-disabled"></v-icon>
|
||||
<v-card-title class="text-h5">
|
||||
No Tiles Available
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
Your current role ({{ userRole }}) doesn't have access to any dashboard tiles.
|
||||
Contact your administrator for additional permissions.
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
</Draggable>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<v-row class="mt-8">
|
||||
<v-col cols="12">
|
||||
<v-card-title class="text-h5 font-weight-bold pa-0">
|
||||
<v-icon icon="mdi-chart-line" class="mr-2"></v-icon>
|
||||
System Health Dashboard
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
variant="text"
|
||||
class="ml-2"
|
||||
@click="healthMonitor.refreshAll"
|
||||
:loading="healthMonitor.loading"
|
||||
></v-btn>
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="pa-0">
|
||||
Real-time system metrics from health-monitor API
|
||||
<v-chip
|
||||
v-if="healthMonitor.metrics"
|
||||
:color="healthMonitor.systemStatusColor"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-icon :icon="healthMonitor.systemStatusIcon" size="small" class="mr-1"></v-icon>
|
||||
{{ healthMonitor.metrics?.system_status?.toUpperCase() || 'LOADING' }}
|
||||
</v-chip>
|
||||
</v-card-subtitle>
|
||||
</v-col>
|
||||
|
||||
<!-- Total Assets -->
|
||||
<v-col cols="12" md="3">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6 d-flex align-center">
|
||||
<v-icon icon="mdi-database" class="mr-2"></v-icon>
|
||||
Total Assets
|
||||
<v-spacer></v-spacer>
|
||||
<v-progress-circular
|
||||
v-if="healthMonitor.loading && !healthMonitor.metrics"
|
||||
indeterminate
|
||||
size="20"
|
||||
width="2"
|
||||
></v-progress-circular>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h4 font-weight-bold text-primary">
|
||||
{{ healthMonitor.metrics?.total_assets?.toLocaleString() || '--' }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>Vehicles, services, and organizations</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Total Organizations -->
|
||||
<v-col cols="12" md="3">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6 d-flex align-center">
|
||||
<v-icon icon="mdi-office-building" class="mr-2"></v-icon>
|
||||
Organizations
|
||||
<v-spacer></v-spacer>
|
||||
<v-progress-circular
|
||||
v-if="healthMonitor.loading && !healthMonitor.metrics"
|
||||
indeterminate
|
||||
size="20"
|
||||
width="2"
|
||||
></v-progress-circular>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h4 font-weight-bold text-success">
|
||||
{{ healthMonitor.metrics?.total_organizations?.toLocaleString() || '--' }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>Registered business entities</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Critical Alerts -->
|
||||
<v-col cols="12" md="3">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6 d-flex align-center">
|
||||
<v-icon icon="mdi-alert" class="mr-2"></v-icon>
|
||||
Critical Alerts (24h)
|
||||
<v-spacer></v-spacer>
|
||||
<v-progress-circular
|
||||
v-if="healthMonitor.loading && !healthMonitor.metrics"
|
||||
indeterminate
|
||||
size="20"
|
||||
width="2"
|
||||
></v-progress-circular>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h4 font-weight-bold" :class="healthMonitor.metrics?.critical_alerts_24h ? 'text-error' : 'text-info'">
|
||||
{{ healthMonitor.metrics?.critical_alerts_24h || 0 }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>
|
||||
<span v-if="healthMonitor.metrics?.critical_alerts_24h">Requires immediate attention</span>
|
||||
<span v-else>No critical issues</span>
|
||||
</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- System Uptime -->
|
||||
<v-col cols="12" md="3">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6 d-flex align-center">
|
||||
<v-icon icon="mdi-heart-pulse" class="mr-2"></v-icon>
|
||||
System Uptime
|
||||
<v-spacer></v-spacer>
|
||||
<v-progress-circular
|
||||
v-if="healthMonitor.loading && !healthMonitor.metrics"
|
||||
indeterminate
|
||||
size="20"
|
||||
width="2"
|
||||
></v-progress-circular>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h4 font-weight-bold text-warning">
|
||||
{{ healthMonitor.formattedUptime }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>
|
||||
Response: {{ healthMonitor.formattedResponseTime }}
|
||||
<v-icon
|
||||
v-if="healthMonitor.metrics?.response_time_ms < 100"
|
||||
icon="mdi-check"
|
||||
color="success"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
v-else-if="healthMonitor.metrics?.response_time_ms < 300"
|
||||
icon="mdi-alert"
|
||||
color="warning"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
icon="mdi-alert-circle"
|
||||
color="error"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
></v-icon>
|
||||
</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Additional Metrics Row -->
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon icon="mdi-account-group" class="mr-2"></v-icon>
|
||||
Active Users
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h3 font-weight-bold text-primary">
|
||||
{{ healthMonitor.metrics?.active_users?.toLocaleString() || '--' }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>Currently logged in users</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon icon="mdi-database-export" class="mr-2"></v-icon>
|
||||
DB Connections
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h3 font-weight-bold" :class="getDbConnectionClass(healthMonitor.metrics?.database_connections)">
|
||||
{{ healthMonitor.metrics?.database_connections || '--' }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>
|
||||
<span v-if="healthMonitor.metrics?.database_connections > 40" class="text-error">High load</span>
|
||||
<span v-else-if="healthMonitor.metrics?.database_connections > 20" class="text-warning">Moderate load</span>
|
||||
<span v-else class="text-success">Normal load</span>
|
||||
</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon icon="mdi-update" class="mr-2"></v-icon>
|
||||
Last Updated
|
||||
</v-card-title>
|
||||
<v-card-text class="text-h5 font-weight-bold text-grey">
|
||||
{{ healthMonitor.lastUpdated ? formatTime(healthMonitor.lastUpdated) : 'Never' }}
|
||||
</v-card-text>
|
||||
<v-card-subtitle>
|
||||
<v-icon icon="mdi-clock-outline" size="small" class="mr-1"></v-icon>
|
||||
Auto-refresh every 30s
|
||||
</v-card-subtitle>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<!-- Footer -->
|
||||
<v-footer app color="surface" class="px-4">
|
||||
<v-spacer></v-spacer>
|
||||
<div class="text-caption text-disabled">
|
||||
Geographical Scope: {{ regionCode || 'Global' }} •
|
||||
Last sync: {{ new Date().toLocaleTimeString() }} •
|
||||
<v-icon icon="mdi-circle-small" class="mx-1" color="success"></v-icon>
|
||||
All systems operational
|
||||
</div>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
import { useRBAC } from '~/composables/useRBAC'
|
||||
import { useHealthMonitor } from '~/composables/useHealthMonitor'
|
||||
import { useTileStore } from '~/stores/tiles'
|
||||
import TileCard from '~/components/TileCard.vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// i18n
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
// State
|
||||
const drawer = ref(false)
|
||||
const appVersion = '1.0.0'
|
||||
const tileStore = useTileStore()
|
||||
const isRestoringLayout = ref(false)
|
||||
const draggableTiles = ref<any[]>([])
|
||||
|
||||
// Stores and composables
|
||||
const authStore = useAuthStore()
|
||||
const rbac = useRBAC()
|
||||
const healthMonitor = useHealthMonitor()
|
||||
|
||||
// Computed properties
|
||||
const userEmail = computed(() => authStore.user?.email || '')
|
||||
const userRole = computed(() => authStore.getUserRole || '')
|
||||
const userRank = computed(() => authStore.getUserRank || 0)
|
||||
const scopeLevel = computed(() => authStore.getScopeLevel || '')
|
||||
const regionCode = computed(() => authStore.getRegionCode || '')
|
||||
const scopeId = computed(() => authStore.getScopeId || 0)
|
||||
const roleColor = computed(() => rbac.getRoleColor())
|
||||
const filteredTiles = computed(() => tileStore.visibleTiles)
|
||||
|
||||
// Watch for changes to filteredTiles and update draggableTiles
|
||||
watch(filteredTiles, (newTiles) => {
|
||||
draggableTiles.value = [...newTiles]
|
||||
}, { immediate: true })
|
||||
|
||||
// Methods
|
||||
function logout() {
|
||||
authStore.logout()
|
||||
navigateTo('/login')
|
||||
}
|
||||
|
||||
// Drag & Drop handling
|
||||
function onDragEnd() {
|
||||
const tileIds = draggableTiles.value.map(tile => tile.id)
|
||||
tileStore.updateTilePositions(tileIds)
|
||||
}
|
||||
|
||||
// Tile click handling
|
||||
function handleTileClick(tile: any) {
|
||||
const routes: Record<string, string> = {
|
||||
'ai-logs': '/ai-logs',
|
||||
'financial-dashboard': '/finance',
|
||||
'salesperson-hub': '/sales',
|
||||
'user-management': '/users',
|
||||
'service-moderation-map': '/moderation-map',
|
||||
'gamification-control': '/gamification',
|
||||
'system-health': '/system'
|
||||
}
|
||||
|
||||
const route = routes[tile.id]
|
||||
if (route) {
|
||||
navigateTo(route)
|
||||
} else {
|
||||
console.warn(`No route defined for tile: ${tile.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default layout
|
||||
async function restoreDefaultLayout() {
|
||||
isRestoringLayout.value = true
|
||||
try {
|
||||
tileStore.resetPreferences()
|
||||
// Show success message
|
||||
console.log('Layout restored to default')
|
||||
} catch (error) {
|
||||
console.error('Failed to restore layout:', error)
|
||||
} finally {
|
||||
isRestoringLayout.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
const getDbConnectionClass = (connections: number | undefined) => {
|
||||
if (!connections) return 'text-grey'
|
||||
if (connections > 40) return 'text-error'
|
||||
if (connections > 20) return 'text-warning'
|
||||
return 'text-success'
|
||||
}
|
||||
|
||||
const formatTime = (value: any) => {
|
||||
if (!value) return 'N/A';
|
||||
try {
|
||||
const d = new Date(value);
|
||||
// Check if it's a valid date
|
||||
if (isNaN(d.getTime())) return String(value);
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
} catch (e) {
|
||||
return 'Invalid Time';
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
console.log('Dashboard mounted for user:', userEmail.value)
|
||||
// Initialize health monitor data
|
||||
healthMonitor.initialize()
|
||||
// Load tile preferences
|
||||
tileStore.loadPreferences()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-main {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.v-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* Drag & Drop Styles */
|
||||
.drag-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.drag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -12px;
|
||||
}
|
||||
|
||||
.drag-col {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ghost-tile {
|
||||
opacity: 0.5;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chosen-tile {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.02);
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user