diff --git a/.vscode/settings.json b/.vscode/settings.json index a841788..bb3a478 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,20 @@ { "cSpell.words": [ "Adminannouncements", + "lucide", + "modyfi", "mythicalclient", "mythicaldash", "Predis", + "qrcode", "recommand", "Regen", "shortdescription", "SOURCECODE", + "supportbuddy", "Swal", - "sweetalert" + "sweetalert", + "vueuse" ], "json.maxItemsComputed": 10000, "editor.largeFileOptimizations": false, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 8386137..0670dad 100755 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,11 +1,22 @@ <template> - <router-view></router-view> + <router-view v-slot="{ Component }"> + <component :is="Component" /> + </router-view> </template> <script lang="ts"> import { defineComponent } from 'vue'; +import { useRouter } from 'vue-router'; export default defineComponent({ - name: 'App', + name: 'App', + setup() { + const router = useRouter(); + + router.beforeEach((to, from, next) => { + window.scrollTo(0, 0); + next(); + }); + }, }); </script> diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 4666a76..dfbb175 100755 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -3,6 +3,18 @@ @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); + +@layer base { + html { + font-family: 'Poppins', sans-serif; + } +} + +html, body { + overscroll-behavior-y: none; +} + /** Tell the browser that this is a dark theme. **/ diff --git a/frontend/src/components/admin/LayoutDashboard.vue b/frontend/src/components/admin/LayoutDashboard.vue index 2aadb41..8dc4996 100755 --- a/frontend/src/components/admin/LayoutDashboard.vue +++ b/frontend/src/components/admin/LayoutDashboard.vue @@ -2,23 +2,28 @@ <template> <div class="min-h-screen bg-gray-900 text-gray-100 font-sans"> <!-- Mobile Menu Button --> - <button @click="isSidebarOpen = !isSidebarOpen" - class="lg:hidden fixed top-4 left-4 z-50 p-2 bg-gray-800/50 rounded-full backdrop-blur-sm"> + <button + @click="isSidebarOpen = !isSidebarOpen" + class="lg:hidden fixed top-4 left-4 z-50 p-2 bg-gray-800/50 rounded-full backdrop-blur-sm" + > <Menu v-if="!isSidebarOpen" class="w-6 h-6 text-pink-400" /> <X v-else class="w-6 h-6 text-pink-400" /> </button> <!-- Sidebar --> - <aside :class="[ - 'fixed inset-y-0 left-0 z-40 w-64 transition-transform duration-300 ease-in-out transform', - isSidebarOpen ? 'translate-x-0' : '-translate-x-full', - 'lg:translate-x-0 bg-gray-800/50 backdrop-blur-md', - ]"> + <aside + :class="[ + 'fixed inset-y-0 left-0 z-40 w-64 transition-transform duration-300 ease-in-out transform', + isSidebarOpen ? 'translate-x-0' : '-translate-x-full', + 'lg:translate-x-0 bg-gray-800/50 backdrop-blur-md', + ]" + > <div class="p-6"> <h1 - class="text-2xl font-bold bg-gradient-to-r from-pink-500 to-violet-500 bg-clip-text text-transparent"> - <span class="block text-lg">{{ Settings.getSetting("debug_name")}}</span> - <span class="block text-sm text-gray-400">{{ Settings.getSetting("debug_version") }}</span> + class="text-2xl font-bold bg-gradient-to-r from-pink-500 to-violet-500 bg-clip-text text-transparent" + > + <span class="block text-lg">{{ Settings.getSetting('debug_name') }}</span> + <span class="block text-sm text-gray-400">{{ Settings.getSetting('debug_version') }}</span> </h1> </div> <nav class="p-6"> @@ -28,26 +33,36 @@ <li v-for="item in menuGroup.items" :key="item.name"> <!-- Menu item with submenu --> <template v-if="item.subMenu"> - <div @click="toggleSubmenu(item)" - class="flex items-center px-4 py-2 rounded-lg transition-all duration-200 hover:bg-gray-700/50 cursor-pointer"> + <div + @click="toggleSubmenu(item)" + class="flex items-center px-4 py-2 rounded-lg transition-all duration-200 hover:bg-gray-700/50 cursor-pointer" + > <component :is="item.icon" class="w-5 h-5 mr-3 text-pink-400" /> <span>{{ item.name }}</span> - <ChevronDown class="w-4 h-4 ml-auto transition-transform duration-200" - :class="{ 'rotate-180': item.isOpen }" /> - <span v-if="'count' in item" - class="ml-2 text-xs bg-violet-500 text-white px-2 py-1 rounded-full"> + <ChevronDown + class="w-4 h-4 ml-auto transition-transform duration-200" + :class="{ 'rotate-180': item.isOpen }" + /> + <span + v-if="'count' in item" + class="ml-2 text-xs bg-violet-500 text-white px-2 py-1 rounded-full" + > {{ item.count }} </span> </div> <ul v-if="item.isOpen" class="mt-2 ml-4 space-y-2"> <li v-for="subItem in item.subMenu" :key="subItem.name"> - <RouterLink :to="subItem.path || ''" + <RouterLink + :to="subItem.path || ''" class="flex items-center px-4 py-2 rounded-lg transition-all duration-200 hover:bg-gray-700/50" - :class="{ 'bg-gray-700/50': route.path === subItem.path }"> + :class="{ 'bg-gray-700/50': route.path === subItem.path }" + > <component :is="subItem.icon" class="w-5 h-5 mr-3 text-pink-400" /> <span>{{ subItem.name }}</span> - <span v-if="'count' in subItem" - class="ml-auto text-xs bg-violet-500 text-white px-2 py-1 rounded-full"> + <span + v-if="'count' in subItem" + class="ml-auto text-xs bg-violet-500 text-white px-2 py-1 rounded-full" + > {{ subItem.count }} </span> </RouterLink> @@ -55,13 +70,18 @@ </ul> </template> <!-- Regular menu item --> - <RouterLink v-else :to="item.path || ''" + <RouterLink + v-else + :to="item.path || ''" class="flex items-center px-4 py-2 rounded-lg transition-all duration-200 hover:bg-gray-700/50" - :class="{ 'bg-gray-700/50': item.active }"> + :class="{ 'bg-gray-700/50': item.active }" + > <component :is="item.icon" class="w-5 h-5 mr-3 text-pink-400" /> <span>{{ item.name }}</span> - <span v-if="'count' in item" - class="ml-auto text-xs bg-violet-500 text-white px-2 py-1 rounded-full"> + <span + v-if="'count' in item" + class="ml-auto text-xs bg-violet-500 text-white px-2 py-1 rounded-full" + > {{ item.count }} </span> </RouterLink> @@ -71,38 +91,55 @@ </nav> </aside> - - <div class="lg:ml-64 min-h-screen flex flex-col"> <!-- Top Navigation --> <header class="bg-gray-800/50 backdrop-blur-md p-4 flex items-center justify-between"> <div class="relative w-full max-w-xl"> - <input v-model="searchQuery" type="search" placeholder="Search..." + <input + v-model="searchQuery" + type="search" + placeholder="Search..." class="w-full bg-gray-700/50 text-gray-100 placeholder-gray-400 rounded-full py-2 pl-10 pr-4 focus:outline-none focus:ring-2 focus:ring-pink-500" - @focus="isSearchFocused = true" @blur="handleSearchBlur" /> + @focus="isSearchFocused = true" + @blur="handleSearchBlur" + /> <Search class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" /> <!-- Search Results Dropdown --> - <div v-if="isSearchFocused && filteredResults.length > 0" - class="absolute z-10 w-full mt-2 bg-gray-800/90 backdrop-blur-md rounded-lg shadow-xl max-h-60 overflow-y-auto"> - <a v-for="result in filteredResults" :key="result.id" :href="result.path" + <div + v-if="isSearchFocused && filteredResults.length > 0" + class="absolute z-10 w-full mt-2 bg-gray-800/90 backdrop-blur-md rounded-lg shadow-xl max-h-60 overflow-y-auto" + > + <a + v-for="result in filteredResults" + :key="result.id" + :href="result.path" class="block px-4 py-2 hover:bg-gray-700/50 transition-colors duration-200" - @mousedown.prevent="handleResultClick(result)"> + @mousedown.prevent="handleResultClick(result)" + > {{ result.name }} </a> </div> </div> <div class="relative ml-4"> - <button @click="isProfileOpen = !isProfileOpen" - class="flex items-center space-x-2 focus:outline-none"> + <button + @click="isProfileOpen = !isProfileOpen" + class="flex items-center space-x-2 focus:outline-none" + > <img :src="Session.getInfo('avatar')" alt="User Avatar" class="w-8 h-8 rounded-full" /> <ChevronDown class="w-4 h-4 text-gray-400" :class="{ 'rotate-180': isProfileOpen }" /> </button> - <div v-if="isProfileOpen" - class="absolute right-0 mt-2 w-48 bg-gray-800/90 backdrop-blur-md rounded-lg shadow-xl py-1 animate-fadeIn"> - <RouterLink v-for="item in profileMenu" :key="item.name" :to="item.path" - class="block px-4 py-2 hover:bg-gray-700/50 transition-colors duration-200"> + <div + v-if="isProfileOpen" + class="absolute right-0 mt-2 w-48 bg-gray-800/90 backdrop-blur-md rounded-lg shadow-xl py-1 animate-fadeIn" + > + <RouterLink + v-for="item in profileMenu" + :key="item.name" + :to="item.path" + class="block px-4 py-2 hover:bg-gray-700/50 transition-colors duration-200" + > {{ item.name }} </RouterLink> </div> @@ -121,8 +158,12 @@ <div class="flex flex-col md:flex-row justify-between items-center"> <p class="text-gray-400 text-sm mb-2 md:mb-0">© 2023 MythicalClient. All rights reserved.</p> <div class="flex space-x-4"> - <a v-for="link in footerLinks" :key="link.name" :href="link.path" - class="text-gray-400 hover:text-pink-400 text-sm transition-colors duration-200"> + <a + v-for="link in footerLinks" + :key="link.name" + :href="link.path" + class="text-gray-400 hover:text-pink-400 text-sm transition-colors duration-200" + > {{ link.name }} </a> </div> @@ -160,15 +201,13 @@ if (!Session.isSessionValid()) { router.push('/auth/login'); } - try { Session.startSession(); } catch (error) { console.error('Session failed:', error); } - -if (Session.getInfo('role') == "1" && Session.getInfo('role') == "2") { +if (Session.getInfo('role') == '1' && Session.getInfo('role') == '2') { router.push('/dashboard'); } @@ -207,8 +246,6 @@ Dashboard.get().then((data) => { const route = useRoute(); const adminBaseUri = '/mc-admin'; - - interface MenuItem { name: string; path?: string; @@ -254,7 +291,7 @@ const menuGroups = ref<{ title: string; items: MenuItem[] }[]>([ name: 'Create Invoice', path: `${adminBaseUri}/invoices/create`, icon: PlusCircle, - } + }, ], active: route.path === `${adminBaseUri}/invoices`, count: computed(() => dashBoard.value.count.invoices_count || 0), @@ -265,8 +302,8 @@ const menuGroups = ref<{ title: string; items: MenuItem[] }[]>([ icon: InfoIcon, active: route.path === `${adminBaseUri}/tickets`, count: computed(() => dashBoard.value.count.tickets_count || 0), - } - ] + }, + ], }, { title: 'Advanced', @@ -288,8 +325,7 @@ const menuGroups = ref<{ title: string; items: MenuItem[] }[]>([ path: `${adminBaseUri}/mounts`, icon: HardDrive, active: route.path === `${adminBaseUri}/mounts`, - } - + }, ], }, ]); diff --git a/frontend/src/components/client/Dashboard/Account/Activities.vue b/frontend/src/components/client/Dashboard/Account/Activities.vue index c60dd8d..59d8e21 100755 --- a/frontend/src/components/client/Dashboard/Account/Activities.vue +++ b/frontend/src/components/client/Dashboard/Account/Activities.vue @@ -11,13 +11,13 @@ const { t } = useI18n(); document.title = t('account.pages.activity.page.title'); interface Activity { - id: number; - user: string; - action: string; - ip_address: string; - deleted: boolean | string; - locked: boolean | string; - date: string; + id: number; + user: string; + action: string; + ip_address: string; + deleted: boolean | string; + locked: boolean | string; + date: string; } const activities = ref<Activity[]>([]); @@ -25,68 +25,72 @@ const loading = ref(true); const error = ref<string | null>(null); const fetchActivities = async () => { - try { - const response = await Activities.get(); - activities.value = response; - } catch (err) { - error.value = err instanceof Error ? err.message : t('account.pages.activity.page.table.error'); - } finally { - loading.value = false; - } + try { + const response = await Activities.get(); + activities.value = response; + } catch (err) { + error.value = err instanceof Error ? err.message : t('account.pages.activity.page.table.error'); + } finally { + loading.value = false; + } }; onMounted(fetchActivities); onErrorCaptured((err) => { - error.value = 'An unexpected error occurred'; - console.error('Error captured:', err); - return false; + error.value = 'An unexpected error occurred'; + console.error('Error captured:', err); + return false; }); const columnsActivities = [ - { - accessorKey: 'action', - header: t('account.pages.activity.page.table.columns.action'), - }, - { - accessorKey: 'ip_address', - header: t('account.pages.activity.page.table.columns.ip'), - }, - { - accessorKey: 'date', - header: t('account.pages.activity.page.table.columns.date'), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - cell: (info: any) => format(new Date(info.getValue()), 'MMM d, yyyy HH:mm'), - }, + { + accessorKey: 'action', + header: t('account.pages.activity.page.table.columns.action'), + }, + { + accessorKey: 'ip_address', + header: t('account.pages.activity.page.table.columns.ip'), + }, + { + accessorKey: 'date', + header: t('account.pages.activity.page.table.columns.date'), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + cell: (info: any) => format(new Date(info.getValue()), 'MMM d, yyyy HH:mm'), + }, ]; </script> <template> - <LayoutAccount /> + <LayoutAccount /> - <div> - <div v-if="loading" class="text-center py-4"> - <div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900"></div> - <p class="mt-2">Loading activities...</p> - </div> + <div> + <div v-if="loading" class="text-center py-4"> + <div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-900"></div> + <p class="mt-2">Loading activities...</p> + </div> - <div v-else-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded" role="alert"> - <p>{{ error }}</p> - </div> + <div v-else-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded" role="alert"> + <p>{{ error }}</p> + </div> - <div v-else class="overflow-x-auto"> - <TableTanstack :data="activities" :columns="columnsActivities" :tableName="t('account.pages.activity.page.title')" /> + <div v-else class="overflow-x-auto"> + <TableTanstack + :data="activities" + :columns="columnsActivities" + :tableName="t('account.pages.activity.page.title')" + /> + </div> </div> - </div> </template> <style scoped> .overflow-x-auto::-webkit-scrollbar { - display: none; + display: none; } .overflow-x-auto { - -ms-overflow-style: none; - scrollbar-width: none; + -ms-overflow-style: none; + scrollbar-width: none; } </style> diff --git a/frontend/src/components/client/Dashboard/Account/Mails.vue b/frontend/src/components/client/Dashboard/Account/Mails.vue index 264b33f..b9ed497 100755 --- a/frontend/src/components/client/Dashboard/Account/Mails.vue +++ b/frontend/src/components/client/Dashboard/Account/Mails.vue @@ -62,14 +62,19 @@ const columnsEmails = [ accessorKey: 'actions', header: t('account.pages.emails.page.table.columns.actions'), enableSorting: false, - cell: ({ row }: { row: { original: Email } }) => h('button', { - class: 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600', target: '_blank', rel: 'noopener noreferrer', - onClick: () => window.location.href = `/api/user/session/emails/${row.original.id}/raw` - }, t('account.pages.emails.page.table.results.viewButton')), + cell: ({ row }: { row: { original: Email } }) => + h( + 'button', + { + class: 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600', + target: '_blank', + rel: 'noopener noreferrer', + onClick: () => (window.location.href = `/api/user/session/emails/${row.original.id}/raw`), + }, + t('account.pages.emails.page.table.results.viewButton'), + ), }, ]; - - </script> <template> <LayoutAccount /> diff --git a/frontend/src/components/client/Dashboard/Account/Security.vue b/frontend/src/components/client/Dashboard/Account/Security.vue index 28d1f62..280c84c 100755 --- a/frontend/src/components/client/Dashboard/Account/Security.vue +++ b/frontend/src/components/client/Dashboard/Account/Security.vue @@ -45,12 +45,15 @@ const disable2FA = () => { <LayoutAccount /> <!-- Change Password --> - <CardComponent :cardTitle="t('account.pages.security.page.cards.password.title')" :cardDescription="t('account.pages.security.page.cards.password.subTitle')"> + <CardComponent + :cardTitle="t('account.pages.security.page.cards.password.title')" + :cardDescription="t('account.pages.security.page.cards.password.subTitle')" + > <router-link to="/auth/forgot-password" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded text-sm font-medium transition-colors" > - {{ t('account.pages.security.page.cards.password.change_button.label') }} + {{ t('account.pages.security.page.cards.password.change_button.label') }} </router-link> </CardComponent> <br /> @@ -60,21 +63,25 @@ const disable2FA = () => { :cardDescription="t('account.pages.security.page.cards.twofactor.subTitle')" > <div v-if="is2FAEnabled" class="flex items-center justify-between"> - <p class="text-sm text-gray-100">{{ t('account.pages.security.page.cards.twofactor.disable_button.description')}}</p> + <p class="text-sm text-gray-100"> + {{ t('account.pages.security.page.cards.twofactor.disable_button.description') }} + </p> <button @click="disable2FA" class="ml-4 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors" > - {{ t('account.pages.security.page.cards.twofactor.disable_button.label')}} + {{ t('account.pages.security.page.cards.twofactor.disable_button.label') }} </button> </div> <div v-else class="flex items-center justify-between"> - <p class="text-sm text-gray-100">{{ t('account.pages.security.page.cards.twofactor.enable_button.description')}}</p> + <p class="text-sm text-gray-100"> + {{ t('account.pages.security.page.cards.twofactor.enable_button.description') }} + </p> <button @click="enable2FA" class="ml-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium transition-colors" > - {{ t('account.pages.security.page.cards.twofactor.enable_button.label') }} + {{ t('account.pages.security.page.cards.twofactor.enable_button.label') }} </button> </div> </CardComponent> diff --git a/frontend/src/components/client/Dashboard/Admin/Settings/Advanced.vue b/frontend/src/components/client/Dashboard/Admin/Settings/Advanced.vue deleted file mode 100755 index f931c86..0000000 --- a/frontend/src/components/client/Dashboard/Admin/Settings/Advanced.vue +++ /dev/null @@ -1,60 +0,0 @@ -<script setup lang="ts"> -import { ref } from 'vue'; -import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; - -const advancedConfig = ref({ - maintenance: false, - debug: false, - cacheDuration: 60, -}); - -function saveEmailSettings() {} - -function testEmailConnection() {} -</script> -<template> - <CardComponent - cardTitle="Advanced Settings" - cardDescription="Configure advanced settings such as maintenance mode, debug mode, and cache duration." - > - <div class="space-y-4"> - <div> - <label class="flex items-center"> - <input - v-model="advancedConfig.maintenance" - type="checkbox" - class="rounded border-gray-700/50 bg-gray-800/50 text-purple-500 focus:ring-purple-500/50" - /> - <span class="ml-2 text-sm text-gray-300">Maintenance Mode</span> - </label> - </div> - <div> - <label class="flex items-center"> - <input - v-model="advancedConfig.debug" - type="checkbox" - class="rounded border-gray-700/50 bg-gray-800/50 text-purple-500 focus:ring-purple-500/50" - /> - <span class="ml-2 text-sm text-gray-300">Debug Mode</span> - </label> - </div> - <div class="mt-6"> - <hr class="border-gray-700/50" /> - </div> - <div class="mt-4 flex space-x-4"> - <button - @click="saveEmailSettings" - class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded text-sm font-medium transition-colors" - > - Save Settings - </button> - <button - @click="testEmailConnection" - class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm font-medium transition-colors" - > - Test Connection - </button> - </div> - </div> - </CardComponent> -</template> diff --git a/frontend/src/components/client/Dashboard/Admin/Settings/CloudFlare.vue b/frontend/src/components/client/Dashboard/Admin/Settings/CloudFlare.vue deleted file mode 100755 index 08f1e72..0000000 --- a/frontend/src/components/client/Dashboard/Admin/Settings/CloudFlare.vue +++ /dev/null @@ -1,96 +0,0 @@ -<script setup lang="ts"> -import { ref } from 'vue'; -import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; -import TextInput from '@/components/client/ui/TextForms/TextInput.vue'; -import SelectInput from '@/components/client/ui/TextForms/SelectInput.vue'; - -const fetchCloudflareData = async () => { - try { - const response = await fetch('/api/system/fetchcloudflaretrusedip'); - const data = await response.json(); - cloudflareConfig.value.trustedProxies = data.ipv4.join(', '); - } catch (error) { - console.error('Error fetching Cloudflare IPs:', error); - } -}; - -const cloudflareConfig = ref({ - turnstileSiteKey: '', - turnstileSecretKey: '', - trustedProxies: '', - securityLevel: 'essentially_off', -}); -</script> -<template> - <CardComponent cardTitle="Cloudflare Settings" cardDescription="Configure your Cloudflare settings."> - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> - <!-- Cloudflare Turnstile Settings --> - <div class="space-y-4"> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Turnstile Site Key</label> - <TextInput - v-model="cloudflareConfig.turnstileSiteKey" - type="text" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Turnstile Secret Key</label> - <TextInput - v-model="cloudflareConfig.turnstileSecretKey" - type="text" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - </div> - - <!-- Cloudflare Proxies Settings --> - <div class="space-y-4"> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Trusted Proxies</label> - <div class="flex items-center mt-2"> - <TextInput - v-model="cloudflareConfig.trustedProxies" - type="text" - placeholder="Comma-separated list of IPs" - class="flex-grow bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - <button - type="button" - @click="fetchCloudflareData" - class="ml-2 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm font-medium transition-colors" - > - Fetch - </button> - </div> - </div> - - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Security Level</label> - <SelectInput - v-model="cloudflareConfig.securityLevel" - :options="[ - { value: 'essentially_off', label: 'Essentially Off' }, - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'under_attack', label: 'Under Attack' }, - ]" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - </div> - </div> - <div class="mt-6"> - <hr class="border-gray-700/50" /> - </div> - <div class="flex justify-start mt-4"> - <button - type="button" - class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded text-sm font-medium transition-colors" - > - Save changes - </button> - </div> - </CardComponent> -</template> diff --git a/frontend/src/components/client/Dashboard/Admin/Settings/Dashboard.vue b/frontend/src/components/client/Dashboard/Admin/Settings/Dashboard.vue deleted file mode 100755 index 2c40a38..0000000 --- a/frontend/src/components/client/Dashboard/Admin/Settings/Dashboard.vue +++ /dev/null @@ -1,128 +0,0 @@ -<script setup lang="ts"> -import { ref } from 'vue'; -import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; -import TextInput from '@/components/client/ui/TextForms/TextInput.vue'; -import SelectInput from '@/components/client/ui/TextForms/SelectInput.vue'; - -const serverConfig = ref({ - appName: 'MythicalSystems', - domain: 'mythicalsystems.xyz', - logo: 'https://github.com/mythicalltd.png', - theme: 'mythicalui', - accentColor: 'purple', - language: 'en', -}); - -const accentColors = [ - { value: 'purple', class: 'bg-purple-500' }, - { value: 'blue', class: 'bg-blue-500' }, - { value: 'green', class: 'bg-green-500' }, - { value: 'red', class: 'bg-red-500' }, - { value: 'yellow', class: 'bg-yellow-500' }, - { value: 'pink', class: 'bg-pink-500' }, -]; -</script> -<template> - <CardComponent cardTitle="Settings" cardDescription="Customize your application settings."> - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> - <!-- Basic Settings --> - <div class="space-y-4"> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Application Name</label> - <TextInput - v-model="serverConfig.appName" - type="text" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Domain</label> - <TextInput - v-model="serverConfig.domain" - type="text" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Application Logo</label> - <div class="flex items-center gap-4"> - <img - :src="serverConfig.logo || '/placeholder.svg?height=40&width=40'" - alt="Logo" - class="w-10 h-10 rounded bg-gray-700" - /> - <TextInput - v-model="serverConfig.logo" - type="text" - placeholder="Enter logo URL" - class="flex-1 bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - <button - type="button" - class="px-3 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm font-medium transition-colors" - > - Upload - </button> - </div> - </div> - </div> - - <!-- Theme Settings --> - <div class="space-y-4"> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Theme</label> - <SelectInput - v-model="serverConfig.theme" - :options="[ - { value: 'mythicalui', label: 'MythicalUI' }, - { value: 'vuexy', label: 'Vuexy' }, - { value: 'hopeui', label: 'HopeUI' }, - ]" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Accent Color</label> - <div class="flex flex-wrap gap-2 ml-1"> - <button - v-for="color in accentColors" - :key="color.value" - @click="serverConfig.accentColor = color.value" - class="w-10 h-10 rounded-full border-2 focus:outline-none" - :class="[ - color.class, - serverConfig.accentColor === color.value ? 'border-white' : 'border-transparent', - ]" - /> - </div> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Language</label> - <SelectInput - v-model="serverConfig.language" - :options="[ - { value: 'en', label: 'English' }, - { value: 'es', label: 'Español' }, - { value: 'fr', label: 'Français' }, - { value: 'de', label: 'Deutsch' }, - { value: 'it', label: 'Italiano' }, - { value: 'pt', label: 'Português' }, - { value: 'ru', label: 'Русский' }, - { value: 'zh', label: '中文' }, - ]" - /> - </div> - </div> - </div> - <div class="mt-6"> - <hr class="border-gray-700/50" /> - </div> - <div class="flex justify-start mt-4"> - <button - type="button" - class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded text-sm font-medium transition-colors" - > - Save changes - </button> - </div> - </CardComponent> -</template> diff --git a/frontend/src/components/client/Dashboard/Admin/Settings/Email.vue b/frontend/src/components/client/Dashboard/Admin/Settings/Email.vue deleted file mode 100755 index c00f13c..0000000 --- a/frontend/src/components/client/Dashboard/Admin/Settings/Email.vue +++ /dev/null @@ -1,98 +0,0 @@ -<script setup lang="ts"> -import { ref } from 'vue'; -import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; -import TextInput from '@/components/client/ui/TextForms/TextInput.vue'; -import SelectInput from '@/components/client/ui/TextForms/SelectInput.vue'; - -const emailConfig = ref({ - host: 'smtp.mythicalsystems.xyz', - port: '587', - username: 'noreply@mythicalsystems.xyz', - password: '', - fromEmail: 'noreply@mythicalsystems.xyz', - encryption: 'tls', -}); - -function testEmailConnection() {} - -function saveEmailSettings() {} -</script> - -<template> - <CardComponent - cardTitle="Email Settings" - cardDescription="Use our fancy email server to send emails for password resets logins or other cool stuff!" - > - <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">SMTP Host</label> - <TextInput - v-model="emailConfig.host" - type="text" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">SMTP Port</label> - <TextInput - v-model="emailConfig.port" - type="number" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Username</label> - <TextInput - v-model="emailConfig.username" - type="text" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Password</label> - <TextInput - v-model="emailConfig.password" - type="password" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">From Email</label> - <TextInput - v-model="emailConfig.fromEmail" - type="email" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - <div> - <label class="block text-sm font-medium text-gray-400 mb-1">Encryption</label> - <SelectInput - v-model="emailConfig.encryption" - :options="[ - { value: 'tls', label: 'TLS' }, - { value: 'ssl', label: 'SSL' }, - { value: 'none', label: 'None' }, - ]" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded px-3 py-2 text-sm text-gray-100 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50" - /> - </div> - </div> - <div class="mt-6"> - <hr class="border-gray-700/50" /> - </div> - <div class="mt-4 flex space-x-4"> - <button - @click="saveEmailSettings" - class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded text-sm font-medium transition-colors" - > - Save Settings - </button> - <button - @click="testEmailConnection" - class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm font-medium transition-colors" - > - Test Connection - </button> - </div> - </CardComponent> -</template> diff --git a/frontend/src/components/client/LayoutDashboard.vue b/frontend/src/components/client/LayoutDashboard.vue index 82c0086..ef3d07e 100755 --- a/frontend/src/components/client/LayoutDashboard.vue +++ b/frontend/src/components/client/LayoutDashboard.vue @@ -1,459 +1,255 @@ <template> - <div class="min-h-screen bg-gradient text-gray-100"> - <div v-if="loading" class="fixed inset-0 z-50 flex items-center justify-center bg-gradient"> - <div class="text-center"> - <div class="w-16 h-16 mb-4 mx-auto"> - <svg class="animate-spin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <circle - class="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - stroke-width="4" - ></circle> - <path - class="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" - ></path> - </svg> - </div> - <div - class="text-xl font-bold bg-gradient-to-r from-purple-400 to-purple-600 bg-clip-text text-transparent" - > - Loading... - </div> - </div> - </div> - - <!-- Mobile Sidebar Overlay --> - <template v-if="!loading"> - <div - v-if="isSidebarOpen" - class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 lg:hidden" - @click="closeSidebar" - ></div> - - <!-- Top Navigation Bar --> - <nav - class="fixed top-0 left-0 right-0 h-16 bg-gray-900/50 backdrop-blur-sm border-b border-gray-700/50 z-30" - > - <div class="h-full px-4 flex items-center justify-between"> - <!-- Left: Logo & Menu Button --> - <div class="flex items-center gap-3"> - <button class="lg:hidden p-2 hover:bg-gray-800/50 rounded-lg" @click="toggleSidebar"> - <MenuIcon v-if="!isSidebarOpen" class="w-5 h-5" /> - <XIcon v-else class="w-5 h-5" /> - </button> - - <div class="flex items-center gap-2"> - <div class="w-8 h-8 flex items-center justify-center"> - <img :src="Settings.getSetting('app_logo')" alt="MythicalClient" class="h-6 w-6" /> - </div> - <span - class="text-xl font-bold bg-gradient-to-r from-purple-400 to-purple-600 bg-clip-text text-transparent" - > - {{ Settings.getSetting('app_name') }} - </span> - <!-- Search Bar (Desktop) --> - <div class="hidden lg:block absolute left-1/2 transform -translate-x-1/2"> - <div class="relative"> - <SearchIcon class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" /> - <input - type="text" - placeholder="Search (Ctrl + /)" - class="px-10 py-2 w-64 bg-gray-800/50 border border-gray-700/50 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/50" - @click="toggleSearch" - readonly - /> - </div> - </div> - <!-- Search Icon (Mobile) --> - <button class="lg:hidden p-2 hover:bg-gray-800/50 rounded-lg" @click="toggleSearch"> - <SearchIcon class="w-5 h-5" /> - </button> - </div> - </div> - - <!-- Right: Actions --> - <div class="flex items-center gap-2"> - <button @click="toggleNotifications" class="p-2 hover:bg-gray-800/50 rounded-lg relative"> - <BellIcon class="w-5 h-5" /> - <span class="absolute top-1.5 right-1.5 w-2 h-2 bg-purple-500 rounded-full"></span> - </button> - - <button @click="toggleProfile" class="p-2 hover:bg-gray-800/50 rounded-lg"> - <UserIcon class="w-5 h-5" /> - </button> - </div> - </div> - </nav> - - <!-- Sidebar --> - <aside - class="fixed top-0 left-0 h-full w-64 bg-gray-900/50 backdrop-blur-sm border-r border-gray-700/50 transform transition-transform duration-200 ease-in-out z-50 lg:translate-x-0 lg:z-20" - :class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full'" - > - <!-- Sidebar Content --> - <div class="flex flex-col h-full pt-16"> - <div class="flex-1 overflow-y-auto"> - <nav class="p-4"> - <div v-for="(section, index) in menuSections" :key="index" class="mb-6"> - <div class="text-xs uppercase tracking-wider text-gray-500 font-medium px-4 mb-2"> - {{ section.title }} - </div> - <div class="space-y-1"> - <RouterLink - v-for="item in section.items" - :key="item.name" - :to="item.href" - class="flex items-center gap-3 px-4 py-2 rounded-lg hover:bg-gray-800/50 transition-colors" - :class="{ 'bg-purple-500/10 text-purple-400': item.active }" - > - <component :is="item.icon" class="w-5 h-5" /> - {{ item.name }} - </RouterLink> - </div> - </div> - </nav> - </div> - </div> - </aside> - - <!-- Main Content --> - <main class="pt-16 lg:pl-64 min-h-screen"> - <div class="p-6"> - <slot></slot> - </div> - </main> - - <!-- Search Modal --> - <div v-if="isSearchOpen" class="fixed inset-0 bg-gray-900/95 backdrop-blur-sm z-50" @click="closeSearch"> - <div class="max-w-3xl mx-auto pt-32 px-4" @click.stop> - <div class="relative"> - <SearchIcon class="absolute left-4 top-3.5 h-5 w-5 text-gray-400" /> - <input - type="text" - placeholder="Search (Ctrl+/)" - class="w-full bg-gray-800/50 border border-gray-700/50 rounded-lg pl-11 pr-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500/50" - @keydown.esc="closeSearch" - ref="searchInput" - /> - </div> - </div> - </div> - - <!-- Notifications Dropdown --> - <div - v-if="isNotificationsOpen" - class="absolute top-16 right-4 w-80 bg-gray-900/95 backdrop-blur-sm border border-gray-700/50 rounded-lg shadow-xl z-50 dropdown" - > - <div class="p-4"> - <h3 class="font-semibold mb-4 text-purple-400">Notifications</h3> - <div class="space-y-4"> - <div - v-for="notification in notifications" - :key="notification.id" - class="flex items-start gap-3 p-2 hover:bg-gray-800/50 rounded-lg transition-colors" - > - <div - class="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center flex-shrink-0" - > - <component :is="notification.icon" class="h-4 w-4 text-purple-400" /> - </div> - <div> - <p class="font-medium">{{ notification.title }}</p> - <p class="text-sm text-gray-400">{{ notification.time }}</p> - </div> - </div> - </div> - </div> - </div> - - <!-- Profile Dropdown --> - <div - v-if="isProfileOpen" - class="absolute top-16 right-4 w-64 bg-gray-900/95 backdrop-blur-sm border border-gray-700/50 rounded-lg shadow-xl z-50 dropdown" - > - <div class="p-4"> - <div class="flex items-center gap-3 mb-4"> - <div class="h-10 w-10 rounded-full bg-purple-500/20 flex items-center justify-center"> - <UserIcon class="h-6 w-6 text-purple-400" /> - </div> - <div> - <p class="font-medium"> - {{ Session.getInfo('first_name') }} {{ Session.getInfo('last_name') }} - </p> - <p class="text-sm text-gray-400">{{ Session.getInfo('role_name') }}</p> - </div> - </div> - <div class="space-y-1"> - <RouterLink - :to="item.href" - v-for="item in profileMenu" - :key="item.name" - class="w-full text-left px-3 py-2 rounded-lg hover:bg-gray-800/50 transition-colors flex items-center gap-3" - > - <component :is="item.icon" class="h-5 w-5 text-purple-400" /> - {{ item.name }} - </RouterLink> - </div> - </div> - </div> - <br /> - <br /> - <!-- Footer --> - <footer - class="fixed bottom-0 left-0 right-0 bg-gray-900/50 backdrop-blur-sm border-t border-gray-700/50 py-4 z-50" - > - <div class="flex justify-between items-center text-gray-400 text-sm px-6"> - <!-- Left side --> - <div class="flex items-center gap-4"> - <span>Copyright © 2020-{{ new Date().getFullYear() }} MythicalSystems</span> - </div> - <!-- Right side --> - <div class="flex gap-4"> - <RouterLink to="/terms" class="hover:text-purple-400 transition-colors">Terms</RouterLink> - <RouterLink to="/privacy" class="hover:text-purple-400 transition-colors">Privacy</RouterLink> - <RouterLink to="/support" class="hover:text-purple-400 transition-colors">Support</RouterLink> - </div> - </div> - </footer> - </template> + <div class="min-h-screen bg-[#0a0a1f] relative overflow-hidden"> + <!-- Background elements --> + <div class="absolute inset-0 bg-gradient-to-b from-[#0a0a1f] via-[#1a0b2e] to-[#0a0a1f]"> + <div class="stars"></div> + <div class="mountains"></div> </div> + + <!-- Content wrapper --> + <div class="relative z-10 min-h-screen"> + <LoadingScreen v-if="loading" /> + + <template v-if="!loading"> + <!-- Backdrop for mobile sidebar --> + <div + v-if="isSidebarOpen" + class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 lg:hidden" + @click="closeSidebar" + ></div> + + <TopNavBar + :isSidebarOpen="isSidebarOpen" + @toggle-sidebar="toggleSidebar" + @toggle-search="toggleSearch" + @toggle-notifications="toggleNotifications" + @toggle-profile="toggleProfile" + class="bg-[#0a0a1f]/80 backdrop-blur-sm border-b border-purple-900/50" + /> + + <!-- Sidebar with updated styling --> + <Sidebar + :isSidebarOpen="isSidebarOpen" + class="bg-[#0a0a1f]/90 backdrop-blur-md border-r border-purple-900/50" + /> + + <!-- Main Content --> + <main class="pt-16 lg:pl-64 min-h-screen relative"> + <div class="p-4 md:p-6 max-w-7xl mx-auto"> + <slot></slot> + </div> + </main> + + <!-- Modals and dropdowns --> + <SearchModal + :isSearchOpen="isSearchOpen" + @close="closeSearch" + @navigate="navigateToResult" + class="bg-[#0a0a1f]/95 backdrop-blur-lg" + /> + + <NotificationsDropdown + :isOpen="isNotificationsOpen" + class="bg-[#0a0a1f]/95 backdrop-blur-lg border border-purple-900/50" + /> + + <ProfileDropdown + :isOpen="isProfileOpen" + :profileMenu="profileMenu" + :userInfo="userInfo" + class="bg-[#0a0a1f]/95 backdrop-blur-lg border border-purple-900/50" + /> + + <!-- Footer --> + <footer class="relative z-10 py-4 px-6 text-center text-sm text-gray-400"> + <a href="https://mythical.systems" class="hover:text-purple-400 transition-colors"> + MythicalSystems + </a> + <p>LTD 2020 - {{ new Date().getFullYear() }}</p> + </footer> + </template> + </div> + </div> </template> + <script setup lang="ts"> -import { ref, onMounted, onUnmounted, type FunctionalComponent } from 'vue'; -import { - Search as SearchIcon, - Bell as BellIcon, - User as UserIcon, - Menu as MenuIcon, - X as XIcon, - Users as UsersIcon, - Settings as SettingsIcon, - LogOut as LogOutIcon, - LayoutDashboard as LayoutDashboardIcon, - Server as ServerIcon, - Database as DatabaseIcon, - AlertTriangle as AlertTriangleIcon, - Ticket as TicketIcon, -} from 'lucide-vue-next'; -import Settings from '@/mythicalclient/Settings'; -import Session from '@/mythicalclient/Session'; -import router from '@/router'; -import Translation from '@/mythicalclient/Translation'; -import StorageMonitor from '@/mythicalclient/StorageMonitor'; - -new StorageMonitor(); +import { ref, onMounted, onUnmounted, computed } from 'vue' +import { useRouter } from 'vue-router' +import LoadingScreen from '@/components/client/ui/LoadingScreen.vue' +import TopNavBar from '@/components/client/layout/TopNavBar.vue' +import Sidebar from '@/components/client/layout/Sidebar.vue' +import SearchModal from '@/components/client/layout/SearchModal.vue' +import NotificationsDropdown from '@/components/client/layout/NotificationsDropdown.vue' +import ProfileDropdown from '@/components/client/layout/ProfileDropdown.vue' +import { SettingsIcon, LogOutIcon, UsersIcon } from 'lucide-vue-next' +import Session from '@/mythicalclient/Session' +import StorageMonitor from '@/mythicalclient/StorageMonitor' + +new StorageMonitor() + +const router = useRouter() if (!Session.isSessionValid()) { - router.push('/auth/login'); + router.push('/auth/login') } try { - Session.startSession(); + Session.startSession() } catch (error) { - console.error('Session failed:', error); + console.error('Session failed:', error) } -const loading = ref(true); - -// Mobile Sidebar State -const isSidebarOpen = ref(false); +const loading = ref(true) +const isSidebarOpen = ref(false) +const isSearchOpen = ref(false) +const isNotificationsOpen = ref(false) +const isProfileOpen = ref(false) +// Toggle functions const toggleSidebar = () => { - isSidebarOpen.value = !isSidebarOpen.value; -}; - -const closeSidebar = () => { - isSidebarOpen.value = false; -}; - -// Dropdowns State -const isSearchOpen = ref(false); -const isNotificationsOpen = ref(false); -const isProfileOpen = ref(false); -const searchInput = ref<HTMLInputElement | null>(null); - -// Menu Sections -const isActiveRoute = (routes: string | string[]) => { - return routes.includes(window.location.pathname); -}; - -const menuSections = ref([ - { - title: 'General', - items: [ - { - name: Translation.getTranslation('components.sidebar.dashboard'), - icon: LayoutDashboardIcon, - href: '/', - active: isActiveRoute(['/']), - }, - { - name: Translation.getTranslation('components.sidebar.tickets'), - icon: TicketIcon, - href: '/ticket', - active: isActiveRoute(['/ticket']), - }, - ], - }, -]); - -// Profile Menu - -interface ProfileMenuItem { - name: string; - icon: FunctionalComponent; - href: string; + isSidebarOpen.value = !isSidebarOpen.value } -const profileMenu = ref<ProfileMenuItem[]>([]); - -const role = Session.getInfo('role_real_name'); -if (['admin', 'administrator', 'support', 'supportbuddy'].includes(role)) { - profileMenu.value = [ - { name: 'Settings', icon: SettingsIcon, href: '/account' }, - { name: 'Admin Area', icon: UsersIcon, href: '/mc-admin' }, - { name: 'Logout', icon: LogOutIcon, href: '/auth/logout' }, - ]; - console.log('User is Admin: ', role); -} else { - profileMenu.value = [ - { name: 'Settings', icon: SettingsIcon, href: '/account' }, - { name: 'Logout', icon: LogOutIcon, href: '/auth/logout' }, - ]; - console.log('User is not Admin:', role); +const closeSidebar = () => { + isSidebarOpen.value = false } -// Sample Notifications -const notifications = ref([ - { id: 1, title: 'High CPU Usage Alert', time: '5 minutes ago', icon: AlertTriangleIcon }, - { id: 2, title: 'System Update Available', time: '1 hour ago', icon: ServerIcon }, - { id: 3, title: 'Backup Completed', time: '2 hours ago', icon: DatabaseIcon }, -]); - -// Toggle Functions const toggleSearch = () => { - isSearchOpen.value = true; - isNotificationsOpen.value = false; - isProfileOpen.value = false; - setTimeout(() => { - searchInput.value?.focus(); - }, 100); -}; + isSearchOpen.value = true + isNotificationsOpen.value = false + isProfileOpen.value = false +} const toggleNotifications = () => { - isNotificationsOpen.value = !isNotificationsOpen.value; - isProfileOpen.value = false; - isSearchOpen.value = false; -}; + isNotificationsOpen.value = !isNotificationsOpen.value + isProfileOpen.value = false + isSearchOpen.value = false +} const toggleProfile = () => { - isProfileOpen.value = !isProfileOpen.value; - isNotificationsOpen.value = false; - isSearchOpen.value = false; -}; + isProfileOpen.value = !isProfileOpen.value + isNotificationsOpen.value = false + isSearchOpen.value = false +} -// Close Functions const closeSearch = () => { - isSearchOpen.value = false; -}; + isSearchOpen.value = false +} + +const navigateToResult = (href: string) => { + closeSearch() + router.push(href) +} const closeDropdowns = () => { - isNotificationsOpen.value = false; - isProfileOpen.value = false; -}; + isNotificationsOpen.value = false + isProfileOpen.value = false +} -// Click Outside Handler +// Event handlers const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement | null; - if (target && !target.closest('.dropdown') && !target.closest('button')) { - closeDropdowns(); - } -}; + const target = event.target as HTMLElement | null + if (target && !target.closest('.dropdown') && !target.closest('button')) { + closeDropdowns() + } +} -// Keyboard Shortcuts const handleKeydown = (event: KeyboardEvent) => { - if (event.ctrlKey && event.key === '/') { - event.preventDefault(); - toggleSearch(); - } - if (event.key === 'Escape') { - closeSearch(); - closeDropdowns(); - closeSidebar(); - } -}; - -// Lifecycle Hooks -onMounted(() => { - document.addEventListener('click', handleClickOutside); - document.addEventListener('keydown', handleKeydown); - document.addEventListener('visibilitychange', handleVisibilityChange); - if (sessionStorage.getItem('firstLoad') === null) { - loading.value = true; - setTimeout(() => { - loading.value = false; - sessionStorage.setItem('firstLoad', 'false'); - }, 2000); - } else { - loading.value = false; - } -}); + if (event.ctrlKey && event.key === 'S') { + event.preventDefault() + toggleSearch() + } + if (event.key === 'Escape') { + closeSearch() + closeDropdowns() + closeSidebar() + } +} -onUnmounted(() => { - document.removeEventListener('click', handleClickOutside); - document.removeEventListener('keydown', handleKeydown); - document.removeEventListener('visibilitychange', handleVisibilityChange); -}); +const handleVisibilityChange = () => { + document.title = document.hidden + ? `${document.title} - Inactive` + : document.title.replace(' - Inactive', '') +} -const isPageActive = ref(true); +// Lifecycle hooks +onMounted(() => { + document.addEventListener('click', handleClickOutside) + document.addEventListener('keydown', handleKeydown) + document.addEventListener('visibilitychange', handleVisibilityChange) -const handleVisibilityChange = () => { - if (document.hidden) { - isPageActive.value = false; - document.title = `${document.title} - Inactive`; - console.log('Page is inactive'); - } else { - isPageActive.value = true; - document.title = document.title.replace(' - Inactive', ''); - console.log('Page is active'); - } -}; + if (sessionStorage.getItem('firstLoad') === null) { + loading.value = true + setTimeout(() => { + loading.value = false + sessionStorage.setItem('firstLoad', 'false') + }, 2000) + } else { + loading.value = false + } +}) + +onUnmounted(() => { + document.removeEventListener('click', handleClickOutside) + document.removeEventListener('keydown', handleKeydown) + document.removeEventListener('visibilitychange', handleVisibilityChange) +}) + +// Computed properties +const profileMenu = computed(() => { + const menu = [ + { name: 'Settings', icon: SettingsIcon, href: '/account' }, + { name: 'Logout', icon: LogOutIcon, href: '/auth/logout' }, + ] + + const role = Session.getInfo('role_real_name') + if (['admin', 'administrator', 'support', 'supportbuddy'].includes(role)) { + menu.splice(1, 0, { name: 'Admin Area', icon: UsersIcon, href: '/mc-admin' }) + } + + return menu +}) + +const userInfo = computed(() => ({ + firstName: Session.getInfo('first_name'), + lastName: Session.getInfo('last_name'), + roleName: Session.getInfo('role_name'), +})) </script> <style scoped> -.bg-gradient { - background: radial-gradient(circle at center, #2d1b69 0%, #1a103f 50%, #0f0a24 100%); +.stars { + position: absolute; + inset: 0; + background-image: radial-gradient(2px 2px at calc(random() * 100%) calc(random() * 100%), white, transparent); + background-size: 200px 200px; + animation: twinkle 8s infinite; } -:deep(.dropdown) { - animation: dropdown 0.2s ease-out; -} - -@keyframes dropdown { - from { - opacity: 0; - transform: translateY(-8px); - } - - to { - opacity: 1; - transform: translateY(0); - } +.mountains { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 30vh; + background-image: linear-gradient(170deg, transparent 0%, #0a0a1f 80%), + linear-gradient(150deg, #1a0b2e 0%, transparent 100%); + clip-path: polygon(0 100%, 20% 65%, 40% 90%, 60% 60%, 80% 85%, 100% 50%, 100% 100%); } -/* Preloader animation */ -@keyframes spin { - to { - transform: rotate(360deg); - } +@keyframes twinkle { + 0%, 100% { + opacity: 0.8; + } + 50% { + opacity: 0.4; + } } -.animate-spin { - animation: spin 1s linear infinite; +/* Mobile optimizations */ +@media (max-width: 768px) { + .mountains { + height: 20vh; + } } </style> diff --git a/frontend/src/components/client/layout/Footer.vue b/frontend/src/components/client/layout/Footer.vue new file mode 100755 index 0000000..a8b00a7 --- /dev/null +++ b/frontend/src/components/client/layout/Footer.vue @@ -0,0 +1,22 @@ +<template> + <footer class="fixed bottom-0 left-0 right-0 bg-gray-900/50 backdrop-blur-sm border-t border-gray-700/50 py-4 z-50"> + <div class="flex justify-between items-center text-gray-400 text-sm px-6"> + <!-- Left side --> + <div class="flex items-center gap-4"> + <span>Copyright © 2020-{{ currentYear }} MythicalSystems</span> + </div> + <!-- Right side --> + <div class="flex gap-4"> + <RouterLink to="/terms" class="hover:text-purple-400 transition-colors">Terms</RouterLink> + <RouterLink to="/privacy" class="hover:text-purple-400 transition-colors">Privacy</RouterLink> + <RouterLink to="/support" class="hover:text-purple-400 transition-colors">Support</RouterLink> + </div> + </div> + </footer> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; + +const currentYear = computed(() => new Date().getFullYear()); +</script> diff --git a/frontend/src/components/client/layout/NotificationsDropdown.vue b/frontend/src/components/client/layout/NotificationsDropdown.vue new file mode 100755 index 0000000..73253c5 --- /dev/null +++ b/frontend/src/components/client/layout/NotificationsDropdown.vue @@ -0,0 +1,57 @@ +<template> + <div + v-if="isOpen" + class="absolute top-16 right-4 w-80 bg-gray-900/95 backdrop-blur-sm border border-gray-700/50 rounded-lg shadow-xl z-50 dropdown" + > + <div class="p-4"> + <h3 class="font-semibold mb-4 text-purple-400">Notifications</h3> + <div class="space-y-4"> + <div + v-for="notification in notifications" + :key="notification.id" + class="flex items-start gap-3 p-2 hover:bg-gray-800/50 rounded-lg transition-colors" + > + <div class="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center flex-shrink-0"> + <component :is="notification.icon" class="h-4 w-4 text-purple-400" /> + </div> + <div> + <p class="font-medium">{{ notification.title }}</p> + <p class="text-sm text-gray-400">{{ notification.time }}</p> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { AlertTriangle as AlertTriangleIcon, Server as ServerIcon, Database as DatabaseIcon } from 'lucide-vue-next'; + +defineProps<{ + isOpen: boolean; +}>(); + +const notifications = [ + { id: 1, title: 'High CPU Usage Alert', time: '5 minutes ago', icon: AlertTriangleIcon }, + { id: 2, title: 'System Update Available', time: '1 hour ago', icon: ServerIcon }, + { id: 3, title: 'Backup Completed', time: '2 hours ago', icon: DatabaseIcon }, +]; +</script> + +<style scoped> +.dropdown { + animation: dropdown 0.2s ease-out; +} + +@keyframes dropdown { + from { + opacity: 0; + transform: translateY(-8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} +</style> diff --git a/frontend/src/components/client/layout/ProfileDropdown.vue b/frontend/src/components/client/layout/ProfileDropdown.vue new file mode 100755 index 0000000..5c21c11 --- /dev/null +++ b/frontend/src/components/client/layout/ProfileDropdown.vue @@ -0,0 +1,65 @@ +<template> + <div + v-if="isOpen" + class="absolute top-16 right-4 w-64 bg-gray-900/95 backdrop-blur-sm border border-gray-700/50 rounded-lg shadow-xl z-50 dropdown" + > + <div class="p-4"> + <div class="flex items-center gap-3 mb-4"> + <div class="h-10 w-10 rounded-full bg-purple-500/20 flex items-center justify-center"> + <UserIcon class="h-6 w-6 text-purple-400" /> + </div> + <div> + <p class="font-medium">{{ userInfo.firstName }} {{ userInfo.lastName }}</p> + <p class="text-sm text-gray-400">{{ userInfo.roleName }}</p> + </div> + </div> + <div class="space-y-1"> + <RouterLink + :to="item.href" + v-for="item in profileMenu" + :key="item.name" + class="w-full text-left px-3 py-2 rounded-lg hover:bg-gray-800/50 transition-colors flex items-center gap-3" + > + <component :is="item.icon" class="h-5 w-5 text-purple-400" /> + {{ item.name }} + </RouterLink> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { UserIcon } from 'lucide-vue-next'; + +defineProps<{ + isOpen: boolean; + profileMenu: Array<{ + name: string; + icon: unknown; + href: string; + }>; + userInfo: { + firstName: string; + lastName: string; + roleName: string; + }; +}>(); +</script> + +<style scoped> +.dropdown { + animation: dropdown 0.2s ease-out; +} + +@keyframes dropdown { + from { + opacity: 0; + transform: translateY(-8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} +</style> diff --git a/frontend/src/components/client/layout/SearchModal.vue b/frontend/src/components/client/layout/SearchModal.vue new file mode 100755 index 0000000..51f5554 --- /dev/null +++ b/frontend/src/components/client/layout/SearchModal.vue @@ -0,0 +1,143 @@ +<template> + <div v-if="isSearchOpen" class="fixed inset-0 bg-gray-900/95 backdrop-blur-sm z-50" @click="$emit('close')"> + <div class="max-w-3xl mx-auto pt-32 px-4" @click.stop> + <div class="relative"> + <SearchIcon class="absolute left-4 top-3.5 h-5 w-5 text-gray-400" /> + <input + type="text" + placeholder="Search (Ctrl+/)" + v-model="searchQuery" + class="w-full bg-gray-800/50 border border-gray-700/50 rounded-lg pl-11 pr-4 py-3 focus:outline-none focus:ring-2 focus:ring-purple-500/50" + @keydown.esc="$emit('close')" + @input="performSearch" + ref="searchInput" + /> + </div> + <!-- Search Results --> + <div v-if="searchResults.length > 0" class="mt-4 bg-gray-800/50 rounded-lg border border-gray-700/50"> + <div + v-for="result in searchResults" + :key="result.href" + class="p-4 hover:bg-gray-700/50 cursor-pointer" + @click="navigateToResult(result.href)" + > + <div class="flex items-center gap-3"> + <component :is="result.icon" class="w-5 h-5 text-purple-400" /> + <div> + <div class="font-medium">{{ result.title }}</div> + <div class="text-sm text-gray-400">{{ result.description }}</div> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref, onMounted, watch } from 'vue'; +import { + FileTextIcon, + GlobeIcon, + LayoutDashboardIcon, + Search as SearchIcon, + ServerIcon, + TicketIcon, +} from 'lucide-vue-next'; +import { useRouter } from 'vue-router'; + +const props = defineProps<{ + isSearchOpen: boolean; +}>(); + +const emit = defineEmits(['close', 'navigate']); + +const router = useRouter(); +const searchQuery = ref(''); +const searchResults = ref< + Array<{ + title: string; + description: string; + href: string; + icon: unknown; + }> +>([]); +const searchInput = ref<HTMLInputElement | null>(null); + +const searchableItems = [ + { + title: 'Dashboard', + description: 'View your main dashboard', + href: '/', + icon: LayoutDashboardIcon, + }, + { + title: 'Services', + description: 'Manage your services', + href: '/services', + icon: ServerIcon, + }, + { + title: 'Invoices', + description: 'View and manage invoices', + href: '/invoices', + icon: FileTextIcon, + }, + { + title: 'Support Tickets', + description: 'View your support tickets', + href: '/ticket', + icon: TicketIcon, + }, + { + title: 'Knowledge Base', + description: 'Browse help articles', + href: '/knowledge-base', + icon: GlobeIcon, + }, +]; + +const performSearch = () => { + if (!searchQuery.value.trim()) { + searchResults.value = []; + return; + } + + const lowercaseQuery = searchQuery.value.toLowerCase(); + searchResults.value = searchableItems.filter( + (item) => + item.title.toLowerCase().includes(lowercaseQuery) || + item.description.toLowerCase().includes(lowercaseQuery), + ); + + // Auto-navigate if there's an exact match + const exactMatch = searchResults.value.find((item) => item.title.toLowerCase() === lowercaseQuery); + if (exactMatch) { + navigateToResult(exactMatch.href); + } +}; + +const navigateToResult = (href: string) => { + emit('close'); + router.push(href); +}; + +watch( + () => props.isSearchOpen, + (newValue) => { + if (newValue) { + setTimeout(() => { + if (searchInput.value) { + searchInput.value.focus(); + } + }, 100); + } + }, +); + +onMounted(() => { + if (searchInput.value) { + searchInput.value.addEventListener('input', performSearch); + } +}); +</script> diff --git a/frontend/src/components/client/layout/Sidebar.vue b/frontend/src/components/client/layout/Sidebar.vue new file mode 100755 index 0000000..1475911 --- /dev/null +++ b/frontend/src/components/client/layout/Sidebar.vue @@ -0,0 +1,180 @@ +<template> + <aside + class="fixed top-0 left-0 h-full w-64 bg-gray-900/50 backdrop-blur-sm border-r border-gray-700/50 transform transition-transform duration-200 ease-in-out z-50 lg:translate-x-0 lg:z-20" + :class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full'" + > + <!-- Sidebar Content --> + <div class="flex flex-col h-full pt-16"> + <div class="flex-1 overflow-y-auto"> + <nav class="p-4"> + <div v-for="(section, index) in menuSections" :key="index" class="mb-6"> + <div class="text-xs uppercase tracking-wider text-gray-500 font-medium px-4 mb-2"> + {{ section.title }} + </div> + <div class="space-y-1"> + <template v-for="item in section.items" :key="item.name"> + <div v-if="item.subitems"> + <button + @click="toggleSubitems(item)" + class="w-full flex items-center justify-between gap-3 px-4 py-2 rounded-lg hover:bg-gray-800/50 transition-colors" + :class="{ 'bg-purple-500/10 text-purple-400': item.active }" + > + <div class="flex items-center gap-3"> + <component :is="item.icon" class="w-5 h-5" /> + {{ item.name }} + </div> + <ChevronDownIcon + class="w-4 h-4 transition-transform duration-200" + :class="{ 'rotate-180': item.expanded }" + /> + </button> + <transition + enter-active-class="transition-all duration-300 ease-in-out" + leave-active-class="transition-all duration-300 ease-in-out" + enter-from-class="opacity-0 max-h-0" + enter-to-class="opacity-100 max-h-[500px]" + leave-from-class="opacity-100 max-h-[500px]" + leave-to-class="opacity-0 max-h-0" + > + <div v-show="item.expanded" class="ml-4 mt-1 space-y-1 overflow-hidden"> + <RouterLink + v-for="subitem in item.subitems" + :key="subitem.name" + :to="subitem.href" + class="flex items-center gap-3 px-4 py-2 rounded-lg hover:bg-gray-800/50 transition-colors text-sm" + :class="{ 'bg-purple-500/10 text-purple-400': subitem.active }" + > + <component :is="subitem.icon" class="w-4 h-4" /> + {{ subitem.name }} + </RouterLink> + </div> + </transition> + </div> + <RouterLink + v-else + :to="item.href" + class="flex items-center gap-3 px-4 py-2 rounded-lg hover:bg-gray-800/50 transition-colors" + :class="{ 'bg-purple-500/10 text-purple-400': item.active }" + > + <component :is="item.icon" class="w-5 h-5" /> + {{ item.name }} + </RouterLink> + </template> + </div> + </div> + </nav> + </div> + </div> + </aside> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import { + AlertTriangleIcon, + ChevronDown as ChevronDownIcon, + CopyPlusIcon, + FileTextIcon, + GlobeIcon, + LayoutDashboardIcon, + ServerIcon, + TicketIcon, +} from 'lucide-vue-next'; +import Translation from '@/mythicalclient/Translation'; + +defineProps<{ + isSidebarOpen: boolean; +}>(); + +const isActiveRoute = (routes: string | string[]) => { + return routes.includes(window.location.pathname); +}; + +const menuSections = ref([ + { + title: 'General', + items: [ + { + name: Translation.getTranslation('components.sidebar.dashboard'), + icon: LayoutDashboardIcon, + href: '/', + active: isActiveRoute(['/dashboard']), + }, + { + name: 'Services', + icon: ServerIcon, + href: '/services', + active: isActiveRoute(['/services']), + expanded: false, + subitems: [ + { + name: 'All Services', + icon: ServerIcon, + href: '/services', + active: isActiveRoute(['/services']), + }, + { + name: 'Add Service', + icon: CopyPlusIcon, + href: '/services/add', + active: isActiveRoute(['/services/add']), + }, + ], + }, + { + name: 'Invoices', + icon: FileTextIcon, + href: '/invoices', + active: isActiveRoute(['/invoices']), + }, + ], + }, + { + title: 'Support', + items: [ + { + name: 'Knowledge Base', + icon: GlobeIcon, + href: '/knowledge-base', + active: isActiveRoute(['/knowledge-base']), + }, + { + name: Translation.getTranslation('components.sidebar.tickets'), + icon: TicketIcon, + href: '/ticket', + active: isActiveRoute(['/ticket']), + expanded: false, + subitems: [ + { + name: 'Open Ticket', + icon: AlertTriangleIcon, + href: '/ticket/open', + active: isActiveRoute(['/ticket/open']), + }, + { + name: 'All Tickets', + icon: TicketIcon, + href: '/ticket', + active: isActiveRoute(['/ticket']), + }, + ], + }, + { + name: 'Announcements', + icon: FileTextIcon, + href: '/announcements', + active: isActiveRoute(['/announcements']), + }, + ], + }, +]); +const toggleSubitems = (item: { expanded: boolean }) => { + item.expanded = !item.expanded; +}; +</script> + +<style scoped> +.rotate-180 { + transform: rotate(180deg); +} +</style> diff --git a/frontend/src/components/client/layout/TopNavBar.vue b/frontend/src/components/client/layout/TopNavBar.vue new file mode 100755 index 0000000..e6ca0f2 --- /dev/null +++ b/frontend/src/components/client/layout/TopNavBar.vue @@ -0,0 +1,75 @@ +<template> + <nav class="fixed top-0 left-0 right-0 h-16 bg-gray-900/50 backdrop-blur-sm border-b border-gray-700/50 z-30"> + <div class="h-full px-4 flex items-center justify-between"> + <!-- Left: Logo & Menu Button --> + <div class="flex items-center gap-3"> + <button class="lg:hidden p-2 hover:bg-gray-800/50 rounded-lg" @click="$emit('toggle-sidebar')"> + <MenuIcon v-if="!isSidebarOpen" class="w-5 h-5" /> + <XIcon v-else class="w-5 h-5" /> + </button> + + <div class="flex items-center gap-2"> + <div class="w-8 h-8 flex items-center justify-center"> + <img :src="appLogo" alt="MythicalClient" class="h-6 w-6" /> + </div> + <span + class="text-xl font-bold bg-gradient-to-r from-purple-400 to-purple-600 bg-clip-text text-transparent" + > + {{ appName }} + </span> + </div> + </div> + + <!-- Search Bar (Desktop) --> + <div class="hidden lg:block absolute left-1/2 transform -translate-x-1/2"> + <div class="relative"> + <SearchIcon class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" /> + <input + type="text" + placeholder="Search (Ctrl + /)" + class="px-10 py-2 w-64 bg-gray-800/50 border border-gray-700/50 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/50" + @click="$emit('toggle-search')" + readonly + /> + </div> + </div> + + <!-- Search Icon (Mobile) --> + <button class="lg:hidden p-2 hover:bg-gray-800/50 rounded-lg" @click="$emit('toggle-search')"> + <SearchIcon class="w-5 h-5" /> + </button> + + <!-- Right: Actions --> + <div class="flex items-center gap-2"> + <button @click="$emit('toggle-notifications')" class="p-2 hover:bg-gray-800/50 rounded-lg relative"> + <BellIcon class="w-5 h-5" /> + <span class="absolute top-1.5 right-1.5 w-2 h-2 bg-purple-500 rounded-full"></span> + </button> + + <button @click="$emit('toggle-profile')" class="p-2 hover:bg-gray-800/50 rounded-lg"> + <UserIcon class="w-5 h-5" /> + </button> + </div> + </div> + </nav> +</template> + +<script setup lang="ts"> +import { + Search as SearchIcon, + Bell as BellIcon, + User as UserIcon, + Menu as MenuIcon, + X as XIcon, +} from 'lucide-vue-next'; +import Settings from '@/mythicalclient/Settings'; + +defineProps<{ + isSidebarOpen: boolean; +}>(); + +defineEmits(['toggle-sidebar', 'toggle-search', 'toggle-notifications', 'toggle-profile']); + +const appLogo = Settings.getSetting('app_logo'); +const appName = Settings.getSetting('app_name'); +</script> diff --git a/frontend/src/components/client/ui/LoadingScreen.vue b/frontend/src/components/client/ui/LoadingScreen.vue new file mode 100755 index 0000000..90dd5a6 --- /dev/null +++ b/frontend/src/components/client/ui/LoadingScreen.vue @@ -0,0 +1,37 @@ +<template> + <div class="fixed inset-0 z-50 flex items-center justify-center bg-gradient"> + <div class="text-center"> + <div class="w-16 h-16 mb-4 mx-auto"> + <svg class="animate-spin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> + <path + class="opacity-75" + fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + ></path> + </svg> + </div> + <div class="text-xl font-bold bg-gradient-to-r from-purple-400 to-purple-600 bg-clip-text text-transparent"> + Loading... + </div> + </div> + </div> +</template> + +<script setup lang="ts"></script> + +<style scoped> +.bg-gradient { + background: radial-gradient(circle at center, #2d1b69 0%, #1a103f 50%, #0f0a24 100%); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} +</style> diff --git a/frontend/src/locale/en.yml b/frontend/src/locale/en.yml index 8e58e15..07b4f39 100755 --- a/frontend/src/locale/en.yml +++ b/frontend/src/locale/en.yml @@ -313,7 +313,7 @@ account: update_button: label: Update reset: Reset - emails: + emails: alerts: success: title: Success @@ -328,7 +328,7 @@ account: subTitle: See your emails! table: columns: - id: ID + id: ID subject: Subject date: Date from: From @@ -354,11 +354,11 @@ account: title: Two-Factor Authentication (2FA) subTitle: You want to be sure your account will not be stolen? enable_button: - description: "2FA is currently disabled. Enable it to secure your account." + description: '2FA is currently disabled. Enable it to secure your account.' label: Enable loading: Enabling... disable_button: - description: "2FA is currently enabled. Disable it to put your account into a less secure state." + description: '2FA is currently enabled. Disable it to put your account into a less secure state.' label: Disable loading: Disabling... password: diff --git a/frontend/src/mythicalclient/Activities.ts b/frontend/src/mythicalclient/Activities.ts index 637e340..2c871eb 100755 --- a/frontend/src/mythicalclient/Activities.ts +++ b/frontend/src/mythicalclient/Activities.ts @@ -1,7 +1,7 @@ class Activities { /** * Get the activities for the current session - * + * * @returns The response from the server */ public static async get() { @@ -13,4 +13,4 @@ class Activities { } } -export default Activities; \ No newline at end of file +export default Activities; diff --git a/frontend/src/mythicalclient/Mails.ts b/frontend/src/mythicalclient/Mails.ts index fff8446..9fe73f5 100755 --- a/frontend/src/mythicalclient/Mails.ts +++ b/frontend/src/mythicalclient/Mails.ts @@ -1,7 +1,7 @@ class Mails { /** * Get the Mails for the current session - * + * * @returns The response from the server */ public static async get() { @@ -13,4 +13,4 @@ class Mails { } } -export default Mails; \ No newline at end of file +export default Mails; diff --git a/frontend/src/mythicalclient/Settings.ts b/frontend/src/mythicalclient/Settings.ts index 3c814a5..79c2b16 100755 --- a/frontend/src/mythicalclient/Settings.ts +++ b/frontend/src/mythicalclient/Settings.ts @@ -43,7 +43,7 @@ class Settings { } for (const [key, value] of Object.entries(fetchedCore)) { - localStorage.setItem(key, JSON.stringify(value)); + localStorage.setItem(key, JSON.stringify(value)); } } catch (error) { console.error('Failed to initialize settings:', error); diff --git a/frontend/src/mythicalclient/admin/Dashboard.ts b/frontend/src/mythicalclient/admin/Dashboard.ts index 70c343d..169c7ec 100755 --- a/frontend/src/mythicalclient/admin/Dashboard.ts +++ b/frontend/src/mythicalclient/admin/Dashboard.ts @@ -1,7 +1,7 @@ class Dashboard { /** * Get the dashboard data - * + * * @returns */ public static async get() { diff --git a/frontend/src/views/admin/Home.vue b/frontend/src/views/admin/Home.vue index 8d15130..6072165 100755 --- a/frontend/src/views/admin/Home.vue +++ b/frontend/src/views/admin/Home.vue @@ -21,25 +21,30 @@ <Heart class="w-5 h-5 mr-2" /> <h2 class="font-medium text-gray-100">Support {{ Settings.getSetting('debug_name') }}</h2> </div> - <a href="https://github.com/sponsors/nayskutzu" - class="bg-gradient-to-r from-pink-500 to-violet-500 text-white px-4 py-2 rounded-lg transition-all duration-200 hover:opacity-80"> + <a + href="https://github.com/sponsors/nayskutzu" + class="bg-gradient-to-r from-pink-500 to-violet-500 text-white px-4 py-2 rounded-lg transition-all duration-200 hover:opacity-80" + > Donate </a> </div> - <p class="text-gray-400">Your support helps us continue to improve {{ - Settings.getSetting('debug_name')}}!</p> + <p class="text-gray-400"> + Your support helps us continue to improve {{ Settings.getSetting('debug_name') }}! + </p> </div> </div> <!-- Warning Section --> <div class="bg-gray-800/50 backdrop-blur-md rounded-lg p-4"> - <div class="flex items-center mb-2"> + <div class="flex items-center mb-2"> <InfoIcon class="w-5 h-5 mr-2" /> <h2 class="font-medium">Free Edition</h2> </div> - <p class="">You are currently using the free edition of MythicalClient. Upgrade to the - premium edition for more features and support.</p> + <p class=""> + You are currently using the free edition of MythicalClient. Upgrade to the premium edition for more + features and support. + </p> </div> - <br> + <br /> <!-- Help Section --> <div class="bg-gray-800/50 backdrop-blur-md rounded-lg p-4"> <div class="flex items-center justify-between mb-4"> @@ -47,8 +52,10 @@ <HelpCircle class="w-5 h-5 mr-2" /> <h2 class="font-medium text-gray-100">Need Help?</h2> </div> - <a href="https://www.mythical.systems/docs" - class="bg-gradient-to-r from-violet-500 to-pink-500 text-white px-4 py-2 rounded-lg transition-all duration-200 hover:opacity-80"> + <a + href="https://www.mythical.systems/docs" + class="bg-gradient-to-r from-violet-500 to-pink-500 text-white px-4 py-2 rounded-lg transition-all duration-200 hover:opacity-80" + > Read Docs </a> </div> diff --git a/frontend/src/views/client/Home.vue b/frontend/src/views/client/Home.vue index 4b63811..9925985 100755 --- a/frontend/src/views/client/Home.vue +++ b/frontend/src/views/client/Home.vue @@ -1,127 +1,220 @@ <template> <LayoutDashboard> - <div class="p-6"> - <h1 class="text-3xl font-bold mb-2">My Dashboard</h1> - <div class="text-gray-400"> - <RouterLink to="/" class="hover:text-gray-300">Portal Home</RouterLink> - <span class="mx-2">/</span> - <span>Client Area</span> - </div> - </div> - - <div class="px-6 grid gap-6 lg:grid-cols-4"> - <!-- Left Column --> - <div class="space-y-6"> - <!-- Support PIN --> - <CardComponent> - <h2 class="text-gray-400 mb-2">Support Pin</h2> - <div class="flex items-center gap-2"> - <span class="text-[#7cff4d] text-2xl font-mono">637238</span> - <button class="text-gray-400 hover:text-gray-300"> - <RefreshCcwIcon class="w-4 h-4" /> - </button> + <div class="p-6 space-y-6"> + <div class="flex justify-between items-center"> + <div> + <h1 class="text-3xl font-bold text-white mb-2">My Dashboard</h1> + <div class="text-purple-200 text-sm"> + <RouterLink to="/" class="hover:text-white transition-colors">Portal Home</RouterLink> + <span class="mx-2">/</span> + <span>Client Area</span> </div> - </CardComponent> - <!-- Profile Card --> - <CardComponent> - <div class="flex flex-col items-center text-center"> - <img - :src="`${Session.getInfo('avatar')}?height=80&width=80`" - alt="Profile" - class="w-20 h-20 rounded-full mb-4" - /> - <div class="text-xl mb-4"> - {{ Session.getInfo('company_name') }} ({{ Session.getInfo('vat_number') }}) - </div> - <div class="text-gray-400 text-sm space-y-1 mb-4"> - <div>{{ Session.getInfo('first_name') }} {{ Session.getInfo('last_name') }}</div> - <div>{{ Session.getInfo('address1') }}</div> - <div> - {{ Session.getInfo('city') }} ({{ Session.getInfo('postcode') }}), - {{ Session.getInfo('country') }} - </div> - </div> - <div class="flex gap-2"> - <RouterLink to="/account" class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded" - >Update - </RouterLink> - <a href="/api/auth/logout" class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded" - >Logout</a - > - </div> + </div> + <div class="flex items-center space-x-4"> + <div class="text-right"> + <p class="text-purple-200 text-sm">Welcome back,</p> + <p class="text-white font-semibold">{{ Session.getInfo('first_name') }}</p> </div> - </CardComponent> + <img + :src="`${Session.getInfo('avatar')}?height=40&width=40`" + alt="Profile" + class="w-10 h-10 rounded-full border-2 border-purple-500" + /> + </div> </div> - <!-- Main Content --> - <div class="lg:col-span-3 space-y-6"> - <!-- Stats Grid --> - <div class="grid grid-cols-2 lg:grid-cols-4 gap-4"> + + <div class="grid gap-6 lg:grid-cols-4"> + <!-- Left Column --> + <div class="space-y-6"> + <!-- Support PIN --> <CardComponent> - <ServerIcon class="w-5 h-5 text-blue-400 mb-4" /> - <div class="text-4xl font-bold text-blue-400 mb-2">2</div> - <div class="text-gray-400">Services</div> + <h2 class="text-purple-200 text-sm font-medium mb-2">Support Pin</h2> + <div class="flex items-center gap-2"> + <span class="text-emerald-400 text-2xl font-mono font-bold">637238</span> + <button class="text-purple-500 hover:text-white transition-colors"> + <RefreshCcwIcon class="w-4 h-4" /> + </button> + </div> </CardComponent> + + <!-- Profile Card --> <CardComponent> - <GlobeIcon class="w-5 h-5 text-blue-400 mb-4" /> - <div class="text-4xl font-bold text-blue-400 mb-2">0</div> - <div class="text-gray-400">Domains</div> + <div class="flex flex-col items-center text-center"> + <MarketIcon class="w-20 h-20 text-purple-500 mb-4" /> + <div class="text-xl text-white mb-2"> + {{ Session.getInfo('company_name') }} + </div> + <div class="text-purple-200 text-sm space-y-1 mb-4"> + <div>{{ Session.getInfo('vat_number') }}</div> + <div>{{ Session.getInfo('address1') }}</div> + <div> + {{ Session.getInfo('city') }} ({{ Session.getInfo('postcode') }}), + {{ Session.getInfo('country') }} + </div> + </div> + <div class="flex gap-2 w-full"> + <RouterLink + to="/account" + class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-sm" + > + Update + </RouterLink> + <a + href="/api/auth/logout" + class="flex-1 px-4 py-2 bg-purple-800 hover:bg-purple-700 text-white rounded transition-colors text-sm" + > + Logout + </a> + </div> + </div> </CardComponent> + + <!-- Billing Summary --> <CardComponent> - <FileTextIcon class="w-5 h-5 text-blue-400 mb-4" /> - <div class="text-4xl font-bold text-blue-400 mb-2">0</div> - <div class="text-gray-400">Unpaid Invoices</div> + <h2 class="text-white text-lg font-semibold mb-4">Billing Summary</h2> + <div class="space-y-2 text-sm"> + <div class="flex justify-between"> + <span class="text-purple-200">Current Balance:</span> + <span class="text-white font-medium">$250.00</span> + </div> + <div class="flex justify-between"> + <span class="text-purple-200">Next Invoice:</span> + <span class="text-white font-medium">$120.00</span> + </div> + <div class="flex justify-between"> + <span class="text-purple-200">Due Date:</span> + <span class="text-white font-medium">15 Jul 2023</span> + </div> + </div> + <RouterLink + to="/billing" + class="mt-4 block w-full px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-center text-sm" + > + View Billing Details + </RouterLink> </CardComponent> + </div> + + <!-- Main Content --> + <div class="lg:col-span-3 space-y-6"> + <!-- Stats Grid --> + <div class="grid grid-cols-3 gap-4"> + <CardComponent v-for="(stat, index) in stats" :key="index"> + <component :is="stat.icon" class="w-6 h-6 text-purple-500 mb-2" /> + <div class="text-3xl font-bold text-white mb-1">{{ stat.value }}</div> + <div class="text-purple-200 text-sm">{{ stat.label }}</div> + </CardComponent> + </div> + + <!-- Active Products --> <CardComponent> - <TicketIcon class="w-5 h-5 text-blue-400 mb-4" /> - <div class="text-4xl font-bold text-blue-400 mb-2">1</div> - <div class="text-gray-400">Tickets</div> + <div class="flex items-center justify-between mb-4"> + <h2 class="text-lg font-semibold text-white">Your Active Products/Services</h2> + <button class="text-purple-500 hover:text-white transition-colors"> + <MenuIcon class="w-5 h-5" /> + </button> + </div> + <div class="space-y-4"> + <div + v-for="(server, index) in servers" + :key="index" + class="flex items-center justify-between py-3 border-b border-purple-700 last:border-0" + > + <div> + <div class="font-medium text-white">{{ server.name }}</div> + <div class="text-sm text-purple-500">{{ server.hostname }}</div> + </div> + <div class="flex items-center gap-3"> + <span + class="px-2 py-1 bg-emerald-500/20 text-emerald-400 rounded text-xs font-medium" + > + Active + </span> + <button + class="px-3 py-1 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-sm" + > + Manage + </button> + </div> + </div> + </div> </CardComponent> - </div> - <!-- Active Products --> - <CardComponent> - <div class="p-4 border-b border-gray-800"> - <div class="flex items-center justify-between"> - <h2 class="text-lg font-semibold">Your Active Products/Services</h2> - <MenuIcon class="w-5 h-5 text-gray-400" /> + <!-- Recent Tickets --> + <CardComponent> + <h2 class="text-lg font-semibold text-white mb-4">Recent Tickets</h2> + <div class="space-y-3"> + <div + v-for="ticket in recentTickets" + :key="ticket.id" + class="flex items-center justify-between py-2 border-b border-purple-700 last:border-0" + > + <div> + <div class="font-medium text-white">{{ ticket.title }}</div> + <div class="text-sm text-purple-500">{{ ticket.date }}</div> + </div> + <span + :class="[ + 'px-2 py-1 rounded text-xs font-medium', + ticket.status === 'Open' + ? 'bg-yellow-500/20 text-yellow-400' + : 'bg-emerald-500/20 text-emerald-400', + ]" + > + {{ ticket.status }} + </span> + </div> </div> - </div> - <div class="p-4 space-y-4"> - <div - v-for="(server, index) in servers" - :key="index" - class="flex items-center justify-between py-3 border-b border-gray-800 last:border-0" + <RouterLink + to="/support" + class="mt-4 block w-full px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-center text-sm" > - <div> - <div class="font-medium">{{ server.name }}</div> - <div class="text-sm text-gray-400">{{ server.hostname }}</div> - </div> - <div class="flex items-center gap-3"> - <span class="px-2 py-1 bg-green-500/20 text-green-400 rounded text-sm">Active</span> - <button class="px-4 py-1 bg-gray-800 hover:bg-gray-700 rounded">Manage</button> + View All Tickets + </RouterLink> + </CardComponent> + + <!-- Announcements --> + <CardComponent> + <h2 class="text-lg font-semibold text-white mb-4">Announcements</h2> + <div class="space-y-4"> + <div + v-for="announcement in announcements" + :key="announcement.id" + class="border-b border-purple-700 last:border-0 pb-4 last:pb-0" + > + <h3 class="text-white font-medium mb-1">{{ announcement.title }}</h3> + <p class="text-purple-200 text-sm mb-2">{{ announcement.content }}</p> + <span class="text-purple-500 text-xs">{{ announcement.date }}</span> </div> </div> - </div> - </CardComponent> + </CardComponent> + </div> </div> </div> </LayoutDashboard> </template> <script setup lang="ts"> +import { ref } from 'vue'; import LayoutDashboard from '@/components/client/LayoutDashboard.vue'; import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; import Session from '@/mythicalclient/Session'; import { RefreshCcw as RefreshCcwIcon, Server as ServerIcon, - Globe as GlobeIcon, FileText as FileTextIcon, Ticket as TicketIcon, Menu as MenuIcon, + BookMarkedIcon as MarketIcon, } from 'lucide-vue-next'; -const servers = [ +const stats = [ + { icon: ServerIcon, value: '2', label: 'Services' }, + { icon: FileTextIcon, value: '0', label: 'Unpaid Invoices' }, + { icon: TicketIcon, value: '1', label: 'Tickets' }, +]; + +const servers = ref([ { name: 'Storage Root Server Frankfurt - Storage KVM S', hostname: 'backup2.mythical.systems', @@ -130,5 +223,26 @@ const servers = [ name: 'Storage Root Server Frankfurt - Storage KVM S', hostname: 'backup.mythical.systems', }, -]; +]); + +const recentTickets = ref([ + { id: 1, title: 'Server Performance Issue', date: '2023-07-01', status: 'Open' }, + { id: 2, title: 'Billing Inquiry', date: '2023-06-28', status: 'Closed' }, + { id: 3, title: 'Domain Transfer Request', date: '2023-06-25', status: 'Open' }, +]); + +const announcements = ref([ + { + id: 1, + title: 'Scheduled Maintenance', + content: 'We will be performing scheduled maintenance on July 15th from 2 AM to 4 AM UTC.', + date: '2023-07-05', + }, + { + id: 2, + title: 'New Feature: Two-Factor Authentication', + content: 'Enhance your account security with our new two-factor authentication feature.', + date: '2023-06-30', + }, +]); </script> diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1b61703..b9dec3a 100755 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -735,68 +735,68 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@^22.9.0": - version "22.10.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" - integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== + version "22.10.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.3.tgz#cdc2a89bf6e5d5e593fad08e83f74d7348d5dd10" + integrity sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw== dependencies: undici-types "~6.20.0" -"@typescript-eslint/eslint-plugin@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz#c78e363ab5fe3b21dd1c90d8be9581534417f78e" - integrity sha512-adig4SzPLjeQ0Tm+jvsozSGiCliI2ajeURDGHjZ2llnA+A67HihCQ+a3amtPhUakd1GlwHxSRvzOZktbEvhPPg== +"@typescript-eslint/eslint-plugin@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz#2b1e1b791e21d5fc27ddc93884db066444f597b5" + integrity sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.18.2" - "@typescript-eslint/type-utils" "8.18.2" - "@typescript-eslint/utils" "8.18.2" - "@typescript-eslint/visitor-keys" "8.18.2" + "@typescript-eslint/scope-manager" "8.19.0" + "@typescript-eslint/type-utils" "8.19.0" + "@typescript-eslint/utils" "8.19.0" + "@typescript-eslint/visitor-keys" "8.19.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.2.tgz#0379a2e881d51d8fcf7ebdfa0dd18eee79182ce2" - integrity sha512-y7tcq4StgxQD4mDr9+Jb26dZ+HTZ/SkfqpXSiqeUXZHxOUyjWDKsmwKhJ0/tApR08DgOhrFAoAhyB80/p3ViuA== +"@typescript-eslint/parser@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.19.0.tgz#f1512e6e5c491b03aabb2718b95becde22b15292" + integrity sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw== dependencies: - "@typescript-eslint/scope-manager" "8.18.2" - "@typescript-eslint/types" "8.18.2" - "@typescript-eslint/typescript-estree" "8.18.2" - "@typescript-eslint/visitor-keys" "8.18.2" + "@typescript-eslint/scope-manager" "8.19.0" + "@typescript-eslint/types" "8.19.0" + "@typescript-eslint/typescript-estree" "8.19.0" + "@typescript-eslint/visitor-keys" "8.19.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.2.tgz#d193c200d61eb0ddec5987c8e48c9d4e1c0510bd" - integrity sha512-YJFSfbd0CJjy14r/EvWapYgV4R5CHzptssoag2M7y3Ra7XNta6GPAJPPP5KGB9j14viYXyrzRO5GkX7CRfo8/g== +"@typescript-eslint/scope-manager@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz#28fa413a334f70e8b506a968531e0a7c9c3076dc" + integrity sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA== dependencies: - "@typescript-eslint/types" "8.18.2" - "@typescript-eslint/visitor-keys" "8.18.2" + "@typescript-eslint/types" "8.19.0" + "@typescript-eslint/visitor-keys" "8.19.0" -"@typescript-eslint/type-utils@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.2.tgz#5ad07e09002eee237591881df674c1c0c91ca52f" - integrity sha512-AB/Wr1Lz31bzHfGm/jgbFR0VB0SML/hd2P1yxzKDM48YmP7vbyJNHRExUE/wZsQj2wUCvbWH8poNHFuxLqCTnA== +"@typescript-eslint/type-utils@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz#41abd7d2e4cf93b6854b1fe6cbf416fab5abf89f" + integrity sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg== dependencies: - "@typescript-eslint/typescript-estree" "8.18.2" - "@typescript-eslint/utils" "8.18.2" + "@typescript-eslint/typescript-estree" "8.19.0" + "@typescript-eslint/utils" "8.19.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.2.tgz#5ebad5b384c8aa1c0f86cee1c61bcdbe7511f547" - integrity sha512-Z/zblEPp8cIvmEn6+tPDIHUbRu/0z5lqZ+NvolL5SvXWT5rQy7+Nch83M0++XzO0XrWRFWECgOAyE8bsJTl1GQ== +"@typescript-eslint/types@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.0.tgz#a190a25c5484a42b81eaad06989579fdeb478cbb" + integrity sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA== -"@typescript-eslint/typescript-estree@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.2.tgz#fffb85527f8304e29bfbbdc712f4515da9f8b47c" - integrity sha512-WXAVt595HjpmlfH4crSdM/1bcsqh+1weFRWIa9XMTx/XHZ9TCKMcr725tLYqWOgzKdeDrqVHxFotrvWcEsk2Tg== +"@typescript-eslint/typescript-estree@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz#6b4f48f98ffad6597379951b115710f4d68c9ccb" + integrity sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw== dependencies: - "@typescript-eslint/types" "8.18.2" - "@typescript-eslint/visitor-keys" "8.18.2" + "@typescript-eslint/types" "8.19.0" + "@typescript-eslint/visitor-keys" "8.19.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -804,22 +804,22 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.2.tgz#a2635f71904a84f9e47fe1b6f65a6d944ff1adf9" - integrity sha512-Cr4A0H7DtVIPkauj4sTSXVl+VBWewE9/o40KcF3TV9aqDEOWoXF3/+oRXNby3DYzZeCATvbdksYsGZzplwnK/Q== +"@typescript-eslint/utils@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.0.tgz#33824310e1fccc17f27fbd1030fd8bbd9a674684" + integrity sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.18.2" - "@typescript-eslint/types" "8.18.2" - "@typescript-eslint/typescript-estree" "8.18.2" + "@typescript-eslint/scope-manager" "8.19.0" + "@typescript-eslint/types" "8.19.0" + "@typescript-eslint/typescript-estree" "8.19.0" -"@typescript-eslint/visitor-keys@8.18.2": - version "8.18.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.2.tgz#b3e434b701f086b10a7c82416ebc56899d27ef2f" - integrity sha512-zORcwn4C3trOWiCqFQP1x6G3xTRyZ1LYydnj51cRnJ6hxBlr/cKPckk+PKPUw/fXmvfKTcw7bwY3w9izgx5jZw== +"@typescript-eslint/visitor-keys@8.19.0": + version "8.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz#dc313f735e64c4979c9073f51ffcefb6d9be5c77" + integrity sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w== dependencies: - "@typescript-eslint/types" "8.18.2" + "@typescript-eslint/types" "8.19.0" eslint-visitor-keys "^4.2.0" "@vitejs/plugin-vue-jsx@^4.0.1": @@ -2910,13 +2910,13 @@ type-fest@^0.20.2: integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== typescript-eslint@^8.18.1: - version "8.18.2" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.18.2.tgz#71334dcf843adc3fbb771dce5ade7876aa0d62b7" - integrity sha512-KuXezG6jHkvC3MvizeXgupZzaG5wjhU3yE8E7e6viOvAvD9xAWYp8/vy0WULTGe9DYDWcQu7aW03YIV3mSitrQ== + version "8.19.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.19.0.tgz#e4ff06b19f2f9807a2c26147a0199a109944d9e0" + integrity sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ== dependencies: - "@typescript-eslint/eslint-plugin" "8.18.2" - "@typescript-eslint/parser" "8.18.2" - "@typescript-eslint/utils" "8.18.2" + "@typescript-eslint/eslint-plugin" "8.19.0" + "@typescript-eslint/parser" "8.19.0" + "@typescript-eslint/utils" "8.19.0" typescript@~5.6.3: version "5.6.3" @@ -3175,9 +3175,9 @@ yallist@^3.0.2: integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^2.3.4: - version "2.6.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" - integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== + version "2.7.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== yargs-parser@^18.1.2: version "18.1.3"