├── src ├── api │ ├── task.ts │ ├── chat.ts │ ├── event.ts │ ├── user.ts │ └── type.ts ├── scss │ ├── components │ │ ├── _VCard.scss │ │ ├── _VInput.scss │ │ ├── _VTextField.scss │ │ ├── _VNavigationDrawer.scss │ │ ├── _VShadow.scss │ │ └── _VButtons.scss │ ├── layout │ │ ├── _container.scss │ │ └── _sidebar.scss │ ├── style.scss │ ├── pages │ │ ├── _auth.scss │ │ └── _dashboards.scss │ ├── _utils.scss │ ├── _mixins.scss │ └── _variables.scss ├── views │ ├── pages │ │ ├── UserProfile.vue │ │ ├── SamplePage.vue │ │ └── NotFound.vue │ ├── profile │ │ └── ProfileView.vue │ ├── apps │ │ ├── event-utils.ts │ │ ├── EmailView.vue │ │ └── CalendarView.vue │ ├── auth │ │ ├── RegisterView.vue │ │ └── LoginView.vue │ ├── dashboard │ │ └── IndexView.vue │ ├── setting │ │ ├── AccountSetting.vue │ │ └── SettingView.vue │ └── forms │ │ └── FormView.vue ├── components │ ├── ThemeCustomizer.vue │ ├── shared │ │ ├── UiTextfieldPrimary.vue │ │ ├── UiChildCard.vue │ │ ├── UiTableCard.vue │ │ ├── UiParentCard.vue │ │ ├── WidgetCard.vue │ │ ├── CardHeaderFooter.vue │ │ └── WidgetCardv2.vue │ ├── AppToolbar.vue │ ├── Snackbar.vue │ ├── charts │ │ ├── apex-chart │ │ │ ├── ApexChartExpenseRatio.vue │ │ │ ├── ApexChartStatistics.vue │ │ │ ├── ApexChartHorizontalBar.vue │ │ │ ├── ApexChartBalance.vue │ │ │ ├── ApexChartDataScience.vue │ │ │ ├── ApexChartMobileComparison.vue │ │ │ ├── ApexChartAreaChart.vue │ │ │ ├── ApexChartDailySalesStates.vue │ │ │ ├── ApexChartNewTechnologiesData.vue │ │ │ └── ApexChartStocksPrices.vue │ │ ├── MonthlyEarnings.vue │ │ ├── YearlyBreakup.vue │ │ └── SalesOverview.vue │ ├── picker │ │ └── AppDatePicker.vue │ ├── forms │ │ ├── RegisterForm.vue │ │ ├── BasicConnectionForm.vue │ │ ├── BasicSecurityForm.vue │ │ ├── BasicAccountForm.vue │ │ ├── UserForm.vue │ │ ├── LoginForm.vue │ │ └── EventForm.vue │ ├── chat │ │ ├── ChatAvatar.vue │ │ └── ChatMessage.vue │ ├── Logo.vue │ ├── dropdown │ │ ├── NotificationDropdown.vue │ │ └── ProfileDropdown.vue │ ├── form-layout │ │ ├── VerticalForm.vue │ │ ├── TabForm.vue │ │ ├── VerticalFormWithIcon.vue │ │ ├── HorizontalForm.vue │ │ └── HorizontalFormWithIcon.vue │ ├── dashboard │ │ ├── RecentTransaction.vue │ │ └── RecentTask.vue │ ├── card │ │ └── VerticalProfileCard.vue │ ├── AppCardCode.vue │ ├── Notification.vue │ └── AppSidebar.vue ├── plugins │ ├── msw │ │ ├── handlers │ │ │ ├── taskHandler.ts │ │ │ ├── authHandler.ts │ │ │ ├── userHandler.ts │ │ │ ├── db.ts │ │ │ └── eventHandler.ts │ │ └── index.ts │ ├── i18n │ │ ├── index.ts │ │ └── locales │ │ │ ├── zhHans.json │ │ │ └── en.json │ ├── vuetify │ │ ├── index.ts │ │ ├── theme.ts │ │ ├── constants.ts │ │ └── defaults.ts │ └── axios.ts ├── assets │ ├── images │ │ ├── products │ │ │ ├── s4.jpg │ │ │ ├── s5.jpg │ │ │ ├── s7.jpg │ │ │ └── s11.jpg │ │ ├── users │ │ │ └── avatar-1.jpg │ │ ├── avatars │ │ │ ├── avatar-1.png │ │ │ ├── avatar-2.png │ │ │ ├── avatar-3.png │ │ │ ├── avatar-4.png │ │ │ ├── avatar-5.png │ │ │ ├── avatar-6.png │ │ │ ├── avatar-7.png │ │ │ ├── avatar-8.png │ │ │ ├── avatar-9.png │ │ │ └── avatar-10.png │ │ └── logo.svg │ └── svg │ │ ├── auth-v1-top-shape.svg │ │ ├── auth-v1-bottom-shape.svg │ │ ├── paypal.svg │ │ └── keyboard.svg ├── composables │ ├── index.ts │ ├── useDrawer.ts │ ├── useFormValidation.ts │ └── useApi.ts ├── App.vue ├── layouts │ ├── BlankLayout.vue │ └── DefaultLayout.vue ├── store │ ├── snackbarStore.ts │ ├── index.ts │ ├── chatStore.ts │ ├── eventStore.ts │ └── userStore.ts ├── router │ ├── public.ts │ ├── index.ts │ └── private.ts ├── main.ts ├── types │ ├── dashboard │ │ └── index.ts │ └── themeTypes │ │ └── ThemeType.ts ├── theme │ └── LightTheme.ts ├── utils │ ├── index.ts │ ├── formatters.ts │ └── validators.ts └── data │ └── dashboard │ └── dashboardData.ts ├── env.d.ts ├── .env ├── public ├── vma_preview.webp ├── assets │ └── images │ │ ├── emoji.webp │ │ ├── users │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ ├── user.jpg │ │ ├── user2.jpg │ │ └── avatar-1.jpg │ │ ├── bg │ │ ├── bg_mac.png │ │ └── cooking.png │ │ ├── products │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── s4.jpg │ │ └── iphon12.png │ │ ├── avatars │ │ ├── avatar-1.png │ │ ├── avatar-2.png │ │ ├── avatar-3.png │ │ ├── avatar-4.png │ │ ├── avatar-5.png │ │ ├── avatar-6.png │ │ ├── avatar-7.png │ │ ├── avatar-8.png │ │ ├── avatar-9.png │ │ └── avatar-10.png │ │ └── icon │ │ ├── success.svg │ │ ├── danger.svg │ │ └── warning.svg ├── logo.svg ├── favicon.svg └── loader.css ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .eslintrc.cjs ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── vite.config.ts ├── .github └── workflows │ └── ci.yml ├── index.html ├── package.json ├── README.zh-CN.md └── CHANGELOG.md /src/api/task.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/scss/components/_VCard.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/components/_VInput.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/pages/UserProfile.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ThemeCustomizer.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/components/_VTextField.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/components/_VNavigationDrawer.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/plugins/msw/handlers/taskHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse, delay } from 'msw'; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_NAME="Vue Material Admin" 2 | VITE_API_URL="/" 3 | VITE_APP_WEBSTORAGE_NAMESPACE="vma" 4 | -------------------------------------------------------------------------------- /public/vma_preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/vma_preview.webp -------------------------------------------------------------------------------- /public/assets/images/emoji.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/emoji.webp -------------------------------------------------------------------------------- /public/assets/images/users/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/1.jpg -------------------------------------------------------------------------------- /public/assets/images/users/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/2.jpg -------------------------------------------------------------------------------- /public/assets/images/users/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/3.jpg -------------------------------------------------------------------------------- /public/assets/images/users/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/4.jpg -------------------------------------------------------------------------------- /public/assets/images/users/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/5.jpg -------------------------------------------------------------------------------- /public/assets/images/users/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/6.jpg -------------------------------------------------------------------------------- /public/assets/images/users/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/7.jpg -------------------------------------------------------------------------------- /public/assets/images/users/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/8.jpg -------------------------------------------------------------------------------- /src/assets/images/products/s4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/products/s4.jpg -------------------------------------------------------------------------------- /src/assets/images/products/s5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/products/s5.jpg -------------------------------------------------------------------------------- /src/assets/images/products/s7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/products/s7.jpg -------------------------------------------------------------------------------- /public/assets/images/bg/bg_mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/bg/bg_mac.png -------------------------------------------------------------------------------- /public/assets/images/bg/cooking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/bg/cooking.png -------------------------------------------------------------------------------- /public/assets/images/products/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/products/1.jpg -------------------------------------------------------------------------------- /public/assets/images/products/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/products/2.jpg -------------------------------------------------------------------------------- /public/assets/images/products/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/products/3.jpg -------------------------------------------------------------------------------- /public/assets/images/products/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/products/4.jpg -------------------------------------------------------------------------------- /public/assets/images/products/s4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/products/s4.jpg -------------------------------------------------------------------------------- /public/assets/images/users/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/user.jpg -------------------------------------------------------------------------------- /public/assets/images/users/user2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/user2.jpg -------------------------------------------------------------------------------- /src/assets/images/products/s11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/products/s11.jpg -------------------------------------------------------------------------------- /src/assets/images/users/avatar-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/users/avatar-1.jpg -------------------------------------------------------------------------------- /src/scss/components/_VShadow.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | .elevation-10 { 3 | box-shadow: $box-shadow !important; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-1.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-2.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-3.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-4.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-5.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-6.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-7.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-8.png -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-9.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-1.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-2.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-3.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-4.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-5.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-6.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-7.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-8.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-9.png -------------------------------------------------------------------------------- /public/assets/images/products/iphon12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/products/iphon12.png -------------------------------------------------------------------------------- /public/assets/images/users/avatar-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/users/avatar-1.jpg -------------------------------------------------------------------------------- /src/assets/images/avatars/avatar-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/src/assets/images/avatars/avatar-10.png -------------------------------------------------------------------------------- /public/assets/images/avatars/avatar-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tookit/vue-material-admin/HEAD/public/assets/images/avatars/avatar-10.png -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | // Export all composables for easy importing 2 | export * from './useApi'; 3 | export * from './useDrawer'; 4 | export * from './useFormValidation'; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /src/views/profile/ProfileView.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /public/assets/images/icon/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/layout/_container.scss: -------------------------------------------------------------------------------- 1 | .page-wrapper { 2 | min-height: calc(100vh - 162px); 3 | padding: 24px; 4 | padding-bottom: 30px; 5 | 6 | @media screen and (max-width:600px){ 7 | padding: 10px; 8 | } 9 | } -------------------------------------------------------------------------------- /public/assets/images/icon/danger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/components/shared/UiTextfieldPrimary.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /public/assets/images/icon/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "stylelint.vscode-stylelint", 6 | "Vue.volar", 7 | "Vue.vscode-typescript-vue-plugin" 8 | ], 9 | "unwantedRecommendations": [ 10 | "octref.vetur" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/svg/auth-v1-top-shape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/svg/auth-v1-bottom-shape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import 'vuetify/styles/main.sass'; 2 | @import './utils'; 3 | @import './layout/container'; 4 | @import './layout/sidebar'; 5 | @import './components/VCard'; 6 | @import './components/VInput'; 7 | @import './components/VNavigationDrawer'; 8 | @import './components/VShadow'; 9 | @import './components/VTextField'; 10 | @import './pages/dashboards'; 11 | @import './pages/auth'; 12 | -------------------------------------------------------------------------------- /src/views/pages/SamplePage.vue: -------------------------------------------------------------------------------- 1 | 4 | 13 | -------------------------------------------------------------------------------- /src/api/chat.ts: -------------------------------------------------------------------------------- 1 | import axiosIns from '@/plugins/axios'; 2 | import { IChatInit } from './type'; 3 | 4 | /** 5 | * Init Chat 6 | */ 7 | export async function initChat() { 8 | const options = { 9 | method: 'get', 10 | url: '/api/chat', 11 | delay: 2000, 12 | headers: { 13 | 'Content-Type': 'application/json' 14 | } 15 | }; 16 | return await axiosIns.request(options); 17 | } 18 | -------------------------------------------------------------------------------- /src/store/snackbarStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useSnackbarStore = defineStore('snackbar', { 4 | state: () => ({ 5 | color: 'error', 6 | message: '', 7 | showSnackbar: false 8 | }), 9 | actions: { 10 | showMessage(message, color = 'error') { 11 | this.color = color; 12 | this.message = message; 13 | this.showSnackbar = true; 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/shared/UiChildCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/scss/pages/_auth.scss: -------------------------------------------------------------------------------- 1 | .auth{ 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | &::before{ 8 | content: ""; 9 | position: absolute; 10 | height: 100%; 11 | width: 100%; 12 | opacity: 0.3; 13 | left: 0; 14 | top: 0; 15 | bottom: 0; 16 | background: var(--v-theme-background); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /src/components/AppToolbar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/plugins/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | // import { en, zhHans } from 'vuetify/locale'; 3 | 4 | const messages = Object.fromEntries( 5 | Object.entries( 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | import.meta.glob<{ default: any }>('./locales/*.json', { eager: true }) 8 | ).map(([key, value]) => [key.slice(10, -5), value.default]) 9 | ); 10 | 11 | export default createI18n({ 12 | legacy: false, 13 | locale: 'en', 14 | fallbackLocale: 'en', 15 | messages 16 | }); 17 | -------------------------------------------------------------------------------- /src/router/public.ts: -------------------------------------------------------------------------------- 1 | const PublicRoutes = { 2 | path: '/auth', 3 | component: () => import('@/layouts/BlankLayout.vue'), 4 | meta: { 5 | requiresAuth: false 6 | }, 7 | children: [ 8 | { 9 | name: 'Login', 10 | path: '/auth/login', 11 | component: () => import('@/views/auth/LoginView.vue') 12 | }, 13 | { 14 | name: 'Register', 15 | path: '/auth/register', 16 | component: () => import('@/views/auth/RegisterView.vue') 17 | } 18 | ] 19 | }; 20 | 21 | export default PublicRoutes; 22 | -------------------------------------------------------------------------------- /src/scss/_utils.scss: -------------------------------------------------------------------------------- 1 | $theme-colors-name: ( 2 | "primary", 3 | "secondary", 4 | "error", 5 | "info", 6 | "success", 7 | "warning" 8 | ) !default; 9 | 10 | // [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })], 11 | @each $color-name in $theme-colors-name { 12 | .bg-light-#{$color-name} { 13 | background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important; 14 | } 15 | } 16 | 17 | .bg-transparent { 18 | background-color: transparent; 19 | } -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; 2 | import { createPinia, type Pinia } from 'pinia'; 3 | 4 | // Pinia Stores 5 | import { useUserStore } from '@/store/userStore'; 6 | import { useSnackbarStore } from '@/store/snackbarStore'; 7 | import { useCalendarStore } from '@/store/eventStore'; 8 | 9 | /** Pinia Store */ 10 | const pinia: Pinia = createPinia(); 11 | pinia.use(piniaPluginPersistedstate); 12 | 13 | export default pinia; 14 | 15 | export { useUserStore, useCalendarStore, useSnackbarStore }; 16 | -------------------------------------------------------------------------------- /src/views/apps/event-utils.ts: -------------------------------------------------------------------------------- 1 | import { EventInput } from '@fullcalendar/core'; 2 | 3 | let eventGuid = 0; 4 | const todayStr = new Date().toISOString().replace(/T.*$/, ''); // YYYY-MM-DD of today 5 | 6 | export const INITIAL_EVENTS: EventInput[] = [ 7 | { 8 | id: createEventId(), 9 | title: 'All-day event', 10 | start: todayStr 11 | }, 12 | { 13 | id: createEventId(), 14 | title: 'Timed event', 15 | start: todayStr + 'T12:00:00' 16 | } 17 | ]; 18 | 19 | export function createEventId() { 20 | return String(eventGuid++); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Snackbar.vue: -------------------------------------------------------------------------------- 1 | 7 | 15 | -------------------------------------------------------------------------------- /public/loader.css: -------------------------------------------------------------------------------- 1 | .loader-wrapper { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | transform: translate(-50%, -50%); 6 | } 7 | .loader { 8 | width: 48px; 9 | height: 48px; 10 | border: 5px solid; 11 | border-color: var(--initial-loader-color, #304FFD) transparent; 12 | border-radius: 50%; 13 | display: inline-block; 14 | box-sizing: border-box; 15 | animation: rotation 1s linear infinite; 16 | } 17 | 18 | @keyframes rotation { 19 | 0% { 20 | transform: rotate(0deg); 21 | } 22 | 100% { 23 | transform: rotate(360deg); 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/shared/UiTableCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartExpenseRatio.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartStatistics.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartHorizontalBar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript/recommended', 10 | '@vue/eslint-config-prettier' 11 | ], 12 | env: { 13 | 'vue/setup-compiler-macros': true 14 | }, 15 | rules: { 16 | 'comma-dangle': 'off', 17 | 'vue/valid-v-slot': 'off', 18 | 'vue/multi-word-component-names': 'off', 19 | '@typescript-eslint/comma-dangle': 'off', 20 | 'prettier/prettier': ['error', { endOfLine: 'lf' }] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/plugins/vuetify/index.ts: -------------------------------------------------------------------------------- 1 | import { createVuetify } from 'vuetify'; 2 | import { createVueI18nAdapter } from 'vuetify/locale/adapters/vue-i18n'; 3 | import i18n from '@/plugins/i18n'; 4 | import { useI18n } from 'vue-i18n'; 5 | import { VuetifyDateAdapter } from 'vuetify/date/adapters/vuetify'; 6 | import '@mdi/font/css/materialdesignicons.css'; 7 | import 'vuetify/styles'; 8 | import theme from './theme'; 9 | import defaults from './defaults'; 10 | 11 | export default createVuetify({ 12 | defaults, 13 | theme, 14 | locale: { 15 | adapter: createVueI18nAdapter({ i18n, useI18n }) 16 | }, 17 | date: { 18 | adapter: VuetifyDateAdapter 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/shared/UiParentCard.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | // ===============================|| Ui Parent Card||=============================== // 9 | 22 | -------------------------------------------------------------------------------- /src/components/shared/WidgetCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartBalance.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /src/plugins/msw/index.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw/browser'; 2 | import { handlerAuth } from '@/plugins/msw/handlers/authHandler'; 3 | import { handlerUser } from '@/plugins/msw/handlers/userHandler'; 4 | import { handlerEvent } from '@/plugins/msw/handlers/eventHandler'; 5 | import { handlerChat } from './handlers/chatHandler'; 6 | 7 | const worker = setupWorker(...handlerAuth, ...handlerUser, ...handlerEvent, ...handlerChat); 8 | 9 | export default function () { 10 | const workerUrl = `${import.meta.env.BASE_URL ?? '/'}mockServiceWorker.js`; 11 | 12 | worker.start({ 13 | serviceWorker: { 14 | url: workerUrl 15 | }, 16 | onUnhandledRequest: 'bypass' 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/api/event.ts: -------------------------------------------------------------------------------- 1 | import axiosIns from '@/plugins/axios'; 2 | import { ICalendarEvent, IEvent } from './type'; 3 | 4 | export async function fetchEvents(params: object) { 5 | const options = { 6 | method: 'GET', 7 | url: '/api/event', 8 | params: params, 9 | headers: { 10 | 'Content-Type': 'application/json' 11 | } 12 | }; 13 | return axiosIns.request>(options); 14 | } 15 | 16 | export async function updateEvent(event: IEvent) { 17 | const options = { 18 | method: 'PUT', 19 | url: `/api/event/${event.id}`, 20 | data: event, 21 | headers: { 22 | 'Content-Type': 'application/json' 23 | } 24 | }; 25 | return axiosIns.request(options); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/shared/CardHeaderFooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import { router } from './router'; 4 | import vuetify from './plugins/vuetify'; 5 | import i18n from '@/plugins/i18n'; 6 | import msw from '@/plugins/msw'; 7 | import store from './store'; 8 | import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'; 9 | import VueApexCharts from 'vue3-apexcharts'; 10 | 11 | // Styles 12 | import '@/scss/style.scss'; 13 | import 'vue3-perfect-scrollbar/style.css'; 14 | 15 | const app = createApp(App); 16 | 17 | // Register plugins 18 | app.use(store); 19 | app.use(router); 20 | app.use(i18n); 21 | app.use(vuetify); 22 | app.use(PerfectScrollbarPlugin); 23 | app.use(VueApexCharts); 24 | app.use(msw); 25 | 26 | app.mount('#app'); 27 | -------------------------------------------------------------------------------- /src/types/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | /*Recent Transaction*/ 2 | type recentTrans = { 3 | title: string; 4 | subtitle: string; 5 | textcolor: string; 6 | boldtext: boolean; 7 | line: boolean; 8 | link: string; 9 | url: string; 10 | }; 11 | 12 | /*product performance*/ 13 | type productPerformanceType = { 14 | id: number; 15 | name: string; 16 | post: string; 17 | pname: string; 18 | status: string; 19 | statuscolor: string; 20 | budget: string; 21 | }; 22 | 23 | /*Products card types*/ 24 | type productsCards = { 25 | title: string; 26 | link: string; 27 | photo: string; 28 | salesPrice: number; 29 | price: number; 30 | rating: number; 31 | }; 32 | 33 | export type { recentTrans, productPerformanceType, productsCards }; 34 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartDataScience.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartMobileComparison.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /src/components/picker/AppDatePicker.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | -------------------------------------------------------------------------------- /src/components/shared/WidgetCardv2.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /src/types/themeTypes/ThemeType.ts: -------------------------------------------------------------------------------- 1 | export type ThemeTypes = { 2 | name: string; 3 | dark: boolean; 4 | variables?: object; 5 | colors: { 6 | primary?: string; 7 | secondary?: string; 8 | info?: string; 9 | success?: string; 10 | accent?: string; 11 | warning?: string; 12 | error?: string; 13 | lightprimary?: string; 14 | lightsecondary?: string; 15 | lightsuccess?: string; 16 | lighterror?: string; 17 | lightinfo?: string; 18 | lightwarning?: string; 19 | textPrimary?: string; 20 | textSecondary?: string; 21 | borderColor?: string; 22 | hoverColor?: string; 23 | inputBorder?: string; 24 | containerBg?: string; 25 | surface?: string; 26 | 'on-surface-variant'?: string; 27 | grey100?: string; 28 | grey200?: string; 29 | muted?: string; 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/composables/useDrawer.ts: -------------------------------------------------------------------------------- 1 | import { reactive, computed } from 'vue'; 2 | 3 | export interface DrawerState { 4 | rail: boolean; 5 | railWidth: number; 6 | icon: string; 7 | } 8 | 9 | export function useDrawer(initialWidth = 256, collapsedWidth = 64) { 10 | const state = reactive({ 11 | rail: false, 12 | railWidth: initialWidth, 13 | icon: 'mdi-arrow-expand-left' 14 | }); 15 | 16 | const toggle = () => { 17 | state.rail = !state.rail; 18 | state.railWidth = state.railWidth === collapsedWidth ? initialWidth : collapsedWidth; 19 | state.icon = state.railWidth === initialWidth ? 'mdi-arrow-expand-left' : 'mdi-arrow-expand-right'; 20 | }; 21 | 22 | const isExpanded = computed(() => state.railWidth === initialWidth); 23 | 24 | return { 25 | state, 26 | toggle, 27 | isExpanded 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | node_modules/ 8 | dist/ 9 | 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | 51 | .vercel 52 | -------------------------------------------------------------------------------- /src/scss/pages/_dashboards.scss: -------------------------------------------------------------------------------- 1 | .month-table { 2 | &.custom-px-0 { 3 | thead { 4 | tr { 5 | th:first-child { 6 | padding-left: 0 !important; 7 | } 8 | th:last-child { 9 | padding-right: 0 !important; 10 | } 11 | } 12 | } 13 | tr.month-item { 14 | td:first-child { 15 | padding-left: 0 !important; 16 | } 17 | td:last-child { 18 | padding-right: 0 !important; 19 | } 20 | } 21 | } 22 | tr.month-item { 23 | td { 24 | padding-top: 16px !important; 25 | padding-bottom: 16px !important; 26 | } 27 | &:hover { 28 | background: transparent !important; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/forms/RegisterForm.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/views/auth/RegisterView.vue: -------------------------------------------------------------------------------- 1 | 5 | 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,py}] 14 | charset = utf-8 15 | 16 | # 4 space indentation 17 | [*.py] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | # 2 space indentation 22 | [*.{vue,scss,ts}] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | # Tab indentation (no size specified) 27 | [Makefile] 28 | indent_style = tab 29 | 30 | # Indentation override for all JS under lib directory 31 | [lib/**.js] 32 | indent_style = space 33 | indent_size = 2 34 | 35 | # Matches the exact files either package.json or .travis.yml 36 | [{package.json,.travis.yml}] 37 | indent_style = space 38 | indent_size = 2 39 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartAreaChart.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { IAccessToken, IErrorResp, IUser } from './type'; 2 | import axiosIns from '@/plugins/axios'; 3 | 4 | export async function login(params: object) { 5 | const options = { 6 | method: 'POST', 7 | url: `/api/auth/login`, 8 | data: params, 9 | headers: { 10 | 'Content-Type': 'application/json' 11 | } 12 | }; 13 | return axiosIns.request(options); 14 | } 15 | 16 | export async function fetchMe() { 17 | const options = { 18 | method: 'GET', 19 | url: `/api/me`, 20 | headers: { 21 | 'Content-Type': 'application/json' 22 | } 23 | }; 24 | return axiosIns.request(options); 25 | } 26 | 27 | export async function fetchUser(params) { 28 | const options = { 29 | method: 'GET', 30 | url: `/api/user`, 31 | params: params, 32 | headers: { 33 | 'Content-Type': 'application/json' 34 | } 35 | }; 36 | return axiosIns.request>(options); 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/msw/handlers/authHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, delay, HttpResponse } from 'msw'; 2 | const isAuthenticated = (username, password) => { 3 | return username === 'admin' && password === '123456'; 4 | }; 5 | 6 | export const handlerAuth = [ 7 | http.post('/api/auth/login', async ({ request }) => { 8 | const { username, password } = (await request.json()) as { username: string; password: string }; 9 | console.log(username, password); 10 | const statusCode = isAuthenticated(username, password) ? 200 : 400; 11 | const data = isAuthenticated(username, password) 12 | ? { 13 | access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTB9.txWLuN4QT5PqTtgHmlOiNerIu5Do51PpYOiZutkyXYg', 14 | expires_in: 86400 15 | } 16 | : { 17 | errorCode: '400', 18 | errorMessage: 'Auth Failed.' 19 | }; 20 | await delay(1000); 21 | return HttpResponse.json(data, { status: statusCode }); 22 | }) 23 | ]; 24 | -------------------------------------------------------------------------------- /src/components/chat/ChatAvatar.vue: -------------------------------------------------------------------------------- 1 | 20 | 35 | -------------------------------------------------------------------------------- /src/theme/LightTheme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeTypes } from '@/types/themeTypes/ThemeType'; 2 | 3 | const PurpleTheme: ThemeTypes = { 4 | name: 'PurpleTheme', 5 | dark: false, 6 | variables: { 7 | 'border-color': '75, 70, 92', 8 | 'carousel-control-size': 10 9 | }, 10 | colors: { 11 | primary: '#213594', 12 | secondary: '#7DA437', 13 | info: '#539BFF', 14 | success: '#13DEB9', 15 | accent: '#FFAB91', 16 | warning: '#FFAE1F', 17 | error: '#FA896B', 18 | muted: '#5a6a85', 19 | lightprimary: '#ECF2FF', 20 | lightsecondary: '#E8F7FF', 21 | lightsuccess: '#E6FFFA', 22 | lighterror: '#FDEDE8', 23 | lightwarning: '#FEF5E5', 24 | textPrimary: '#2A3547', 25 | textSecondary: '#2A3547', 26 | borderColor: '#e5eaef', 27 | inputBorder: '#000', 28 | containerBg: '#ffffff', 29 | hoverColor: '#f6f9fc', 30 | surface: '#fff', 31 | 'on-surface-variant': '#fff', 32 | grey100: '#F2F6FA', 33 | grey200: '#EAEFF4' 34 | } 35 | }; 36 | export { PurpleTheme }; 37 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | -------------------------------------------------------------------------------- /src/store/chatStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { initChat } from '@/api/chat'; 3 | import { IChat, IUser } from '@/api/type'; 4 | 5 | export const useChatStore = defineStore('chat', { 6 | // ℹ️ arrow function recommended for full type inference 7 | state: () => ({ 8 | chats: [] as IChat[], 9 | chatContacts: [] as IUser[], 10 | profile: null 11 | }), 12 | actions: { 13 | setChats(chats) { 14 | this.chats = chats; 15 | }, 16 | setChatContacts(contacts) { 17 | this.chatContacts = contacts; 18 | }, 19 | setProfile(profile) { 20 | this.profile = profile; 21 | }, 22 | async initChat() { 23 | try { 24 | const response = await initChat(); 25 | const { data } = response; 26 | const { chats, contacts } = data; 27 | this.setChats(chats); 28 | this.setChatContacts(contacts); 29 | this.setProfile(this.profile); 30 | return Promise.resolve(response); 31 | } catch (e) { 32 | return Promise.reject(e); 33 | } 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "jsx": "preserve", 11 | "jsxFactory": "h", 12 | "jsxFragmentFactory": "Fragment", 13 | "sourceMap": true, 14 | "noImplicitAny": false, 15 | "resolveJsonModule": true, 16 | "esModuleInterop": true, 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ], 28 | "skipLibCheck": true, 29 | "types": [ 30 | "vite/client", 31 | "unplugin-vue-define-options/macros-global" 32 | ] 33 | }, 34 | "include": [ 35 | "vite.config.*", 36 | "env.d.ts", 37 | "shims.d.ts", 38 | "src/**/*", 39 | "src/**/*.vue", 40 | "auto-imports.d.ts", 41 | "themeConfig.ts" 42 | ], 43 | "exclude": [ 44 | "dist", 45 | "node_modules", 46 | "src/@iconify/*" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 42 | -------------------------------------------------------------------------------- /src/views/dashboard/IndexView.vue: -------------------------------------------------------------------------------- 1 | 9 | 38 | -------------------------------------------------------------------------------- /src/views/auth/LoginView.vue: -------------------------------------------------------------------------------- 1 | 6 | 24 | 25 | 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // eslint 保存格式化 3 | "eslint.enable": true, 4 | "eslint.run": "onType", 5 | "eslint.options": { 6 | "extensions": [".js", ".ts", ".jsx", ".tsx", ".vue"] 7 | }, 8 | // 编辑器保存格式化 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll": "explicit", 11 | "source.fixAll.eslint": "explicit" 12 | }, 13 | // .ts 文件格式化程序 14 | "[typescript]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | // .vue 文件格式化程序 18 | "[vue]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[json]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | // 操作时作为单词分隔符的字符 25 | "editor.wordSeparators": "`~!@#%^&*()=+[{]}\\|;:'\",.<>/?", 26 | // 一个制表符等于的空格数 27 | "editor.tabSize": 2, 28 | // 行尾字符 29 | "files.eol": "\n", 30 | // 保存到额时候用使用 prettier进行格式化 31 | "editor.formatOnSave": true 32 | 33 | // // 不要有分号 34 | // "prettier.semi": false, 35 | // // 使用单引号 36 | // "prettier.singleQuote": true, 37 | // // 默认使用prittier作为格式化工具 38 | // "editor.defaultFormatter": "esbenp.prettier-vscode", 39 | // // 一行的字符数,如果超过会进行换行,默认为80 40 | // "prettier.printWidth": 200, 41 | // // 尾随逗号问题,设置为none 不显示 逗号 42 | // "prettier.trailingComma": "none" 43 | } 44 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { fetchMe } from '@/api/user'; 2 | // import { useUserStore } from '@/store/userStore'; 3 | import { createRouter, createWebHashHistory } from 'vue-router'; 4 | import PrivateRoutes from './private'; 5 | import PublicRoutes from './public'; 6 | 7 | const whiteList = ['Login', 'Register', 'Forget', 'Reset']; 8 | 9 | export const router = createRouter({ 10 | history: createWebHashHistory(import.meta.env.BASE_URL), 11 | scrollBehavior(to) { 12 | if (to.hash) return { el: to.hash, behavior: 'smooth', top: 60 }; 13 | 14 | return { top: 0 }; 15 | }, 16 | routes: [ 17 | { 18 | path: '/:pathMatch(.*)*', 19 | component: () => import('@/views/pages/NotFound.vue') 20 | }, 21 | PrivateRoutes, 22 | PublicRoutes 23 | ] 24 | }); 25 | 26 | // Docs: https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards 27 | router.beforeEach(async (to) => { 28 | const routeName = String(to.name); 29 | if (whiteList.includes(routeName)) { 30 | return true; 31 | } else { 32 | // const userStore = useUserStore(); 33 | try { 34 | const resp = await fetchMe(); 35 | console.log(resp); 36 | return true; 37 | } catch (error) { 38 | return { name: 'Login' }; 39 | } 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/dropdown/NotificationDropdown.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /src/components/form-layout/VerticalForm.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 41 | -------------------------------------------------------------------------------- /src/components/forms/BasicConnectionForm.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | -------------------------------------------------------------------------------- /src/components/forms/BasicSecurityForm.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | -------------------------------------------------------------------------------- /src/store/eventStore.ts: -------------------------------------------------------------------------------- 1 | import type { IEvent, INewEvent } from '@/api/type'; 2 | import axios from '@/plugins/axios'; 3 | import { defineStore } from 'pinia'; 4 | import { fetchEvents, updateEvent } from '@/api/event'; 5 | export const useCalendarStore = defineStore('calendar', { 6 | // arrow function recommended for full type inference 7 | state: () => ({ 8 | availableCalendars: [ 9 | { 10 | color: 'error', 11 | label: 'Personal' 12 | }, 13 | { 14 | color: 'primary', 15 | label: 'Business' 16 | }, 17 | { 18 | color: 'warning', 19 | label: 'Family' 20 | }, 21 | { 22 | color: 'success', 23 | label: 'Holiday' 24 | }, 25 | { 26 | color: 'info', 27 | label: 'Meeting' 28 | } 29 | ], 30 | selectedCalendars: ['Personal', 'Business', 'Family', 'Holiday', 'Meeting'] 31 | }), 32 | actions: { 33 | async fetchEvents() { 34 | return fetchEvents({ calendars: this.selectedCalendars.join(',') }); 35 | }, 36 | async addEvent(event: INewEvent) { 37 | console.log(event); 38 | return fetchEvents({ calendars: this.selectedCalendars.join(',') }); 39 | }, 40 | async updateEvent(event: IEvent) { 41 | return updateEvent(event); 42 | }, 43 | async removeEvent(eventId: string) { 44 | return axios.delete(`/apps/calendar/events/${eventId}`); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/scss/components/_VButtons.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | // 3 | // Light Buttons 4 | // 5 | 6 | .v-btn { 7 | &.bg-lightprimary { 8 | &:hover, 9 | &:active, 10 | &:focus { 11 | background-color: rgb(var(--v-theme-primary)) !important; 12 | color: $white !important; 13 | } 14 | } 15 | &.bg-lightsecondary { 16 | &:hover, 17 | &:active, 18 | &:focus { 19 | background-color: rgb(var(--v-theme-secondary)) !important; 20 | color: $white !important; 21 | } 22 | } 23 | &.text-facebook { 24 | &:hover, 25 | &:active, 26 | &:focus { 27 | background-color: rgb(var(--v-theme-facebook)) !important; 28 | color: $white !important; 29 | } 30 | } 31 | &.text-twitter { 32 | &:hover, 33 | &:active, 34 | &:focus { 35 | background-color: rgb(var(--v-theme-twitter)) !important; 36 | color: $white !important; 37 | } 38 | } 39 | &.text-linkedin { 40 | &:hover, 41 | &:active, 42 | &:focus { 43 | background-color: rgb(var(--v-theme-linkedin)) !important; 44 | color: $white !important; 45 | } 46 | } 47 | } 48 | 49 | .v-btn { 50 | text-transform: capitalize; 51 | letter-spacing: $btn-letter-spacing; 52 | } 53 | .v-btn--icon.v-btn--density-default { 54 | width: calc(var(--v-btn-height) + 6px); 55 | height: calc(var(--v-btn-height) + 6px); 56 | } 57 | 58 | .v-btn-group .v-btn { 59 | height: inherit !important; 60 | } 61 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import vuetify from 'vite-plugin-vuetify'; 5 | import VueDevTools from 'vite-plugin-vue-devtools'; 6 | import DefineOptions from 'unplugin-vue-define-options/vite'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | vuetify({ 13 | autoImport: true 14 | }), 15 | DefineOptions(), 16 | VueDevTools() 17 | ], 18 | resolve: { 19 | alias: { 20 | '@': fileURLToPath(new URL('./src', import.meta.url)) 21 | }, 22 | extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'] 23 | }, 24 | css: { 25 | preprocessorOptions: { 26 | scss: { 27 | api: 'modern-compiler' 28 | } 29 | } 30 | }, 31 | server: { 32 | host: true, 33 | port: 9527, 34 | strictPort: false, 35 | open: false 36 | }, 37 | build: { 38 | target: 'esnext', 39 | minify: 'esbuild', 40 | cssCodeSplit: true, 41 | rollupOptions: { 42 | output: { 43 | manualChunks: { 44 | 'vue-vendor': ['vue', 'vue-router', 'pinia'], 45 | 'vuetify-vendor': ['vuetify'], 46 | 'chart-vendor': ['apexcharts', 'vue3-apexcharts'] 47 | } 48 | } 49 | } 50 | }, 51 | optimizeDeps: { 52 | include: ['vue', 'vue-router', 'pinia', 'vuetify'], 53 | exclude: [] 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/router/private.ts: -------------------------------------------------------------------------------- 1 | const PrivateRoutes = { 2 | path: '/', 3 | name: 'Home', 4 | meta: { 5 | requiresAuth: true 6 | }, 7 | redirect: '/main', 8 | component: () => import('@/layouts/DefaultLayout.vue'), 9 | children: [ 10 | { 11 | name: 'Dashboard', 12 | path: '/', 13 | component: () => import('@/views/dashboard/IndexView.vue') 14 | }, 15 | { 16 | name: 'Form', 17 | path: '/form', 18 | component: () => import('@/views/forms/FormView.vue') 19 | }, 20 | { 21 | name: 'Table', 22 | path: '/user-table', 23 | component: () => import('@/views/tables/TableView.vue') 24 | }, 25 | { 26 | name: 'Chart', 27 | path: '/Chart', 28 | component: () => import('@/views/charts/ChartView.vue') 29 | }, 30 | { 31 | name: 'Calendar', 32 | path: '/calendar', 33 | component: () => import('@/views/apps/CalendarView.vue') 34 | }, 35 | { 36 | name: 'Chat', 37 | path: '/chat', 38 | component: () => import('@/views/apps/ChatView.vue') 39 | }, 40 | { 41 | name: 'Profile', 42 | path: '/profile', 43 | component: () => import('@/views/profile/ProfileView.vue') 44 | }, 45 | { 46 | name: 'Card', 47 | path: '/widget/card', 48 | component: () => import('@/views/widget/BasicCard.vue') 49 | }, 50 | { 51 | name: 'Setting', 52 | path: '/setting', 53 | component: () => import('@/views/setting/SettingView.vue') 54 | } 55 | ] 56 | }; 57 | 58 | export default PrivateRoutes; 59 | -------------------------------------------------------------------------------- /src/components/forms/BasicAccountForm.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 49 | -------------------------------------------------------------------------------- /src/components/dashboard/RecentTransaction.vue: -------------------------------------------------------------------------------- 1 | 4 | 33 | 41 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // 👉 IsEmpty 2 | export const isEmpty = (value: unknown): boolean => { 3 | if (value === null || value === undefined || value === '') return true; 4 | 5 | return !!(Array.isArray(value) && value.length === 0); 6 | }; 7 | 8 | // 👉 IsNullOrUndefined 9 | export const isNullOrUndefined = (value: unknown): value is undefined | null => { 10 | return value === null || value === undefined; 11 | }; 12 | 13 | // 👉 IsEmptyArray 14 | export const isEmptyArray = (arr: unknown): boolean => { 15 | return Array.isArray(arr) && arr.length === 0; 16 | }; 17 | 18 | // 👉 IsObject 19 | export const isObject = (obj: unknown): obj is Record => 20 | obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj); 21 | 22 | export const isToday = (date: Date) => { 23 | const today = new Date(); 24 | 25 | return ( 26 | /* eslint-disable operator-linebreak */ 27 | date.getDate() === today.getDate() && 28 | date.getMonth() === today.getMonth() && 29 | date.getFullYear() === today.getFullYear() 30 | /* eslint-enable */ 31 | ); 32 | }; 33 | 34 | /** 35 | * Convert Hex color to rgb 36 | * @param hex 37 | */ 38 | 39 | export const hexToRgb = (hex: string) => { 40 | // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 41 | const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 42 | 43 | hex = hex.replace(shorthandRegex, (m: string, r: string, g: string, b: string) => { 44 | return r + r + g + g + b + b; 45 | }); 46 | 47 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 48 | 49 | return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : null; 50 | }; 51 | -------------------------------------------------------------------------------- /src/views/setting/AccountSetting.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 46 | -------------------------------------------------------------------------------- /src/components/chat/ChatMessage.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | 60 | -------------------------------------------------------------------------------- /src/components/form-layout/TabForm.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | -------------------------------------------------------------------------------- /src/plugins/msw/handlers/userHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse, delay } from 'msw'; 2 | import { users } from './db'; 3 | 4 | const TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTB9.txWLuN4QT5PqTtgHmlOiNerIu5Do51PpYOiZutkyXYg'; 5 | 6 | export const findUserById = (id) => { 7 | return users.find((item) => item.id === id); 8 | }; 9 | 10 | export const getToken = (request) => { 11 | const authHeader = request.headers.get('Authorization'); 12 | return authHeader?.replace('Bearer ', ''); 13 | }; 14 | 15 | export const handlerUser = [ 16 | // get current user info 17 | http.get('/api/me', async ({ request }) => { 18 | const token = getToken(request); 19 | const data = users; 20 | if (token === TOKEN) { 21 | return HttpResponse.json(data[0], { status: 200 }); 22 | } else { 23 | return HttpResponse.json(data[0], { status: 401 }); 24 | } 25 | }), 26 | 27 | http.get('/api/user', async ({ request }) => { 28 | let data = users; 29 | const { searchParams } = new URL(request.url); 30 | const role = searchParams.get('filter[role]') ?? null; 31 | const status = searchParams.get('filter[status]') ?? null; 32 | if (role) { 33 | data = users.filter((item) => { 34 | return item.role === role; 35 | }); 36 | } 37 | if (status) { 38 | data = users.filter((item) => { 39 | return item.status === status; 40 | }); 41 | } 42 | await delay(2000); 43 | return HttpResponse.json(data, { status: 200 }); 44 | }), 45 | 46 | http.get('/api/user/:id', async ({ params }) => { 47 | const data = findUserById(params.id); 48 | console.log(`Captured a "DELETE /posts/${params.id}" request`); 49 | return HttpResponse.json(data, { status: 200 }); 50 | }) 51 | ]; 52 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartDailySalesStates.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /src/components/dropdown/ProfileDropdown.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /src/views/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 15 | 70 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartNewTechnologiesData.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 69 | -------------------------------------------------------------------------------- /src/plugins/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { router } from '@/router'; 3 | import { useSnackbarStore } from '@/store'; 4 | 5 | const axiosIns = axios.create({ 6 | // You can add your headers here 7 | // ================================ 8 | // baseURL: 'https://some-domain.com/api/', 9 | // timeout: 1000, 10 | // headers: {'X-Custom-Header': 'foobar'} 11 | }); 12 | 13 | // ℹ️ Add request interceptor to send the authorization header on each subsequent request after login 14 | axiosIns.interceptors.request.use((config) => { 15 | // Retrieve token from sessionStorage 16 | const vma = sessionStorage.getItem('vma'); 17 | // If token is found 18 | if (vma) { 19 | // Get request headers and if headers is undefined assign blank object 20 | config.headers = config.headers || {}; 21 | const { token } = JSON.parse(vma); 22 | // Set authorization header 23 | // ℹ️ JSON.parse will convert token to string 24 | config.headers.Authorization = token ? `Bearer ${token}` : ''; 25 | } 26 | 27 | // Return modified config 28 | return config; 29 | }); 30 | 31 | // ℹ️ Add response interceptor to handle 401 response 32 | axiosIns.interceptors.response.use( 33 | (response) => { 34 | return response; 35 | }, 36 | (error) => { 37 | const statusCode = error.response.status; 38 | const snackbarStore = useSnackbarStore(); 39 | switch (statusCode) { 40 | case 401: 41 | // Remove "accessToken" from sessionStorage 42 | localStorage.removeItem('accessToken'); 43 | localStorage.removeItem('userAbilities'); 44 | // If 401 response returned from api 45 | router.push('/auth/login'); 46 | break; 47 | case 400: 48 | snackbarStore.showMessage('Auth Failed'); 49 | break; 50 | default: 51 | break; 52 | } 53 | return Promise.reject(error); 54 | } 55 | ); 56 | 57 | export default axiosIns; 58 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, dev] 6 | pull_request: 7 | branches: [main, dev] 8 | 9 | concurrency: 10 | group: ci-${{ github.ref }}-${{ github.workflow }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: yarn 29 | cache-dependency-path: yarn.lock 30 | 31 | - name: Install dependencies 32 | run: yarn install --frozen-lockfile 33 | 34 | - name: Check vue-tsc availability 35 | id: check_vuetsc 36 | run: | 37 | node -e "const p=require('./package.json'); process.exit(p.devDependencies && p.devDependencies['vue-tsc'] || p.dependencies && p.dependencies['vue-tsc'] ? 0 : 1)" 38 | continue-on-error: true 39 | 40 | - name: Type check 41 | if: steps.check_vuetsc.outcome == 'success' 42 | run: yarn vue-tsc --noEmit 43 | 44 | - name: Check lint script availability 45 | id: check_lint 46 | run: | 47 | node -e "const p=require('./package.json'); process.exit(p.scripts && p.scripts['lint'] ? 0 : 1)" 48 | continue-on-error: true 49 | 50 | - name: Lint 51 | if: steps.check_lint.outcome == 'success' 52 | run: yarn lint 53 | 54 | - name: Build 55 | env: 56 | CI: true 57 | run: yarn build 58 | 59 | - name: Upload build artifact (PR only) 60 | if: ${{ github.event_name == 'pull_request' }} 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: dist-${{ matrix.node-version }} 64 | path: dist 65 | -------------------------------------------------------------------------------- /src/composables/useFormValidation.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue'; 2 | 3 | export interface ValidationRule { 4 | (value: any): boolean | string; 5 | } 6 | 7 | export function useFormValidation() { 8 | const isValid = ref(false); 9 | 10 | // Common validation rules 11 | const rules = { 12 | required: (message = 'This field is required'): ValidationRule => { 13 | return (value: any) => !!value || message; 14 | }, 15 | 16 | email: (message = 'Invalid email address'): ValidationRule => { 17 | return (value: string) => { 18 | const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 19 | return pattern.test(value) || message; 20 | }; 21 | }, 22 | 23 | minLength: (min: number, message?: string): ValidationRule => { 24 | return (value: string) => { 25 | const msg = message || `Minimum ${min} characters required`; 26 | return (value && value.length >= min) || msg; 27 | }; 28 | }, 29 | 30 | maxLength: (max: number, message?: string): ValidationRule => { 31 | return (value: string) => { 32 | const msg = message || `Maximum ${max} characters allowed`; 33 | return !value || value.length <= max || msg; 34 | }; 35 | }, 36 | 37 | numeric: (message = 'Must be a number'): ValidationRule => { 38 | return (value: any) => { 39 | return !value || !isNaN(Number(value)) || message; 40 | }; 41 | }, 42 | 43 | url: (message = 'Invalid URL'): ValidationRule => { 44 | return (value: string) => { 45 | try { 46 | new URL(value); 47 | return true; 48 | } catch { 49 | return message; 50 | } 51 | }; 52 | }, 53 | 54 | match: (compareValue: any, message = 'Values do not match'): ValidationRule => { 55 | return (value: any) => value === compareValue || message; 56 | } 57 | }; 58 | 59 | return { 60 | isValid, 61 | rules 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import { isToday } from './index'; 2 | 3 | export const avatarText = (value: string) => { 4 | if (!value) return ''; 5 | const nameArray = value.split(' '); 6 | 7 | return nameArray.map((word) => word.charAt(0).toUpperCase()).join(''); 8 | }; 9 | 10 | export const kFormatter = (num: number) => { 11 | const regex = /\B(?=(\d{3})+(?!\d))/g; 12 | 13 | return Math.abs(num) > 9999 14 | ? `${Math.sign(num) * +(Math.abs(num) / 1000).toFixed(1)}k` 15 | : Math.abs(num).toFixed(0).replace(regex, ','); 16 | }; 17 | 18 | /** 19 | * Format and return date in Humanize format 20 | * Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format 21 | * Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat 22 | * @param {String} value date to format 23 | * @param {Intl.DateTimeFormatOptions} formatting Intl object to format with 24 | */ 25 | export const formatDate = ( 26 | value: string, 27 | formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' } 28 | ) => { 29 | if (!value) return value; 30 | 31 | return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value)); 32 | }; 33 | 34 | /** 35 | * Return short human friendly month representation of date 36 | * Can also convert date to only time if date is of today (Better UX) 37 | * @param {String} value date to format 38 | * @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current 39 | */ 40 | export const formatDateToMonthShort = (value: string, toTimeForCurrentDay = true) => { 41 | const date = new Date(value); 42 | let formatting: Record = { month: 'short', day: 'numeric' }; 43 | 44 | if (toTimeForCurrentDay && isToday(date)) formatting = { hour: 'numeric', minute: 'numeric' }; 45 | 46 | return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value)); 47 | }; 48 | -------------------------------------------------------------------------------- /src/plugins/vuetify/theme.ts: -------------------------------------------------------------------------------- 1 | import type { VuetifyOptions } from 'vuetify'; 2 | import { 3 | PRIMARY_COLOR, 4 | LIGHT_THEME_COLORS, 5 | DARK_THEME_COLORS, 6 | GREY_PALETTE_LIGHT, 7 | GREY_PALETTE_DARK, 8 | COMMON_THEME_VARIABLES 9 | } from './constants'; 10 | 11 | export const staticPrimaryColor = PRIMARY_COLOR; 12 | 13 | const theme: VuetifyOptions['theme'] = { 14 | defaultTheme: 'light', 15 | themes: { 16 | light: { 17 | dark: false, 18 | colors: { 19 | ...LIGHT_THEME_COLORS, 20 | ...GREY_PALETTE_LIGHT, 21 | 'perfect-scrollbar-thumb': '#DBDADE', 22 | 'skin-bordered-background': '#fff', 23 | 'skin-bordered-surface': '#fff' 24 | }, 25 | variables: { 26 | ...COMMON_THEME_VARIABLES, 27 | 'overlay-scrim-background': '#4C4E64', 28 | 'tooltip-background': '#4A5072', 29 | 'overlay-scrim-opacity': 0.5, 30 | 'border-color': '#2F2B3D', 31 | 'switch-opacity': 0.2, 32 | 'switch-disabled-track-opacity': 0.3, 33 | 'switch-disabled-thumb-opacity': 0.4, 34 | 'switch-checked-disabled-opacity': 0.3, 35 | 'shadow-key-umbra-color': '#2F2B3D' 36 | } 37 | }, 38 | dark: { 39 | dark: true, 40 | colors: { 41 | ...DARK_THEME_COLORS, 42 | ...GREY_PALETTE_DARK, 43 | 'perfect-scrollbar-thumb': '#4A5072', 44 | 'skin-bordered-background': '#2f3349', 45 | 'skin-bordered-surface': '#2f3349' 46 | }, 47 | variables: { 48 | ...COMMON_THEME_VARIABLES, 49 | 'overlay-scrim-background': '#101121', 50 | 'tooltip-background': '#5E6692', 51 | 'overlay-scrim-opacity': 0.6, 52 | 'border-color': '#D0D4F1', 53 | 'switch-opacity': 0.4, 54 | 'switch-disabled-track-opacity': 0.4, 55 | 'switch-disabled-thumb-opacity': 0.8, 56 | 'switch-checked-disabled-opacity': 0.3, 57 | 'shadow-key-umbra-color': '#0F1422' 58 | } 59 | } 60 | } 61 | }; 62 | 63 | export default theme; 64 | -------------------------------------------------------------------------------- /src/components/dashboard/RecentTask.vue: -------------------------------------------------------------------------------- 1 | 70 | 78 | -------------------------------------------------------------------------------- /src/components/form-layout/VerticalFormWithIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 69 | -------------------------------------------------------------------------------- /src/store/userStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { fetchMe } from '@/api/user'; 3 | 4 | export interface IUserState { 5 | token: string; 6 | expire_in: number; 7 | username: string; 8 | avatar: string; 9 | permissions: []; 10 | } 11 | /** User Store */ 12 | export const useUserStore = defineStore('user', { 13 | // Default Config State 14 | state: () => ({ 15 | token: '', 16 | expire_in: 0, 17 | username: '', 18 | avatar: '', 19 | permissions: [], 20 | roles: [ 21 | { title: 'Admin', value: 'admin' }, 22 | { title: 'Author', value: 'author' }, 23 | { title: 'Editor', value: 'editor' }, 24 | { title: 'Maintainer', value: 'maintainer' }, 25 | { title: 'Subscriber', value: 'subscriber' } 26 | ], 27 | statusOptions: [ 28 | { title: 'Pending', value: 'pending' }, 29 | { title: 'Active', value: 'active' }, 30 | { title: 'Inactive', value: 'inactive' } 31 | ] 32 | }), 33 | // Getters 34 | getters: { 35 | getRoles(state) { 36 | return state.roles; 37 | }, 38 | getUsername(state) { 39 | return state.username; 40 | }, 41 | getStatusOptions(state) { 42 | return state.statusOptions; 43 | }, 44 | getAccessToken(state) { 45 | return state.token; 46 | } 47 | }, 48 | // Actions 49 | actions: { 50 | setToken(token: string) { 51 | this.token = token; 52 | }, 53 | setUsername(username: string) { 54 | this.username = username; 55 | }, 56 | setProfile(profile) { 57 | this.username = profile.username; 58 | this.avatar = profile.avatar; 59 | }, 60 | async getProfile(): Promise { 61 | try { 62 | const { data } = await fetchMe(); 63 | // set user profile 64 | this.setProfile(data); 65 | return Promise.resolve(true); 66 | } catch (e) { 67 | return Promise.reject(false); 68 | } 69 | } 70 | }, 71 | // Data persistence destination 72 | persist: { 73 | key: import.meta.env.VITE_APP_WEBSTORAGE_NAMESPACE ?? 'vuetify', 74 | storage: window.sessionStorage 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /src/views/forms/FormView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 54 | 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Materiv - Vue3 Typescript based Admin Dashboard Template 9 | 14 | 18 | 19 | 20 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 | 42 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/composables/useApi.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue'; 2 | import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; 3 | 4 | export interface UseApiOptions { 5 | immediate?: boolean; 6 | onSuccess?: (data: T) => void; 7 | onError?: (error: Error) => void; 8 | } 9 | 10 | export function useApi( 11 | url: string | (() => string), 12 | options: AxiosRequestConfig = {}, 13 | apiOptions: UseApiOptions = {} 14 | ) { 15 | const data: Ref = ref(null); 16 | const error: Ref = ref(null); 17 | const loading = ref(false); 18 | 19 | const execute = async (config?: AxiosRequestConfig) => { 20 | loading.value = true; 21 | error.value = null; 22 | 23 | try { 24 | const requestUrl = typeof url === 'function' ? url() : url; 25 | const response = await axios({ 26 | url: requestUrl, 27 | ...options, 28 | ...config 29 | }); 30 | 31 | data.value = response.data; 32 | apiOptions.onSuccess?.(response.data); 33 | return response.data; 34 | } catch (err) { 35 | const apiError = err as AxiosError; 36 | error.value = apiError as Error; 37 | apiOptions.onError?.(apiError as Error); 38 | throw err; 39 | } finally { 40 | loading.value = false; 41 | } 42 | }; 43 | 44 | if (apiOptions.immediate) { 45 | execute(); 46 | } 47 | 48 | return { 49 | data, 50 | error, 51 | loading, 52 | execute 53 | }; 54 | } 55 | 56 | export function useGet(url: string | (() => string), options: UseApiOptions = {}) { 57 | return useApi(url, { method: 'GET' }, options); 58 | } 59 | 60 | export function usePost(url: string | (() => string), options: UseApiOptions = {}) { 61 | return useApi(url, { method: 'POST' }, options); 62 | } 63 | 64 | export function usePut(url: string | (() => string), options: UseApiOptions = {}) { 65 | return useApi(url, { method: 'PUT' }, options); 66 | } 67 | 68 | export function useDelete(url: string | (() => string), options: UseApiOptions = {}) { 69 | return useApi(url, { method: 'DELETE' }, options); 70 | } 71 | -------------------------------------------------------------------------------- /src/plugins/vuetify/constants.ts: -------------------------------------------------------------------------------- 1 | // Theme constants for DRY principle 2 | export const PRIMARY_COLOR = '#304FFD'; 3 | 4 | export const LIGHT_THEME_COLORS = { 5 | primary: PRIMARY_COLOR, 6 | 'on-primary': '#fff', 7 | secondary: '#FF965D', 8 | 'on-secondary': '#fff', 9 | success: '#28C76F', 10 | 'on-success': '#fff', 11 | info: '#00CFE8', 12 | 'on-info': '#fff', 13 | warning: '#FF9F43', 14 | 'on-warning': '#fff', 15 | error: '#EA5455', 16 | background: '#F8F7FA', 17 | 'on-background': '#2F2B3D', 18 | 'on-surface': '#2F2B3D', 19 | 'on-surface-variant': '#FFF' 20 | } as const; 21 | 22 | export const DARK_THEME_COLORS = { 23 | primary: PRIMARY_COLOR, 24 | 'on-primary': '#fff', 25 | secondary: '#A8AAAE', 26 | 'on-secondary': '#fff', 27 | success: '#28C76F', 28 | 'on-success': '#fff', 29 | info: '#00CFE8', 30 | 'on-info': '#fff', 31 | warning: '#FF9F43', 32 | 'on-warning': '#fff', 33 | error: '#EA5455', 34 | background: '#25293C', 35 | 'on-background': '#D0D4F1', 36 | surface: '#2F3349', 37 | 'on-surface': '#D0D4F1' 38 | } as const; 39 | 40 | export const GREY_PALETTE_LIGHT = { 41 | 'grey-50': '#FAFAFA', 42 | 'grey-100': '#F5F5F5', 43 | 'grey-200': '#EEEEEE', 44 | 'grey-300': '#E0E0E0', 45 | 'grey-400': '#BDBDBD', 46 | 'grey-500': '#9E9E9E', 47 | 'grey-600': '#757575', 48 | 'grey-700': '#616161', 49 | 'grey-800': '#424242', 50 | 'grey-900': '#212121' 51 | } as const; 52 | 53 | export const GREY_PALETTE_DARK = { 54 | 'grey-50': '#26293A', 55 | 'grey-100': '#2F3349', 56 | 'grey-200': '#26293A', 57 | 'grey-300': '#4A5072', 58 | 'grey-400': '#5E6692', 59 | 'grey-500': '#7983BB', 60 | 'grey-600': '#AAB3DE', 61 | 'grey-700': '#B6BEE3', 62 | 'grey-800': '#CFD3EC', 63 | 'grey-900': '#E7E9F6' 64 | } as const; 65 | 66 | export const COMMON_THEME_VARIABLES = { 67 | 'code-color': '#d400ff', 68 | 'hover-opacity': 0.04, 69 | 'focus-opacity': 0.12, 70 | 'selected-opacity': 0.06, 71 | 'activated-opacity': 0.16, 72 | 'pressed-opacity': 0.14, 73 | 'dragged-opacity': 0.1, 74 | 'disabled-opacity': 0.42, 75 | 'border-opacity': 0.16, 76 | 'high-emphasis-opacity': 0.78, 77 | 'medium-emphasis-opacity': 0.68 78 | } as const; 79 | -------------------------------------------------------------------------------- /src/components/charts/apex-chart/ApexChartStocksPrices.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 78 | -------------------------------------------------------------------------------- /src/components/charts/MonthlyEarnings.vue: -------------------------------------------------------------------------------- 1 | 55 | 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "materiv", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit && vite build", 7 | "preview": "vite preview --port 5050", 8 | "typecheck": "vue-tsc --noEmit", 9 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 10 | }, 11 | "dependencies": { 12 | "@braks/vue-flow": "^0.4.38", 13 | "@date-io/dayjs": "^3.0.0", 14 | "@fullcalendar/core": "^6.1.15", 15 | "@fullcalendar/daygrid": "^6.1.15", 16 | "@fullcalendar/interaction": "^6.1.15", 17 | "@fullcalendar/list": "^6.1.15", 18 | "@fullcalendar/timegrid": "^6.1.15", 19 | "@fullcalendar/vue3": "^6.1.15", 20 | "@vueuse/core": "^11.3.0", 21 | "@vueuse/math": "^11.3.0", 22 | "apexcharts": "^3.54.1", 23 | "axios": "^1.7.9", 24 | "date-fns": "^4.1.0", 25 | "dayjs": "^1.11.13", 26 | "pinia": "^2.2.8", 27 | "pinia-plugin-persistedstate": "^4.1.3", 28 | "prismjs": "^1.29.0", 29 | "vue": "^3.5.13", 30 | "vue-flatpickr-component": "^11.0.5", 31 | "vue-i18n": "^10.0.5", 32 | "vue-prism-component": "^2.0.0", 33 | "vue-router": "^4.4.5", 34 | "vue3-apexcharts": "^1.10.0", 35 | "vue3-perfect-scrollbar": "^2.0.0", 36 | "vuetify": "^3.11.0", 37 | "yup": "^1.4.0" 38 | }, 39 | "devDependencies": { 40 | "@mdi/font": "^7.4.47", 41 | "@rushstack/eslint-patch": "^1.10.4", 42 | "@types/chance": "^1.1.6", 43 | "@types/node": "^22.10.1", 44 | "@vitejs/plugin-vue": "^5.2.1", 45 | "@vue/eslint-config-prettier": "^10.1.0", 46 | "@vue/eslint-config-typescript": "^14.1.3", 47 | "@vue/tsconfig": "^0.7.0", 48 | "eslint": "^9.16.0", 49 | "eslint-plugin-vue": "^9.31.0", 50 | "msw": "^2.7.0", 51 | "prettier": "^3.4.2", 52 | "sass": "^1.83.0", 53 | "sass-loader": "^16.0.3", 54 | "typescript": "^5.7.2", 55 | "unplugin-auto-import": "^0.18.5", 56 | "unplugin-vue-components": "^0.27.4", 57 | "unplugin-vue-define-options": "^1.4.11", 58 | "vite": "^6.0.1", 59 | "vite-plugin-vue-devtools": "^7.6.10", 60 | "vite-plugin-vuetify": "^2.0.4", 61 | "vue-tsc": "^2.1.10" 62 | }, 63 | "msw": { 64 | "workerDirectory": [ 65 | "public" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/assets/svg/paypal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | paypal 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/card/VerticalProfileCard.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 72 | -------------------------------------------------------------------------------- /src/views/apps/EmailView.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 76 | 79 | -------------------------------------------------------------------------------- /src/data/dashboard/dashboardData.ts: -------------------------------------------------------------------------------- 1 | import type { recentTrans, productPerformanceType } from '@/types/dashboard/index'; 2 | 3 | /*--Recent Transaction--*/ 4 | const recentTransaction: recentTrans[] = [ 5 | { 6 | title: '09:30 am', 7 | subtitle: 'Payment received from John Doe of $385.90', 8 | textcolor: 'primary', 9 | boldtext: false, 10 | line: true, 11 | link: '', 12 | url: '' 13 | }, 14 | { 15 | title: '10:00 am', 16 | subtitle: 'New sale recorded', 17 | textcolor: 'secondary', 18 | boldtext: true, 19 | line: true, 20 | link: '#ML-3467', 21 | url: '' 22 | }, 23 | { 24 | title: '12:00 am', 25 | subtitle: 'Payment was made of $64.95 to Michael', 26 | textcolor: 'success', 27 | boldtext: false, 28 | line: true, 29 | link: '', 30 | url: '' 31 | }, 32 | { 33 | title: '09:30 am', 34 | subtitle: 'New sale recorded', 35 | textcolor: 'warning', 36 | boldtext: true, 37 | line: true, 38 | link: '#ML-3467', 39 | url: '' 40 | }, 41 | { 42 | title: '09:30 am', 43 | subtitle: 'New arrival recorded', 44 | textcolor: 'error', 45 | boldtext: true, 46 | line: true, 47 | link: '', 48 | url: '' 49 | }, 50 | { 51 | title: '12:00 am', 52 | subtitle: 'Payment Received', 53 | textcolor: 'success', 54 | boldtext: false, 55 | line: false, 56 | link: '', 57 | url: '' 58 | } 59 | ]; 60 | 61 | /*Basic Table 1*/ 62 | const productPerformance: productPerformanceType[] = [ 63 | { 64 | id: 1, 65 | name: 'Sunil Joshi', 66 | post: 'Web Designer', 67 | pname: 'Elite Admin', 68 | status: 'Low', 69 | statuscolor: 'primary', 70 | budget: '$3.9' 71 | }, 72 | { 73 | id: 2, 74 | name: 'Andrew McDownland', 75 | post: 'Project Manager', 76 | pname: 'Real Homes WP Theme', 77 | status: 'Medium', 78 | statuscolor: 'secondary', 79 | budget: '$24.5k' 80 | }, 81 | { 82 | id: 3, 83 | name: 'Christopher Jamil', 84 | post: 'Project Manager', 85 | pname: 'MedicalPro WP Theme', 86 | status: 'High', 87 | statuscolor: 'error', 88 | budget: '$12.8k' 89 | }, 90 | { 91 | id: 4, 92 | name: 'Nirav Joshi', 93 | post: 'Frontend Engineer', 94 | pname: 'Hosting Press HTML', 95 | status: 'Critical', 96 | statuscolor: 'success', 97 | budget: '$2.4k' 98 | } 99 | ]; 100 | 101 | export { recentTransaction, productPerformance }; 102 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Vue Material Admin 2 | 3 | 4 | [![CircleCI](https://circleci.com/gh/tookit/vue-material-admin/tree/dev.svg?style=svg)](https://circleci.com/gh/tookit/vue-material-admin/tree/dev) 5 | 6 | English | [简体中文](./README.zh-CN.md) 7 | 8 | 9 | ## Introduction 10 | Vue Material Admin Template is a [Vue](https://vuejs.org/index.html/) Based Material Design Admin Template. 11 | And use [Vuetifyjs](https://vuetifyjs.com/) as base framework. 12 | Vuetify is Awesome. 13 | 14 | ## Discrod Change 15 | [Discrod channel](https://discord.gg/7f6TVx) 16 | 17 | ## Demo 18 | [Vuetify2 version](https://vma.isocked.com/#/auth/login) 19 | [Vuetify3 version](https://vma3.isocked.com/#/auth/login) 20 | 21 | 22 | ## Preview 23 | ![Preivew](http://doc.isocked.com/img/preview.png) 24 | 25 | ## Documentation 26 | 27 | [doc](http://doc.isocked.com/) 28 | 29 | ## Project Structure 30 | ``` bash 31 | ├── src 32 | │ ├── api 33 | │ ├── components 34 | │ ├── mixins 35 | │ ├── views 36 | │ ├── router 37 | │ ├── store 38 | │ ├── util 39 | │ ├── theme 40 | │ │ ├── default.sass 41 | │ └── App.vue 42 | │ └── event.js 43 | │ └── main.js 44 | ├── dist 45 | ├── release 46 | ├── static (or asset) 47 | ├── node_modules 48 | ├── test 49 | ├── README.md 50 | ├── package.json 51 | ├── vue.config.js 52 | ├── index.html 53 | └── .gitignore 54 | ``` 55 | 56 | ## Project setup 57 | ``` 58 | yarn install 59 | ``` 60 | 61 | ### Compiles and hot-reloads for development 62 | ``` 63 | yarn run serve 64 | ``` 65 | 66 | ### Compiles and minifies for production 67 | ``` 68 | yarn run build 69 | ``` 70 | 71 | ### Run your tests 72 | ``` 73 | yarn run test 74 | ``` 75 | 76 | ### Lints and fixes files 77 | ``` 78 | yarn run lint 79 | ``` 80 | 81 | ### Customize configuration 82 | See [Configuration Reference](https://cli.vuejs.org/config/). 83 | 84 | 85 | ### Reference 86 | 87 | * [Vuetifyjs](https://vuetifyjs.com/) 88 | * [Vue](https://vuejs.org/index.html/) 89 | * [ICON](https://materialdesignicons.com/) 90 | * [SASS](http://sass-lang.com/) 91 | 92 | ### Donate 93 | If you find this project useful, you can buy author a glass of juice :tropical_drink: 94 | 95 | 96 | [Paypal Me](https://www.paypal.me/tookit) 97 | 98 | [Buy me a coffee](https://www.buymeacoffee.com/tookit) 99 | 100 | Buy Me A Coffee 101 | 102 | ## License 103 | 104 | [MIT](https://github.com/tookit/vue-material-admin/blob/master/LICENSE) -------------------------------------------------------------------------------- /src/components/charts/YearlyBreakup.vue: -------------------------------------------------------------------------------- 1 | 45 | 79 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "./variables"; 3 | 4 | // @mixin elevation($z, $important: false) { 5 | // box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null); 6 | // } 7 | 8 | // ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element 9 | @mixin before-pseudo() { 10 | position: relative; 11 | 12 | &::before { 13 | position: absolute; 14 | border-radius: inherit; 15 | background: currentcolor; 16 | block-size: 100%; 17 | content: ""; 18 | inline-size: 100%; 19 | inset: 0; 20 | opacity: 0; 21 | pointer-events: none; 22 | } 23 | } 24 | 25 | @mixin bordered-skin($component, $border-property: "border", $important: false) { 26 | #{$component} { 27 | // background-color: rgb(var(--v-theme-background)); 28 | box-shadow: none !important; 29 | #{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null); 30 | } 31 | } 32 | 33 | // ℹ️ Inspired from vuetify's active-states mixin 34 | // focus => 0.12 & selected => 0.08 35 | @mixin selected-states($selector) { 36 | // #{$selector} { 37 | // opacity: calc(#{map.get(vuetify.$states, "selected")} * var(--v-theme-overlay-multiplier)); 38 | // } 39 | 40 | // &:hover 41 | // #{$selector} { 42 | // opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "hover")} * var(--v-theme-overlay-multiplier)); 43 | // } 44 | 45 | // &:focus-visible 46 | // #{$selector} { 47 | // opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier)); 48 | // } 49 | 50 | // @supports not selector(:focus-visible) { 51 | // &:focus { 52 | // #{$selector} { 53 | // opacity: calc(#{map.get(vuetify.$states, "selected") + map.get(vuetify.$states, "focus")} * var(--v-theme-overlay-multiplier)); 54 | // } 55 | // } 56 | // } 57 | #{$selector} { 58 | opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier)); 59 | } 60 | 61 | &:hover 62 | #{$selector} { 63 | opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier)); 64 | } 65 | 66 | &:focus-visible 67 | #{$selector} { 68 | opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier)); 69 | } 70 | 71 | @supports not selector(:focus-visible) { 72 | &:focus { 73 | #{$selector} { 74 | opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier)); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/views/setting/SettingView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 69 | -------------------------------------------------------------------------------- /src/components/AppCardCode.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 80 | 81 | 89 | -------------------------------------------------------------------------------- /src/components/forms/UserForm.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 93 | @/store/userStore 94 | -------------------------------------------------------------------------------- /src/components/form-layout/HorizontalForm.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 89 | -------------------------------------------------------------------------------- /src/plugins/i18n/locales/zhHans.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ID", 3 | "UI Elements": "UI元素", 4 | "apps": "应用", 5 | "dashboard": "仪表盘", 6 | "chat": "聊天", 7 | "calendar": "日历", 8 | "card": "卡片", 9 | "widgets": "组件", 10 | "auth": "认证", 11 | "login": "登录", 12 | "register": "注册", 13 | "form": "表单", 14 | "table": "表格", 15 | "chart": "图表", 16 | "Create account": "创建账户", 17 | "password": "密码", 18 | "username": "用户名", 19 | "email": "邮箱", 20 | "sign_in": "登录", 21 | "sign_up": "注册", 22 | "remember": "记住", 23 | "forget_pass": "忘记密码", 24 | "username required": "请输入用户名", 25 | "password required": "请输入密码", 26 | "have_account": "已经有账户", 27 | "view": "查看", 28 | "edit": "编辑", 29 | "delete": "删除", 30 | "role": "角色", 31 | "status": "状态", 32 | "apply": "应用", 33 | "reset": "重置", 34 | "avatar": "图像", 35 | "action": "操作", 36 | "sales_overview": "销售统计", 37 | "yearly_breakup": "年度统计", 38 | "monthly_earnings": "月度收入", 39 | "recent_transactions": "最近交易", 40 | "recent_task": "最近任务", 41 | "last_year": "去年", 42 | "name": "用户名", 43 | "position": "职位", 44 | "project": "项目", 45 | "recent": "最近", 46 | "contact": "联系人", 47 | "add_event": "添加事件", 48 | "filter": "过滤", 49 | "view_all": "查看所有", 50 | "personal": "个人", 51 | "business": "商业", 52 | "family": "家庭", 53 | "holiday": "节日", 54 | "meeting": "会议", 55 | "profile": "资料", 56 | "$vuetify": { 57 | "loading": "加载中", 58 | "open": "打开", 59 | "close": "关闭", 60 | "badge": "徽章", 61 | "datePicker": { 62 | "itemsSelected": "'{0} selected'", 63 | "range": { 64 | "title": "Select dates", 65 | "header": "Enter dates" 66 | }, 67 | "title": "Select date", 68 | "header": "Enter date", 69 | "input": { 70 | "placeholder": "Enter date" 71 | } 72 | }, 73 | "dataIterator": { 74 | "loadingText": "加载中" 75 | }, 76 | "dataFooter": { 77 | "pageText": "{0}-{1} 共 {2}", 78 | "firstPage": "首页", 79 | "lastPage": "尾页", 80 | "prevPage": "上一页", 81 | "nextPage": "下一页", 82 | "itemsPerPageText": "每页数目:", 83 | "itemsPerPageAll": "所有" 84 | }, 85 | "pagination": { 86 | "ariaLabel": { 87 | "root": "root", 88 | "previous": "previous", 89 | "next": "next", 90 | "currentPage": "currentPage", 91 | "page": "page" 92 | } 93 | }, 94 | "input": { 95 | "clear": "clear", 96 | "appendAction": "appendAction", 97 | "prependAction": "prependAction", 98 | "counterSize": "counterSize" 99 | }, 100 | "fileInput": { 101 | "counterSize": "counterSize" 102 | }, 103 | "rating": { 104 | "ariaLabel": { 105 | "item": "item" 106 | } 107 | } 108 | }, 109 | "10": "10", 110 | "25": "25", 111 | "50": "50", 112 | "100": "100" 113 | } 114 | -------------------------------------------------------------------------------- /src/components/forms/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [Unreleased] - 2025-12-03 6 | 7 | ### Updated 8 | 9 | - **Vuetify**: Upgraded from 3.5.6 to 3.11.0 (latest version) 10 | - **Vue**: Updated to 3.5.13 with latest Composition API features 11 | - **Vite**: Upgraded to 6.0.1 for improved build performance 12 | - **TypeScript**: Updated to 5.7.2 13 | - **Pinia**: Updated to 2.2.8 14 | - **Vue Router**: Updated to 4.4.5 15 | - **Vue I18n**: Updated to 10.0.5 16 | - All other dependencies updated to their latest stable versions 17 | 18 | ### Added 19 | 20 | - **Composables** (DRY Principle): 21 | - `useDrawer`: Reusable drawer state management 22 | - `useApi`: Generic API call handler with loading/error states 23 | - `useFormValidation`: Common form validation rules 24 | - Centralized composables export in `src/composables/index.ts` 25 | 26 | - **Theme Constants**: Extracted theme colors and variables to `src/plugins/vuetify/constants.ts` for better maintainability 27 | 28 | - **Documentation**: 29 | - Enhanced README with modern project structure 30 | - Added tech stack section 31 | - Added browser support information 32 | - Added Star History chart 33 | - Improved getting started guide 34 | - Added configuration documentation 35 | 36 | ### Changed 37 | 38 | - **Vuetify Plugin**: 39 | - Removed manual component imports (using auto-import) 40 | - Switched to VuetifyDateAdapter (built-in) 41 | - Added explicit 'vuetify/styles' import 42 | - Cleaner plugin configuration 43 | 44 | - **Main.ts**: 45 | - Reorganized plugin registration order 46 | - Added comments for better code organization 47 | - Grouped style imports 48 | 49 | - **Vite Config**: 50 | - Updated to use `node:url` import (Node.js best practice) 51 | - Added build optimization with manual chunks 52 | - Configured modern SCSS compiler API 53 | - Improved code splitting strategy 54 | - Better dependency optimization 55 | 56 | - **AppSidebar**: 57 | - Refactored to use `useDrawer` composable 58 | - Cleaner computed properties 59 | - Improved code readability 60 | - Fixed register route bug 61 | 62 | - **Theme Configuration**: 63 | - Refactored to use constants (DRY principle) 64 | - Reduced code duplication 65 | - Easier to maintain and customize 66 | 67 | ### Best Practices Applied 68 | 69 | - ✅ Composition API with ` 83 | 100 | -------------------------------------------------------------------------------- /src/plugins/i18n/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ID", 3 | "UI Elements": "UI Elements", 4 | "apps": "apps", 5 | "dashboard": "dashboard", 6 | "chat": "chat", 7 | "calendar": "calendar", 8 | "card": "card", 9 | "widgets": "widgets", 10 | "auth": "auth", 11 | "login": "login", 12 | "register": "register", 13 | "form": "form", 14 | "table": "table", 15 | "chart": "chart", 16 | "Create account": "create account", 17 | "password": "password", 18 | "username": "username", 19 | "email": "email", 20 | "sign_in": "sign in", 21 | "sign_up": "sign up", 22 | "remember": "remember", 23 | "forget_pass": "forgot password", 24 | "username required": "username required", 25 | "password required": "password required", 26 | "have_account": "already have account", 27 | "view": "view", 28 | "edit": "edit", 29 | "delete": "delete", 30 | "role": "role", 31 | "status": "status", 32 | "apply": "apply", 33 | "reset": "reset", 34 | "avatar": "avatar", 35 | "action": "action", 36 | "sales_overview": "sales overview", 37 | "yearly_breakup": "yearly breakup", 38 | "monthly_earnings": "monthly earnings", 39 | "recent_transactions": "recent transactions", 40 | "recent_task": "recent task", 41 | "last_year": "last year", 42 | "name": "name", 43 | "position": "position", 44 | "project": "project", 45 | "recent": "recent", 46 | "contact": "contact", 47 | "add_event": "add event", 48 | "filter": "filter", 49 | "view_all": "view all", 50 | "personal": "perosnal", 51 | "business": "business", 52 | "family": "family", 53 | "holiday": "holiday", 54 | "meeting": "meeting", 55 | "profile": "profile", 56 | "$vuetify": { 57 | "loading": "loading", 58 | "open": "open", 59 | "close": "close", 60 | "badge": "badge", 61 | "datePicker": { 62 | "itemsSelected": "'{0} selected'", 63 | "range": { 64 | "title": "Select dates", 65 | "header": "Enter dates" 66 | }, 67 | "title": "Select date", 68 | "header": "Enter date", 69 | "input": { 70 | "placeholder": "Enter date" 71 | } 72 | }, 73 | "dataIterator": { 74 | "loadingText": "loading" 75 | }, 76 | "dataFooter": { 77 | "pageText": "{0}-{1} 共 {2}", 78 | "firstPage": "First Page", 79 | "lastPage": "Last Page", 80 | "prevPage": "Previous Page", 81 | "nextPage": "Next Page", 82 | "itemsPerPageText": "Items:", 83 | "itemsPerPageAll": "All" 84 | }, 85 | "pagination": { 86 | "ariaLabel": { 87 | "root": "root", 88 | "previous": "previous", 89 | "next": "next", 90 | "currentPage": "currentPage", 91 | "page": "page" 92 | } 93 | }, 94 | "input": { 95 | "clear": "clear", 96 | "appendAction": "appendAction", 97 | "prependAction": "prependAction", 98 | "counterSize": "counterSize" 99 | }, 100 | "fileInput": { 101 | "counterSize": "counterSize" 102 | }, 103 | "rating": { 104 | "ariaLabel": { 105 | "item": "item" 106 | } 107 | } 108 | }, 109 | "10": "10", 110 | "25": "25", 111 | "50": "50", 112 | "100": "100" 113 | } 114 | -------------------------------------------------------------------------------- /src/components/form-layout/HorizontalFormWithIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 103 | -------------------------------------------------------------------------------- /src/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 93 | 94 | 99 | -------------------------------------------------------------------------------- /src/components/AppSidebar.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 90 | 91 | 109 | -------------------------------------------------------------------------------- /src/assets/svg/keyboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/scss/layout/_sidebar.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | /*This is for the logo*/ 3 | .leftSidebar { 4 | box-shadow: none !important; 5 | border-right: 1px solid rgb(var(--v-theme-borderColor)); 6 | .logo { 7 | padding-left: 7px; 8 | } 9 | 10 | .mini-icon { 11 | display: none; 12 | } 13 | 14 | .mini-text { 15 | display: block; 16 | } 17 | } 18 | 19 | /*This is for the Vertical sidebar*/ 20 | .scrollnavbar { 21 | height: calc(100vh - 80px); 22 | 23 | .userbottom { 24 | position: fixed; 25 | bottom: 0px; 26 | width: 100%; 27 | } 28 | 29 | .smallCap { 30 | padding: 3px 12px !important; 31 | font-size: 0.875rem; 32 | font-weight: 500; 33 | margin-top: 24px; 34 | color: rgb(var(--v-theme-textPrimary)); 35 | 36 | &:first-child { 37 | margin-top: 0 !important; 38 | } 39 | } 40 | 41 | /*General Menu css*/ 42 | .v-list-group__items .v-list-item, 43 | .v-list-item { 44 | border-radius: $border-radius-root + 4; 45 | padding-inline-start: calc(12px + var(--indent-padding) / 10) !important; 46 | 47 | margin: 0 0 2px; 48 | 49 | &:hover { 50 | color: rgb(var(--v-theme-primary)); 51 | } 52 | 53 | .v-list-item__prepend { 54 | margin-inline-end: 13px; 55 | } 56 | 57 | .v-list-item__append { 58 | font-size: 0.875rem; 59 | 60 | .v-icon { 61 | margin-inline-start: 13px; 62 | } 63 | } 64 | 65 | .v-list-item-title { 66 | font-size: 0.875rem; 67 | } 68 | } 69 | .v-list { 70 | color: rgb(var(--v-theme-textSecondary)); 71 | 72 | > .v-list-item.v-list-item--active, 73 | .v-list-item--active > .v-list-item__overlay { 74 | background: rgb(var(--v-theme-primary)); 75 | color: white !important; 76 | } 77 | 78 | > .v-list-group { 79 | position: relative; 80 | 81 | > .v-list-item--active, 82 | > .v-list-item--active:hover { 83 | background: rgb(var(--v-theme-primary)); 84 | color: white; 85 | } 86 | 87 | .v-list-group__items .v-list-item.v-list-item--active, 88 | .v-list-group__items .v-list-item.v-list-item--active > .v-list-item__overlay { 89 | background: transparent; 90 | color: rgb(var(--v-theme-primary)); 91 | } 92 | } 93 | } 94 | } 95 | 96 | @media only screen and (min-width: 1170px) { 97 | .mini-sidebar { 98 | .logo { 99 | width: 40px; 100 | overflow: hidden; 101 | padding-left: 0; 102 | } 103 | 104 | .mini-icon { 105 | display: block; 106 | } 107 | 108 | .mini-text { 109 | display: none; 110 | } 111 | 112 | .v-list { 113 | padding: 14px !important; 114 | } 115 | 116 | .leftSidebar:hover { 117 | box-shadow: $box-shadow !important; 118 | 119 | .mini-icon { 120 | display: none; 121 | } 122 | 123 | .mini-text { 124 | display: block; 125 | } 126 | } 127 | 128 | .v-navigation-drawer--expand-on-hover:hover { 129 | .logo { 130 | width: 100%; 131 | } 132 | 133 | .v-list .v-list-group__items, 134 | .hide-menu { 135 | opacity: 1; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @use 'sass:map'; 3 | @use 'sass:meta'; 4 | @use 'vuetify/lib/styles/tools/functions' as *; 5 | 6 | // This will false all colors which is not necessory for theme 7 | $color-pack: false; 8 | 9 | 10 | // Global font size and border radius 11 | $font-size-root: 1rem; 12 | $border-radius-root: 4px; 13 | $body-font-family: 'Poppins', sans-serif !default; 14 | $heading-font-family: $body-font-family !default; 15 | 16 | // Global Radius as per breakeven point 17 | $rounded: () !default; 18 | $rounded: map-deep-merge( 19 | ( 20 | 0: 0, 21 | 'sm': $border-radius-root * 0.5, 22 | null: $border-radius-root, 23 | 'md': $border-radius-root * 1, 24 | 'lg': $border-radius-root * 2, 25 | 'xl': $border-radius-root * 6, 26 | 'pill': 9999px, 27 | 'circle': 50%, 28 | 'shaped': $border-radius-root * 6 0 29 | ), 30 | $rounded 31 | ); 32 | // Global Typography 33 | $typography: () !default; 34 | $typography: map-deep-merge( 35 | ( 36 | 'h1': ( 37 | 'size': 2.25rem, 38 | 'weight': 600, 39 | 'line-height': 2.75rem, 40 | 'font-family': inherit 41 | ), 42 | 'h2': ( 43 | 'size': 1.875rem, 44 | 'weight': 600, 45 | 'line-height': 2.25rem, 46 | 'font-family': inherit 47 | ), 48 | 'h3': ( 49 | 'size': 1.5rem, 50 | 'weight': 400, 51 | 'line-height': 2rem, 52 | 'font-family': inherit 53 | ), 54 | 'h4': ( 55 | 'size': 1.3125rem, 56 | 'weight': 400, 57 | 'line-height': 1.6rem, 58 | 'font-family': inherit 59 | ), 60 | 'h5': ( 61 | 'size': 1.125rem, 62 | 'weight': 400, 63 | 'line-height': 1.6rem, 64 | 'font-family': inherit 65 | ), 66 | 'h6': ( 67 | 'size': 1rem, 68 | 'weight': 300, 69 | 'line-height': 1.2rem, 70 | 'font-family': inherit 71 | ), 72 | 'subtitle-1': ( 73 | 'size': 0.875rem, 74 | 'weight': 400, 75 | 'line-height': 1.1rem, 76 | 'font-family': inherit 77 | ), 78 | 'subtitle-2': ( 79 | 'size': 0.75rem, 80 | 'weight': 400, 81 | 'line-height': 1rem, 82 | 'font-family': inherit 83 | ), 84 | 'body-1': ( 85 | 'size': 0.875rem, 86 | 'weight': 400, 87 | 'font-family': inherit 88 | ), 89 | 'body-2': ( 90 | 'size': 0.75rem, 91 | 'weight': 400, 92 | 'font-family': inherit 93 | ), 94 | 'button': ( 95 | 'size': 0.875rem, 96 | 'weight': 500, 97 | 'font-family': inherit, 98 | 'text-transform': capitalize 99 | ), 100 | 'caption': ( 101 | 'size': 0.75rem, 102 | 'weight': 400, 103 | 'font-family': inherit 104 | ), 105 | 'overline': ( 106 | 'size': 0.75rem, 107 | 'weight': 500, 108 | 'font-family': inherit, 109 | 'text-transform': uppercase 110 | ) 111 | ), 112 | $typography 113 | ); 114 | 115 | // Custom Variables 116 | // colors 117 | $white: #fff !default; 118 | 119 | // cards 120 | $card-item-spacer-xy: 20px 24px !default; 121 | $card-text-spacer: 24px !default; 122 | $card-title-size: 18px !default; 123 | // Global Shadow 124 | $box-shadow: rgb(145 158 171 / 20%) 0px 0px 2px 0px, rgb(145 158 171 / 12%) 0px 12px 24px -4px; 125 | 126 | // Buttons 127 | $btn-letter-spacing: 0.0892857143em !default; 128 | -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty, isEmptyArray, isNullOrUndefined } from './index'; 2 | 3 | // 👉 Required Validator 4 | export const requiredValidator = (value: unknown) => { 5 | if (isNullOrUndefined(value) || isEmptyArray(value) || value === false) return 'This field is required'; 6 | 7 | return !!String(value).trim().length || 'This field is required'; 8 | }; 9 | 10 | // 👉 Email Validator 11 | export const emailValidator = (value: unknown) => { 12 | if (isEmpty(value)) return true; 13 | 14 | const re = 15 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 16 | 17 | if (Array.isArray(value)) 18 | return value.every((val) => re.test(String(val))) || 'The Email field must be a valid email'; 19 | 20 | return re.test(String(value)) || 'The Email field must be a valid email'; 21 | }; 22 | 23 | // 👉 Password Validator 24 | export const passwordValidator = (password: string) => { 25 | const regExp = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*()]).{8,}/; 26 | 27 | const validPassword = regExp.test(password); 28 | 29 | return ( 30 | // eslint-disable-next-line operator-linebreak 31 | validPassword || 32 | 'Field must contain at least one uppercase, lowercase, special character and digit with min 8 chars' 33 | ); 34 | }; 35 | 36 | // 👉 Confirm Password Validator 37 | export const confirmedValidator = (value: string, target: string) => 38 | value === target || 'The Confirm Password field confirmation does not match'; 39 | 40 | // 👉 Between Validator 41 | export const betweenValidator = (value: unknown, min: number, max: number) => { 42 | const valueAsNumber = Number(value); 43 | 44 | return (Number(min) <= valueAsNumber && Number(max) >= valueAsNumber) || `Enter number between ${min} and ${max}`; 45 | }; 46 | 47 | // 👉 Integer Validator 48 | export const integerValidator = (value: unknown) => { 49 | if (isEmpty(value)) return true; 50 | 51 | if (Array.isArray(value)) 52 | return value.every((val) => /^-?[0-9]+$/.test(String(val))) || 'This field must be an integer'; 53 | 54 | return /^-?[0-9]+$/.test(String(value)) || 'This field must be an integer'; 55 | }; 56 | 57 | // 👉 Regex Validator 58 | export const regexValidator = (value: unknown, regex: RegExp | string): string | boolean => { 59 | if (isEmpty(value)) return true; 60 | 61 | let regeX = regex; 62 | if (typeof regeX === 'string') regeX = new RegExp(regeX); 63 | 64 | if (Array.isArray(value)) return value.every((val) => regexValidator(val, regeX)); 65 | 66 | return regeX.test(String(value)) || 'The Regex field format is invalid'; 67 | }; 68 | 69 | // 👉 Alpha Validator 70 | export const alphaValidator = (value: unknown) => { 71 | if (isEmpty(value)) return true; 72 | 73 | return /^[A-Z]*$/i.test(String(value)) || 'The Alpha field may only contain alphabetic characters'; 74 | }; 75 | 76 | // 👉 URL Validator 77 | export const urlValidator = (value: unknown) => { 78 | if (isEmpty(value)) return true; 79 | 80 | const re = /^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,5}[\.]{0,1}/; 81 | 82 | return re.test(String(value)) || 'URL is invalid'; 83 | }; 84 | 85 | // 👉 Length Validator 86 | export const lengthValidator = (value: unknown, length: number) => { 87 | if (isEmpty(value)) return true; 88 | 89 | return String(value).length === length || `The Min Character field must be at least ${length} characters`; 90 | }; 91 | 92 | // 👉 Alpha-dash Validator 93 | export const alphaDashValidator = (value: unknown) => { 94 | if (isEmpty(value)) return true; 95 | 96 | const valueAsString = String(value); 97 | 98 | return /^[0-9A-Z_-]*$/i.test(valueAsString) || 'All Character are not valid'; 99 | }; 100 | -------------------------------------------------------------------------------- /src/plugins/msw/handlers/db.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '@/api/type'; 2 | 3 | export const users: IUser[] = [ 4 | { 5 | id: 1, 6 | username: 'admin', 7 | role: 'admin', 8 | company: 'isocked.com', 9 | job: 'Developer', 10 | country: 'China', 11 | city: 'Shenzhen', 12 | phone: '(+86) 4567-8900', 13 | email: 'wangqiangshen@gmail.com', 14 | status: 'active', 15 | avatar: '/assets/images/users/avatar-1.jpg', 16 | access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTB9.txWLuN4QT5PqTtgHmlOiNerIu5Do51PpYOiZutkyXYg' 17 | }, 18 | { 19 | id: 2, 20 | username: 'Jane Wilison', 21 | role: 'author', 22 | company: 'ESG', 23 | job: 'Designer', 24 | country: 'USA', 25 | city: 'New York', 26 | phone: '(+123) 4567-8900', 27 | email: 'jane.wilison@test.com', 28 | status: 'pending', 29 | avatar: '/assets/images/avatars/avatar-2.png' 30 | }, 31 | { 32 | id: 3, 33 | username: 'John Doe', 34 | role: 'author', 35 | company: 'ESG', 36 | job: 'Designer', 37 | country: 'USA', 38 | city: 'New York', 39 | phone: '(+123) 4567-8900', 40 | email: 'john.doe@test.com', 41 | status: 'active', 42 | avatar: '/assets/images/avatars/avatar-3.png' 43 | }, 44 | { 45 | id: 4, 46 | username: 'John Will', 47 | role: 'author', 48 | company: 'ESG', 49 | job: 'Project Manager', 50 | country: 'USA', 51 | city: 'New York', 52 | phone: '(+123) 4567-8900', 53 | email: 'john.will@test.com', 54 | status: 'active', 55 | avatar: '/assets/images/avatars/avatar-4.png' 56 | }, 57 | { 58 | id: 5, 59 | username: 'Maggy Hurran', 60 | role: 'author', 61 | company: 'ESG', 62 | job: 'Developer', 63 | country: 'Canada', 64 | city: 'Wotawa', 65 | phone: '(669) 914-1078', 66 | email: 'maggy.hurran@test.com', 67 | status: 'pending', 68 | avatar: '/assets/images/avatars/avatar-5.png' 69 | }, 70 | { 71 | id: 6, 72 | username: 'Silvain Lane', 73 | role: 'author', 74 | company: 'ESG', 75 | job: 'Developer', 76 | country: 'USA', 77 | city: 'Boston', 78 | phone: '(+123) 914-1078', 79 | email: 'maggy.hurran@test.com', 80 | status: 'pending', 81 | avatar: '/assets/images/avatars/avatar-6.png' 82 | }, 83 | { 84 | id: 7, 85 | username: 'Shane Black', 86 | role: 'author', 87 | company: 'ESG', 88 | job: 'Developer', 89 | country: 'USA', 90 | city: 'Huston', 91 | phone: '(+123) 914-1078', 92 | email: 'shane.black@test.com', 93 | status: 'active', 94 | avatar: '/assets/images/avatars/avatar-7.png' 95 | }, 96 | { 97 | id: 8, 98 | username: 'Jacob Hukins', 99 | role: 'author', 100 | company: 'ESG', 101 | job: 'Designer', 102 | country: 'USA', 103 | city: 'Huston', 104 | phone: '(+123) 456-789', 105 | email: 'jacob.hukins@test.com', 106 | status: 'pending', 107 | avatar: '/assets/images/avatars/avatar-8.png' 108 | }, 109 | { 110 | id: 9, 111 | username: 'Priscilla Lung', 112 | role: 'author', 113 | company: 'ESG', 114 | job: 'Designer', 115 | country: 'China', 116 | city: 'HongKong', 117 | phone: '(+123) 456-789', 118 | email: 'priscilla.lung@test.com', 119 | status: 'active', 120 | avatar: '/assets/images/avatars/avatar-9.png' 121 | }, 122 | { 123 | id: 10, 124 | username: 'Bruce Russel', 125 | role: 'author', 126 | company: 'ESG', 127 | job: 'Developer', 128 | country: 'China', 129 | city: 'TianJin', 130 | phone: '(+123) 456-789', 131 | email: 'Bruce.Russel@test.com', 132 | status: 'active', 133 | avatar: '/assets/images/avatars/avatar-10.png' 134 | }, 135 | { 136 | id: 11, 137 | username: 'Brain Pena', 138 | role: 'author', 139 | company: 'ESG', 140 | job: 'Developer', 141 | country: 'UK', 142 | city: 'London', 143 | phone: '(+123) 456-789', 144 | email: 'brain.pena@test.com', 145 | status: 'active', 146 | avatar: '/assets/images/avatars/avatar-1.png' 147 | }, 148 | { 149 | id: 12, 150 | username: 'Kristin Tina', 151 | role: 'author', 152 | company: 'ESG', 153 | job: 'Developer', 154 | country: 'UK', 155 | city: 'London', 156 | phone: '(+123) 456-789', 157 | email: 'kristin.tina@test.com', 158 | status: 'active', 159 | avatar: '/assets/images/avatars/avatar-2.png' 160 | } 161 | ]; 162 | -------------------------------------------------------------------------------- /src/plugins/vuetify/defaults.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | IconBtn: { 3 | icon: true, 4 | color: 'default', 5 | variant: 'text', 6 | density: 'comfortable', 7 | VIcon: { 8 | size: 22 9 | } 10 | }, 11 | VAlert: { 12 | density: 'comfortable', 13 | VBtn: { 14 | color: undefined 15 | } 16 | }, 17 | VBadge: { 18 | // set v-badge default color to primary 19 | color: 'primary' 20 | }, 21 | VBtn: { 22 | // set v-btn default color to primary 23 | color: 'primary' 24 | }, 25 | VCard: { 26 | elevation: 10, 27 | rounded: 'lg' 28 | }, 29 | VChip: { 30 | size: 'small' 31 | }, 32 | VExpansionPanel: { 33 | expandIcon: 'tabler-chevron-right', 34 | collapseIcon: 'tabler-chevron-right' 35 | }, 36 | VExpansionPanelTitle: { 37 | expandIcon: 'tabler-chevron-right', 38 | collapseIcon: 'tabler-chevron-right' 39 | }, 40 | VList: { 41 | density: 'comfortable', 42 | VCheckboxBtn: { 43 | density: 'compact' 44 | } 45 | }, 46 | VPagination: { 47 | activeColor: 'primary', 48 | density: 'comfortable', 49 | variant: 'tonal' 50 | }, 51 | VTabs: { 52 | // set v-tabs default color to primary 53 | color: 'primary', 54 | density: 'comfortable', 55 | VSlideGroup: { 56 | showArrows: true 57 | } 58 | }, 59 | VTooltip: { 60 | // set v-tooltip default location to top 61 | location: 'top' 62 | }, 63 | VCheckboxBtn: { 64 | color: 'primary' 65 | }, 66 | VCheckbox: { 67 | // set v-checkbox default color to primary 68 | color: 'primary', 69 | density: 'comfortable', 70 | hideDetails: 'auto' 71 | }, 72 | VRadioGroup: { 73 | color: 'primary', 74 | density: 'comfortable', 75 | hideDetails: 'auto' 76 | }, 77 | VRadio: { 78 | density: 'comfortable', 79 | hideDetails: 'auto' 80 | }, 81 | VSelect: { 82 | variant: 'outlined', 83 | density: 'compact', 84 | color: 'primary', 85 | hideDetails: 'auto', 86 | VChip: { 87 | color: 'primary', 88 | label: true 89 | } 90 | }, 91 | VRangeSlider: { 92 | // set v-range-slider default color to primary 93 | color: 'primary', 94 | trackColor: 'rgb(var(--v-theme-on-surface),0.06)', 95 | trackSize: 6, 96 | thumbSize: 7, 97 | density: 'comfortable', 98 | thumbLabel: true, 99 | hideDetails: 'auto' 100 | }, 101 | VRating: { 102 | // set v-rating default color to primary 103 | color: 'warning' 104 | }, 105 | VProgressCircular: { 106 | // set v-progress-circular default color to primary 107 | color: 'primary' 108 | }, 109 | VProgressLinear: { 110 | height: 12, 111 | roundedBar: true, 112 | rounded: true, 113 | bgColor: 'rgb(var(--v-theme-on-surface))' 114 | }, 115 | VSlider: { 116 | // set v-slider default color to primary 117 | color: 'primary', 118 | trackColor: 'rgb(var(--v-theme-on-surface),0.06)', 119 | hideDetails: 'auto', 120 | thumbSize: 7, 121 | trackSize: 6 122 | }, 123 | VTextField: { 124 | variant: 'outlined', 125 | density: 'compact', 126 | color: 'primary', 127 | hideDetails: 'auto' 128 | }, 129 | VAutocomplete: { 130 | variant: 'outlined', 131 | color: 'primary', 132 | density: 'compact', 133 | hideDetails: 'auto', 134 | menuProps: { 135 | contentClass: 'app-autocomplete__content v-autocomplete__content' 136 | }, 137 | VChip: { 138 | color: 'primary', 139 | label: true 140 | } 141 | }, 142 | VCombobox: { 143 | variant: 'outlined', 144 | density: 'compact', 145 | color: 'primary', 146 | hideDetails: 'auto', 147 | VChip: { 148 | color: 'primary', 149 | label: true 150 | } 151 | }, 152 | VFileInput: { 153 | variant: 'outlined', 154 | density: 'compact', 155 | color: 'primary', 156 | hideDetails: 'auto' 157 | }, 158 | VTextarea: { 159 | variant: 'outlined', 160 | density: 'compact', 161 | color: 'primary', 162 | hideDetails: 'auto' 163 | }, 164 | VSwitch: { 165 | // set v-switch default color to primary 166 | inset: true, 167 | color: 'primary', 168 | hideDetails: 'auto' 169 | }, 170 | VTimeline: { 171 | lineThickness: 1 172 | }, 173 | 174 | VDataTable: { 175 | VDataTableFooter: { 176 | VBtn: { 177 | density: 'comfortable', 178 | color: 'default' 179 | } 180 | } 181 | } 182 | }; 183 | -------------------------------------------------------------------------------- /src/views/apps/CalendarView.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 88 | 126 | -------------------------------------------------------------------------------- /src/components/forms/EventForm.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 131 | -------------------------------------------------------------------------------- /src/plugins/msw/handlers/eventHandler.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { ICalendarEvent } from '@/api/type'; 4 | 5 | const date = new Date(); 6 | const nextDay = new Date(new Date().getTime() + 24 * 60 * 60 * 1000); 7 | const nextMonth = 8 | date.getMonth() === 11 9 | ? new Date(date.getFullYear() + 1, 0, 1) 10 | : new Date(date.getFullYear(), date.getMonth() + 1, 1); 11 | const prevMonth = 12 | date.getMonth() === 11 13 | ? new Date(date.getFullYear() - 1, 0, 1) 14 | : new Date(date.getFullYear(), date.getMonth() - 1, 1); 15 | 16 | const events: ICalendarEvent[] = [ 17 | { 18 | id: '1', 19 | url: '', 20 | title: 'Design Review', 21 | start: date.toUTCString(), 22 | end: nextDay.toUTCString(), 23 | allDay: false, 24 | extendedProps: { 25 | calendar: 'Business' 26 | } 27 | }, 28 | { 29 | id: '2', 30 | url: '', 31 | title: 'Meeting With Client', 32 | start: new Date(date.getFullYear(), date.getMonth() + 1, -11).toUTCString(), 33 | end: new Date(date.getFullYear(), date.getMonth() + 1, -10).toUTCString(), 34 | allDay: true, 35 | extendedProps: { 36 | calendar: 'Business' 37 | } 38 | }, 39 | { 40 | id: '3', 41 | url: '', 42 | title: 'Family Trip', 43 | allDay: true, 44 | start: new Date(date.getFullYear(), date.getMonth() + 1, -9).toUTCString(), 45 | end: new Date(date.getFullYear(), date.getMonth() + 1, -7).toUTCString(), 46 | extendedProps: { 47 | calendar: 'Holiday' 48 | } 49 | }, 50 | { 51 | id: '4', 52 | url: '', 53 | title: "Doctor's Appointment", 54 | start: new Date(date.getFullYear(), date.getMonth() + 1, -11).toUTCString(), 55 | end: new Date(date.getFullYear(), date.getMonth() + 1, -10).toUTCString(), 56 | allDay: true, 57 | extendedProps: { 58 | calendar: 'Personal' 59 | } 60 | }, 61 | { 62 | id: '5', 63 | url: '', 64 | title: 'Dart Game?', 65 | start: new Date(date.getFullYear(), date.getMonth() + 1, -13).toUTCString(), 66 | end: new Date(date.getFullYear(), date.getMonth() + 1, -12).toUTCString(), 67 | allDay: true, 68 | extendedProps: { 69 | calendar: 'Metting' 70 | } 71 | }, 72 | { 73 | id: '6', 74 | url: '', 75 | title: 'Meditation', 76 | start: new Date(date.getFullYear(), date.getMonth() + 1, -13).toUTCString(), 77 | end: new Date(date.getFullYear(), date.getMonth() + 1, -12).toUTCString(), 78 | allDay: true, 79 | extendedProps: { 80 | calendar: 'Personal' 81 | } 82 | }, 83 | { 84 | id: '7', 85 | url: '', 86 | title: 'Dinner', 87 | start: new Date(date.getFullYear(), date.getMonth() + 1, -13).toUTCString(), 88 | end: new Date(date.getFullYear(), date.getMonth() + 1, -12).toUTCString(), 89 | allDay: true, 90 | extendedProps: { 91 | calendar: 'Family' 92 | } 93 | }, 94 | { 95 | id: '8', 96 | url: '', 97 | title: 'Product Review', 98 | start: new Date(date.getFullYear(), date.getMonth() + 1, -13).toUTCString(), 99 | end: new Date(date.getFullYear(), date.getMonth() + 1, -12).toUTCString(), 100 | allDay: true, 101 | extendedProps: { 102 | calendar: 'Business' 103 | } 104 | }, 105 | { 106 | id: '9', 107 | url: '', 108 | title: 'Monthly Meeting', 109 | start: nextMonth.toUTCString(), 110 | end: nextMonth.toUTCString(), 111 | allDay: true, 112 | extendedProps: { 113 | calendar: 'Business' 114 | } 115 | }, 116 | { 117 | id: '10', 118 | url: '', 119 | title: 'Monthly Checkup', 120 | start: prevMonth.toUTCString(), 121 | end: prevMonth.toUTCString(), 122 | allDay: true, 123 | extendedProps: { 124 | calendar: 'Personal' 125 | } 126 | } 127 | ]; 128 | 129 | export const handlerEvent = [ 130 | // get current user info 131 | http.get('/api/event', async ({ params }) => { 132 | const data = events; 133 | console.log(params); 134 | return HttpResponse.json(data, { status: 200 }); 135 | }), 136 | 137 | http.post('/api/event', async ({ request }) => { 138 | const blankEvent = { 139 | id: '', 140 | title: '', 141 | start: '', 142 | end: '', 143 | allDay: false, 144 | url: '', 145 | extendedProps: { 146 | calendar: '' 147 | } 148 | }; 149 | const body = request.json(); 150 | const event = Object.assign(blankEvent, body); 151 | events.push(event); 152 | return HttpResponse.json(events, { status: 201 }); 153 | }), 154 | 155 | http.put('/api/event/:id', async ({ params }) => { 156 | const event = events.find((item) => item.id === params.id); 157 | return HttpResponse.json(event, { status: 200 }); 158 | }) 159 | ]; 160 | --------------------------------------------------------------------------------