generated from alphane/template
Initial commit
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<router-view v-if="isUnauthenticatedRoute"></router-view>
|
||||
<!--
|
||||
NOTE: current user will always be defined when the authenticated router view loads.
|
||||
-->
|
||||
<template v-else-if="isReady || isErrored">
|
||||
<router-view />
|
||||
</template>
|
||||
<PageLoader
|
||||
v-else-if="isLoadingAuth0 || isLoadingCurrentUser"
|
||||
message="ALPHANE"
|
||||
/>
|
||||
<PageLoader
|
||||
v-else-if="!isReady"
|
||||
message="Loading"
|
||||
/>
|
||||
<PageLoader
|
||||
v-else
|
||||
message="Initializing"
|
||||
/>
|
||||
<AppSnackbar />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onErrorCaptured, onMounted, ref, watch } from "vue"
|
||||
import { useDisplay } from "vuetify"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import ApiError from "@/api/api-error"
|
||||
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useInterface from "@/use/use-interface"
|
||||
|
||||
import PageLoader from "@/components/common/PageLoader.vue"
|
||||
import AppSnackbar from "@/components/common/AppSnackbar.vue"
|
||||
|
||||
const route = useRoute()
|
||||
const isUnauthenticatedRoute = computed(() => route.meta.requiresAuth === false)
|
||||
|
||||
const { isLoading: isLoadingAuth0, isAuthenticated } = useAuth0()
|
||||
const isReadyAuth0 = computed(() => !isLoadingAuth0.value && isAuthenticated.value)
|
||||
|
||||
const { isReady: isReadyCurrentUser, isLoading: isLoadingCurrentUser, fetch } = useCurrentUser()
|
||||
|
||||
const isReady = computed(() => isReadyAuth0.value && isReadyCurrentUser.value)
|
||||
|
||||
const isErrored = ref(false)
|
||||
const router = useRouter()
|
||||
|
||||
const { mdAndDown } = useDisplay()
|
||||
const { setSidebarDrawer, setSidebarMini } = useInterface()
|
||||
|
||||
onMounted(() => {
|
||||
if (mdAndDown.value === false) {
|
||||
setSidebarDrawer(true)
|
||||
setSidebarMini(false)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
[() => isReadyAuth0.value, isUnauthenticatedRoute],
|
||||
async ([newIsReadyAuth0, newIsUnauthenticatedRoute]) => {
|
||||
// Don't bother attempting to load current user for unauthenticated routes
|
||||
if (newIsUnauthenticatedRoute) return
|
||||
|
||||
if (newIsReadyAuth0 === true && !isReadyCurrentUser.value) {
|
||||
try {
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
console.log("Failed to load current user:", error)
|
||||
isErrored.value = true
|
||||
await router.isReady()
|
||||
await router.push({ name: "SignInPage" })
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onErrorCaptured((error: Error) => {
|
||||
if (error instanceof ApiError) {
|
||||
redirectToAppropriateErrorPage(error.status)
|
||||
} else {
|
||||
console.error(`Unhandled error: ${error}`, { error })
|
||||
}
|
||||
})
|
||||
|
||||
function redirectToAppropriateErrorPage(httpErrorCode: number) {
|
||||
if (httpErrorCode === 400 || httpErrorCode === 422) return
|
||||
|
||||
switch (httpErrorCode) {
|
||||
case 401:
|
||||
return router.replace({
|
||||
name: "errors/UnauthorizedPage",
|
||||
})
|
||||
case 403:
|
||||
return router.replace({
|
||||
name: "errors/ForbiddenPage",
|
||||
})
|
||||
case 404:
|
||||
return router.replace({
|
||||
name: "errors/NotFoundPage",
|
||||
})
|
||||
default:
|
||||
return router.replace({
|
||||
name: "errors/InternalServerErrorPage",
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
export class ApiError extends Error {
|
||||
public readonly name = "ApiError"
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiError
|
||||
@@ -0,0 +1,44 @@
|
||||
/** Keep in sync with api/src/controllers/base-controller.ts#ModelOrder */
|
||||
export type ModelOrder =
|
||||
| [string, string]
|
||||
| [string, string, string]
|
||||
| [string, string, string, string]
|
||||
| [string, string, string, string, string]
|
||||
| [string, string, string, string, string, string]
|
||||
|
||||
export type Policy = {
|
||||
show: boolean
|
||||
create: boolean
|
||||
update: boolean
|
||||
destroy: boolean
|
||||
}
|
||||
|
||||
export type WhereOptions<Model, Attributes extends keyof Model> = {
|
||||
[K in Attributes]?: Model[K] | Model[K][]
|
||||
}
|
||||
|
||||
export type FiltersOptions<Options> = Partial<Options>
|
||||
|
||||
export type QueryOptions<WhereOptions, FiltersOptions> = Partial<{
|
||||
where: WhereOptions
|
||||
filters: FiltersOptions
|
||||
order: ModelOrder[]
|
||||
page: number
|
||||
perPage: number
|
||||
}>
|
||||
|
||||
// Keep in sync with api/src/controllers/base-controller.ts
|
||||
export const MAX_PER_PAGE = 1000
|
||||
|
||||
export type ApiResponseError = { field?: string; text: string }
|
||||
|
||||
export type ApiResponseLegacy<T> = {
|
||||
data: T
|
||||
errors: ApiResponseError[]
|
||||
messages?: string[]
|
||||
}
|
||||
|
||||
export type ApiResponse<TPayload = object> = {
|
||||
errors: ApiResponseError[]
|
||||
messages?: string[]
|
||||
} & TPayload
|
||||
@@ -0,0 +1,23 @@
|
||||
import http from "@/api/http-client"
|
||||
|
||||
import { type Policy } from "@/api/base-api"
|
||||
import { UserRoles, type User } from "@/api/users-api"
|
||||
|
||||
export { UserRoles }
|
||||
|
||||
export type UserAsShow = Pick<
|
||||
User,
|
||||
"id" | "email" | "firstName" | "lastName" | "displayName" | "roles" | "createdAt" | "updatedAt"
|
||||
>
|
||||
|
||||
export const currentUserApi = {
|
||||
async get(): Promise<{
|
||||
user: UserAsShow
|
||||
policy: Policy
|
||||
}> {
|
||||
const { data } = await http.get(`/api/current-user`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
|
||||
export default currentUserApi
|
||||
@@ -0,0 +1,46 @@
|
||||
import qs from "qs"
|
||||
import axios from "axios"
|
||||
|
||||
import { API_BASE_URL } from "@/config"
|
||||
import auth0 from "@/plugins/auth0-plugin"
|
||||
import ApiError from "@/api/api-error"
|
||||
|
||||
export const httpClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
paramsSerializer: {
|
||||
serialize: (params) => {
|
||||
return qs.stringify(params, {
|
||||
arrayFormat: "indices",
|
||||
strictNullHandling: true,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
httpClient.interceptors.request.use(async (config) => {
|
||||
// Only add the Authorization header to requests that start with "/api"
|
||||
if (config.url?.startsWith("/api")) {
|
||||
const accessToken = await auth0.getAccessTokenSilently()
|
||||
config.headers["Authorization"] = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
// Any status codes that falls outside the range of 2xx causes this function to trigger
|
||||
httpClient.interceptors.response.use(null, async (error) => {
|
||||
if (error?.error === "login_required") {
|
||||
throw new ApiError("You must be logged in to access this endpoint", 401)
|
||||
} else if (error?.response?.data?.message) {
|
||||
throw new ApiError(error.response.data.message, error.response.status)
|
||||
} else if (error.message) {
|
||||
throw new ApiError(error.message, error.response?.status || 500)
|
||||
} else {
|
||||
throw new ApiError("An unknown error occurred", error.response?.status || 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default httpClient
|
||||
@@ -0,0 +1,18 @@
|
||||
import http from "@/api/http-client"
|
||||
|
||||
export type Status = {
|
||||
RELEASE_TAG: string
|
||||
GIT_COMMIT_HASH: string
|
||||
}
|
||||
|
||||
export const statusApi = {
|
||||
/**
|
||||
* Note: This is a public API route, and not protected by authentication
|
||||
*/
|
||||
async get(): Promise<Status> {
|
||||
const { data } = await http.get("/_status")
|
||||
return data
|
||||
},
|
||||
}
|
||||
|
||||
export default statusApi
|
||||
@@ -0,0 +1,81 @@
|
||||
import http from "@/api/http-client"
|
||||
import {
|
||||
type FiltersOptions,
|
||||
type ModelOrder,
|
||||
type Policy,
|
||||
type WhereOptions,
|
||||
} from "@/api/base-api"
|
||||
|
||||
/** Keep in sync with api/src/models/user.ts */
|
||||
export enum UserRoles {
|
||||
SYSTEM_ADMIN = "system_admin",
|
||||
USER = "user",
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
displayName: string
|
||||
roles: UserRoles[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type UserAsShow = Omit<User, "organizations"> & {}
|
||||
|
||||
export type UserWhereOptions = WhereOptions<User, "email" | "firstName" | "lastName">
|
||||
|
||||
export type UserFiltersOptions = FiltersOptions<{
|
||||
search: string | string[]
|
||||
}>
|
||||
|
||||
export type UserQueryOptions = {
|
||||
where?: UserWhereOptions
|
||||
filters?: UserFiltersOptions
|
||||
order?: ModelOrder[]
|
||||
page?: number
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
export const usersApi = {
|
||||
UserRoles,
|
||||
async list(params: UserQueryOptions = {}): Promise<{
|
||||
users: User[]
|
||||
totalCount: number
|
||||
}> {
|
||||
const { data } = await http.get("/api/users", {
|
||||
params,
|
||||
})
|
||||
return data
|
||||
},
|
||||
async get(userId: number): Promise<{
|
||||
user: User
|
||||
policy: Policy
|
||||
}> {
|
||||
const { data } = await http.get(`/api/users/${userId}`)
|
||||
return data
|
||||
},
|
||||
async create(attributes: Partial<User>): Promise<{
|
||||
user: User
|
||||
}> {
|
||||
const { data } = await http.post("/api/users", attributes)
|
||||
return data
|
||||
},
|
||||
async update(
|
||||
userId: number,
|
||||
attributes: Partial<User>
|
||||
): Promise<{
|
||||
user: User
|
||||
}> {
|
||||
const { data } = await http.patch(`/api/users/${userId}`, attributes)
|
||||
return data
|
||||
},
|
||||
async delete(userId: number): Promise<void> {
|
||||
const { data } = await http.delete(`/api/users/${userId}`)
|
||||
return data
|
||||
},
|
||||
}
|
||||
|
||||
export default usersApi
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,55 @@
|
||||
<!-- Special module scope required for exports -->
|
||||
<script lang="ts">
|
||||
import { RouteLocationRaw } from "vue-router"
|
||||
|
||||
type Breadcrumb = {
|
||||
title: string
|
||||
to: RouteLocationRaw
|
||||
}
|
||||
|
||||
export { type Breadcrumb }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
breadcrumbs: Breadcrumb[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-3 mb-6">
|
||||
<div class="d-flex justify-space-between">
|
||||
<div class="d-flex py-0 align-center">
|
||||
<div>
|
||||
<h2 class="text-h3 mb-2">{{ props.title }}</h2>
|
||||
<v-breadcrumbs
|
||||
:items="props.breadcrumbs"
|
||||
class="text-h6 font-weight-regular pa-0 ml-n1"
|
||||
>
|
||||
<template
|
||||
v-if="props.breadcrumbs"
|
||||
#divider
|
||||
>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
<template #title="{ item }">
|
||||
<h6 class="text-medium-emphasis text-subtitle-1">{{ item.title }}</h6>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<slot name="append" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.page-breadcrumb {
|
||||
.v-toolbar {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="mb-5 app-card"
|
||||
:to="to"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-if="mainColor && !isNil(progressValue)"
|
||||
:model-value="progressValue"
|
||||
:color="mainColor"
|
||||
height="16"
|
||||
>
|
||||
<span style="font-size: 12px"> {{ progressValue }}% Complete</span>
|
||||
</v-progress-linear>
|
||||
<v-card-title
|
||||
v-if="title"
|
||||
class="d-flex"
|
||||
:class="{ 'mb-n3': subtitle }"
|
||||
>
|
||||
{{ title }}
|
||||
<v-spacer />
|
||||
<slot name="append-title"></slot>
|
||||
</v-card-title>
|
||||
<v-card-subtitle
|
||||
v-if="subtitle"
|
||||
:class="{ 'mt-3': !title }"
|
||||
style="font-weight: 500 !important"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text :class="{ 'mt-n5': subtitle }">
|
||||
<slot></slot>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isNil } from "lodash"
|
||||
|
||||
defineProps<{
|
||||
title?: string
|
||||
subtitle?: string
|
||||
to?: string | { name: string; params?: Record<string, string | number> }
|
||||
mainColor?: string
|
||||
progressValue?: number
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<RouterLink
|
||||
to="/dashboard"
|
||||
class="d-flex"
|
||||
>
|
||||
<img
|
||||
class="ml-0 mt-1"
|
||||
style="height: 36px; transform: rotate(-12deg)"
|
||||
:src="AppLogo"
|
||||
/>
|
||||
<div v-if="sidebarMini || mdAndDown"></div>
|
||||
<div
|
||||
v-else
|
||||
class="d-flex"
|
||||
style="width: 200px"
|
||||
>
|
||||
<div
|
||||
class="mt-1 ml-3 rotyr-font"
|
||||
style="font-size: 26px; color: #505682"
|
||||
>
|
||||
ALPHANE
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppLogo from "@/assets/app_logo_small.png"
|
||||
|
||||
import { useDisplay } from "vuetify"
|
||||
import useInterface from "@/use/use-interface"
|
||||
import { watch } from "vue"
|
||||
|
||||
const { sidebarMini, setSidebarMini } = useInterface()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
watch(mdAndDown, (newVal) => {
|
||||
if (newVal === true) setSidebarMini(false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.logo a {
|
||||
text-decoration: none !important;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="showSnackbar"
|
||||
v-bind="{
|
||||
multiLine: true,
|
||||
timeout: defaultTimeout,
|
||||
...options,
|
||||
}"
|
||||
>
|
||||
<span :class="`text-${constrastingColor}`">
|
||||
{{ message }}
|
||||
</span>
|
||||
<template #actions>
|
||||
<v-btn
|
||||
:color="constrastingColor"
|
||||
variant="text"
|
||||
@click="close"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue"
|
||||
|
||||
import { useSnack } from "@/use/use-snack"
|
||||
import { isEmpty } from "lodash"
|
||||
|
||||
const { message, options, reset } = useSnack()
|
||||
|
||||
const showSnackbar = ref(false)
|
||||
const defaultTimeout = 4000
|
||||
|
||||
const constrastingColor = computed(() => getContrastingColor(options.value.color))
|
||||
|
||||
watch(
|
||||
() => [message.value, options.value],
|
||||
() => {
|
||||
if (isEmpty(message.value)) return
|
||||
|
||||
show()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => showSnackbar.value,
|
||||
(newShowSnackbar) => {
|
||||
if (newShowSnackbar === false) {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
showSnackbar.value = false
|
||||
}
|
||||
|
||||
function show() {
|
||||
showSnackbar.value = true
|
||||
}
|
||||
|
||||
function getContrastingColor(color: string | undefined) {
|
||||
if (color === undefined) return "white"
|
||||
|
||||
const colorMap: {
|
||||
[key: string]: "white" | "black" | undefined
|
||||
} = {
|
||||
primary: "white",
|
||||
secondary: "black",
|
||||
accent: "black",
|
||||
error: "white",
|
||||
info: "white",
|
||||
success: "white",
|
||||
warning: "black",
|
||||
}
|
||||
|
||||
return colorMap[color] || "white"
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useSlots } from "vue"
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
|
||||
const collapsed = ref(false)
|
||||
const slots = useSlots()
|
||||
|
||||
function toggleCollapse() {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mb-5 savable-card">
|
||||
<v-card-text>
|
||||
<div class="d-flex">
|
||||
<div
|
||||
class="d-flex cursor-pointer"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<v-icon
|
||||
:icon="collapsed ? 'mdi-chevron-down' : 'mdi-chevron-up'"
|
||||
color="primary"
|
||||
class="mr-4 mt-1"
|
||||
/>
|
||||
|
||||
<h2 class="text-h5 title mb-1">{{ props.title }}</h2>
|
||||
</div>
|
||||
<v-spacer v-if="slots.rightpart && !collapsed" />
|
||||
<div
|
||||
v-if="slots.rightpart && !collapsed"
|
||||
class="float-right"
|
||||
>
|
||||
<!---Toggle Button For mobile-->
|
||||
<slot name="rightpart"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-slide-y-transition>
|
||||
<div v-show="!collapsed">
|
||||
<v-divider class="my-3" />
|
||||
<slot></slot>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="internalDialog"
|
||||
max-width="500"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<slot>{{ message }}</slot>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:color="confirmColor"
|
||||
variant="flat"
|
||||
@click="confirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
message?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
confirmColor?: string
|
||||
}>(),
|
||||
{
|
||||
title: "Confirm",
|
||||
message: "Are you sure?",
|
||||
confirmText: "Confirm",
|
||||
cancelText: "Cancel",
|
||||
confirmColor: "primary",
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean]
|
||||
confirm: []
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const internalDialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit("update:modelValue", value),
|
||||
})
|
||||
|
||||
function confirm() {
|
||||
emit("confirm")
|
||||
internalDialog.value = false
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit("cancel")
|
||||
internalDialog.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<!-- Vertical layout -->
|
||||
<div v-if="vertical">
|
||||
<dt class="d-flex align-center font-weight-medium">
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
size="18"
|
||||
class="mr-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<span>{{ label }}</span>
|
||||
</dt>
|
||||
<dd class="mt-2">
|
||||
<v-progress-circular
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
size="16"
|
||||
width="1"
|
||||
/>
|
||||
<slot>{{ modelValue }}</slot>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal layout -->
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-center gap-2"
|
||||
>
|
||||
<dt class="d-flex align-center font-weight-medium">
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
size="18"
|
||||
class="mr-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
<span>{{ label }}:</span>
|
||||
</dt>
|
||||
<dd>
|
||||
<v-progress-circular
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
size="16"
|
||||
width="1"
|
||||
/>
|
||||
<slot>{{ modelValue }}</slot>
|
||||
</dd>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string
|
||||
icon?: string
|
||||
modelValue?: string | number | boolean | null | undefined
|
||||
vertical?: boolean
|
||||
loading?: boolean
|
||||
}>(),
|
||||
{
|
||||
icon: "",
|
||||
modelValue: null,
|
||||
vertical: false,
|
||||
loading: false,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gap-2 {
|
||||
gap: 0.5rem; /* 8px */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<v-card :elevation="elevation">
|
||||
<HeaderActionsCardBody
|
||||
:title="title"
|
||||
:header-tag="headerTag"
|
||||
:header-class="headerClass"
|
||||
:divider-class="dividerClass"
|
||||
:hide-body="hideBody"
|
||||
:body-class="bodyClass"
|
||||
:header-icon="headerIcon"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions"></slot>
|
||||
</template>
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template
|
||||
v-if="$slots.actions"
|
||||
#actions
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</template>
|
||||
<!--
|
||||
TODO: replace current #actions slot with #actions-next code.
|
||||
-->
|
||||
<template
|
||||
v-if="$slots['actions-next']"
|
||||
#actions-next
|
||||
>
|
||||
<slot name="actions-next"></slot>
|
||||
</template>
|
||||
</HeaderActionsCardBody>
|
||||
<slot name="content"></slot>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type VCard } from "vuetify/components"
|
||||
|
||||
import { type VueHtmlClass } from "@/utils/utility-types"
|
||||
|
||||
import HeaderActionsCardBody from "@/components/common/HeaderActionsCardBody.vue"
|
||||
|
||||
/**
|
||||
* Keep in sync with web/src/components/common/HeaderActionsCardBody.vue
|
||||
*/
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string
|
||||
headerTag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p"
|
||||
headerClass?: VueHtmlClass
|
||||
dividerClass?: VueHtmlClass
|
||||
elevation?: VCard["elevation"]
|
||||
hideBody?: boolean
|
||||
bodyClass?: VueHtmlClass
|
||||
headerIcon?: string
|
||||
}>(),
|
||||
{
|
||||
title: "",
|
||||
headerTag: "h3",
|
||||
headerClass: "text-h5",
|
||||
dividerClass: "mb-3",
|
||||
elevation: 0,
|
||||
hideBody: false,
|
||||
bodyClass: "",
|
||||
headerIcon: "",
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<v-card-title class="d-flex flex-sm-row justify-sm-space-between align-sm-end">
|
||||
<slot name="header">
|
||||
<component
|
||||
:is="headerTag"
|
||||
v-if="!isEmpty(title)"
|
||||
:class="headerClass"
|
||||
class="py-2"
|
||||
>
|
||||
<v-icon
|
||||
v-if="headerIcon"
|
||||
class="mr-2"
|
||||
>{{ headerIcon }}</v-icon
|
||||
>
|
||||
{{ title }}
|
||||
</component>
|
||||
</slot>
|
||||
<v-spacer class="mt-4 mt-md-0" />
|
||||
<slot name="header-actions"></slot>
|
||||
</v-card-title>
|
||||
<v-divider
|
||||
v-if="!isEmpty(title)"
|
||||
:class="dividerClass"
|
||||
/>
|
||||
<v-card-text
|
||||
v-if="!hideBody"
|
||||
:class="bodyClass"
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
<div
|
||||
v-if="$slots.actions"
|
||||
class="mt-4"
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<!--
|
||||
TODO: replace current #actions slot with #actions-next code.
|
||||
-->
|
||||
<v-card-actions
|
||||
v-if="$slots['actions-next']"
|
||||
class="d-flex flex-column flex-md-row"
|
||||
>
|
||||
<slot name="actions-next"></slot>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isEmpty } from "lodash"
|
||||
|
||||
import { type VueHtmlClass } from "@/utils/utility-types"
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string
|
||||
headerTag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p"
|
||||
headerClass?: VueHtmlClass
|
||||
dividerClass?: VueHtmlClass
|
||||
hideBody?: boolean
|
||||
bodyClass?: VueHtmlClass
|
||||
headerIcon?: string
|
||||
}>(),
|
||||
{
|
||||
title: "",
|
||||
headerTag: "h3",
|
||||
headerClass: "text-h5",
|
||||
dividerClass: "mb-3",
|
||||
hideBody: false,
|
||||
bodyClass: "",
|
||||
headerIcon: "",
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<v-card :elevation="elevation">
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="isValid"
|
||||
@submit="emit('submit', $event)"
|
||||
>
|
||||
<HeaderActionsCardBody
|
||||
:title="title"
|
||||
:header-tag="headerTag"
|
||||
:header-class="headerClass"
|
||||
:divider-class="dividerClass"
|
||||
:body-class="bodyClass"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions"></slot>
|
||||
</template>
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
</template>
|
||||
<template
|
||||
v-if="$slots.actions"
|
||||
#actions
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</template>
|
||||
<!--
|
||||
TODO: replace current #actions slot with #actions-next code.
|
||||
-->
|
||||
<template
|
||||
v-if="$slots['actions-next']"
|
||||
#actions-next
|
||||
>
|
||||
<slot name="actions-next"></slot>
|
||||
</template>
|
||||
</HeaderActionsCardBody>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { isNil } from "lodash"
|
||||
|
||||
import { type VCard, type VForm } from "vuetify/components"
|
||||
import { type SubmitEventPromise } from "vuetify/lib/composables/form"
|
||||
|
||||
import { type VueHtmlClass } from "@/utils/utility-types"
|
||||
|
||||
import HeaderActionsCardBody from "@/components/common/HeaderActionsCardBody.vue"
|
||||
|
||||
const isValid = defineModel<boolean | null>({ default: null })
|
||||
|
||||
/**
|
||||
* Keep in sync with web/src/components/common/HeaderActionsCardBody.vue
|
||||
*/
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string
|
||||
headerTag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
|
||||
headerClass?: VueHtmlClass
|
||||
dividerClass?: VueHtmlClass
|
||||
bodyClass?: VueHtmlClass
|
||||
elevation?: VCard["elevation"]
|
||||
}>(),
|
||||
{
|
||||
title: "",
|
||||
headerTag: "h3",
|
||||
headerClass: "text-h5",
|
||||
dividerClass: "mb-3",
|
||||
bodyClass: "",
|
||||
elevation: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit", value: SubmitEventPromise): void
|
||||
}>()
|
||||
|
||||
const form = ref<InstanceType<typeof VForm> | null>(null)
|
||||
|
||||
async function validate() {
|
||||
if (isNil(form.value)) throw new Error("form component not loaded")
|
||||
|
||||
return form.value?.validate()
|
||||
}
|
||||
|
||||
async function resetValidation() {
|
||||
if (isNil(form.value)) throw new Error("form component not loaded")
|
||||
|
||||
return form.value?.resetValidation()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate,
|
||||
resetValidation,
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="d-flex justify-center align-center h-screen">
|
||||
<div class="d-flex flex-column align-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="64"
|
||||
class="mb-5"
|
||||
color="#505682"
|
||||
/>
|
||||
<h1 class="text-center">{{ message }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
withDefaults(defineProps<{ message: string }>(), { message: "ALPAHNE" })
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<v-text-field
|
||||
:model-value="modelValue"
|
||||
:label="label"
|
||||
:rules="combinedRules"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:clearable="clearable"
|
||||
:hint="hint"
|
||||
:persistent-hint="persistentHint"
|
||||
type="number"
|
||||
suffix="%"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.01"
|
||||
@update:model-value="updateModelValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { VTextField } from "vuetify/components"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number | null | undefined
|
||||
label?: string
|
||||
rules?: VTextField["rules"]
|
||||
disabled?: boolean
|
||||
readonly?: boolean
|
||||
clearable?: boolean
|
||||
hint?: string
|
||||
persistentHint?: boolean
|
||||
}>(),
|
||||
{
|
||||
label: "Percentage",
|
||||
rules: () => [],
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
clearable: false,
|
||||
persistentHint: false,
|
||||
hint: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: number | null]
|
||||
}>()
|
||||
|
||||
const combinedRules = computed(() => {
|
||||
const baseRules = [
|
||||
(value: number | null | undefined) => {
|
||||
if (value === null || value === undefined) return true
|
||||
return true
|
||||
},
|
||||
(value: number | null | undefined) => {
|
||||
if (value === null || value === undefined) return true
|
||||
const numValue = Number(value)
|
||||
if (isNaN(numValue)) return "Must be a valid number"
|
||||
if (numValue < 0) return "Percentage must be at least 0"
|
||||
if (numValue > 100) return "Percentage must be at most 100"
|
||||
return true
|
||||
},
|
||||
]
|
||||
|
||||
return [...baseRules, ...(props.rules || [])]
|
||||
})
|
||||
|
||||
function updateModelValue(value: string | number | null) {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
emit("update:modelValue", null)
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = Number(value)
|
||||
if (isNaN(numValue)) {
|
||||
emit("update:modelValue", null)
|
||||
return
|
||||
}
|
||||
|
||||
// Clamp value between 0 and 100
|
||||
const clampedValue = Math.min(Math.max(numValue, 0), 100)
|
||||
emit("update:modelValue", clampedValue)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="internalDialog"
|
||||
:max-width="mobile ? undefined : maxWidth"
|
||||
:fullscreen="mobile"
|
||||
:persistent="persistent"
|
||||
scrollable
|
||||
>
|
||||
<HeaderActionsCard>
|
||||
<template #header>
|
||||
<div
|
||||
class="d-flex align-start ga-2 w-100"
|
||||
:class="{ 'pl-4': mobile, 'pt-2': mobile }"
|
||||
style="white-space: normal"
|
||||
>
|
||||
<slot name="title">
|
||||
<h3
|
||||
class="text-h5 py-2 flex-grow-1 text-break"
|
||||
style="min-width: 0; overflow-wrap: anywhere; white-space: normal"
|
||||
>
|
||||
<v-icon
|
||||
v-if="titleIcon"
|
||||
class="mr-2"
|
||||
>{{ titleIcon }}</v-icon
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
</slot>
|
||||
<v-btn
|
||||
v-if="!hideClose"
|
||||
icon="mdi-close"
|
||||
:variant="mobile ? 'tonal' : 'text'"
|
||||
density="comfortable"
|
||||
class="flex-shrink-0"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</HeaderActionsCard>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useDisplay } from "vuetify"
|
||||
|
||||
import HeaderActionsCard from "@/components/shared/cards/HeaderActionsCard.vue"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
titleIcon?: string
|
||||
maxWidth?: number | string
|
||||
persistent?: boolean
|
||||
hideClose?: boolean
|
||||
}>(),
|
||||
{
|
||||
title: "",
|
||||
titleIcon: "",
|
||||
maxWidth: 700,
|
||||
persistent: false,
|
||||
hideClose: false,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean]
|
||||
}>()
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
|
||||
const internalDialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
})
|
||||
|
||||
function close() {
|
||||
internalDialog.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<v-date-input
|
||||
v-model="date"
|
||||
v-bind="$attrs"
|
||||
@update:model-value="emitStringResult"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { DateTime } from "luxon"
|
||||
import { isNil } from "lodash"
|
||||
|
||||
/**
|
||||
* A date input accepts and returns a string.
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string | null | undefined
|
||||
returnFormat?: string
|
||||
}>(),
|
||||
{
|
||||
returnFormat: "yyyy-MM-dd",
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string | null]
|
||||
}>()
|
||||
|
||||
const date = ref<Date | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (isNil(newValue)) {
|
||||
date.value = null
|
||||
} else {
|
||||
const dateTime = DateTime.fromISO(newValue)
|
||||
if (dateTime.isValid) {
|
||||
date.value = dateTime.toJSDate()
|
||||
} else {
|
||||
date.value = null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* NOTE: v-date-input returns Date | string, rather than just "string" like it's types imply.
|
||||
*/
|
||||
function emitStringResult(value: unknown) {
|
||||
if (value instanceof Date) {
|
||||
const dateTime = DateTime.fromJSDate(value)
|
||||
if (dateTime.isValid) {
|
||||
const stringValue = dateTime.toFormat(props.returnFormat)
|
||||
emit("update:modelValue", stringValue)
|
||||
} else {
|
||||
emit("update:modelValue", null)
|
||||
}
|
||||
} else if (typeof value === "string") {
|
||||
emit("update:modelValue", value)
|
||||
} else {
|
||||
emit("update:modelValue", null)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<v-app-bar
|
||||
id="top"
|
||||
elevation="10"
|
||||
height="60"
|
||||
class="main-head pl-2"
|
||||
>
|
||||
<div class="mr-3">
|
||||
<AppLogo />
|
||||
</div>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-btn
|
||||
v-if="isSystemAdmin"
|
||||
icon
|
||||
variant="text"
|
||||
class="custom-hover-primary mr-2"
|
||||
size="small"
|
||||
title="AI Assistant"
|
||||
@click="$emit('toggle-ai-panel')"
|
||||
>
|
||||
<v-icon size="28">mdi-robot-outline</v-icon>
|
||||
</v-btn>
|
||||
<ProfileMenu />
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
import AppLogo from "@/components/common/AppLogo.vue"
|
||||
import ProfileMenu from "@/components/layout/ProfileMenu.vue"
|
||||
|
||||
defineEmits<{
|
||||
"toggle-ai-panel": []
|
||||
}>()
|
||||
|
||||
const { isSystemAdmin } = useCurrentUser<true>()
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="sidebarDrawer"
|
||||
left
|
||||
elevation="0"
|
||||
rail-width="75"
|
||||
app
|
||||
class="leftSidebar"
|
||||
:rail="sidebarMini"
|
||||
expand-on-hover
|
||||
width="256"
|
||||
>
|
||||
<div class="pa-2">
|
||||
<v-card
|
||||
v-if="currentUser"
|
||||
variant="outlined"
|
||||
style="overflow: hidden"
|
||||
>
|
||||
<v-card-text
|
||||
v-if="!sidebarMini"
|
||||
class="pa-2"
|
||||
>
|
||||
<div class="font-weight-bold">{{ currentUser.displayName }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<v-list class="d-none py-3 px-4">
|
||||
<v-list-subheader
|
||||
class="text-uppercase text-13 font-weight-semibold textPrimary d-flex align-items-center"
|
||||
>
|
||||
<span class="mini-icon">
|
||||
<IconDotsCircleHorizontal
|
||||
size="16"
|
||||
stroke-width="1.5"
|
||||
class="iconClass"
|
||||
/>
|
||||
</span>
|
||||
<span class="mini-text">Aircraft</span>
|
||||
</v-list-subheader>
|
||||
|
||||
<v-list-item
|
||||
v-scroll-to="{ el: '#top' }"
|
||||
:to="{}"
|
||||
rounded="pill"
|
||||
class="mb-1 px-3"
|
||||
>
|
||||
<template #prepend>
|
||||
<IconPointFilled
|
||||
size="24"
|
||||
class="dot mini-icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<span class="mini-text"> Y-CCAA</span>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IconDotsCircleHorizontal, IconPointFilled } from "@tabler/icons-vue"
|
||||
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useInterface from "@/use/use-interface"
|
||||
|
||||
const { sidebarDrawer, sidebarMini } = useInterface()
|
||||
|
||||
const { currentUser } = useCurrentUser()
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-breadcrumbs
|
||||
class="flex-wrap"
|
||||
:items="breadcrumbsWithExactTrueByDefault"
|
||||
color=""
|
||||
>
|
||||
<template #title="{ item }">
|
||||
<transition
|
||||
name="breadcrumb-title-fade"
|
||||
mode="out-in"
|
||||
>
|
||||
<v-progress-circular
|
||||
v-if="isEmpty(item.title)"
|
||||
key="title-loader"
|
||||
size="16"
|
||||
color="secondary"
|
||||
width="1"
|
||||
indeterminate
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
key="title-text"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</transition>
|
||||
</template>
|
||||
<template #divider>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export { type Breadcrumb } from "@/use/use-breadcrumbs"
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { isEmpty } from "lodash"
|
||||
|
||||
import { type Breadcrumb } from "@/use/use-breadcrumbs"
|
||||
|
||||
const props = defineProps<{
|
||||
items: Breadcrumb[]
|
||||
}>()
|
||||
|
||||
// Changes https://vuetifyjs.com/en/components/breadcrumbs/ default behavior.
|
||||
// By default v-breadcrumbs will disable all crumbs up to the current page in a nested paths.
|
||||
// You can prevent this behavior by using exact: true on each applicable breadcrumb in the items array.
|
||||
const breadcrumbsWithExactTrueByDefault = computed(() =>
|
||||
props.items.map((item) => ({
|
||||
...item,
|
||||
title: item.title ?? "",
|
||||
exact: item.exact ?? true,
|
||||
}))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.breadcrumb-title-fade-enter-active,
|
||||
.breadcrumb-title-fade-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
.breadcrumb-title-fade-enter-from,
|
||||
.breadcrumb-title-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<v-menu
|
||||
v-model="showMenu"
|
||||
:close-on-content-click="false"
|
||||
class="profile_popup"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
variant="tonal"
|
||||
v-bind="props"
|
||||
icon
|
||||
:text="currentUserInitials"
|
||||
@click="showMenu = true"
|
||||
/>
|
||||
</template>
|
||||
<v-sheet
|
||||
rounded="md"
|
||||
width="360"
|
||||
elevation="11"
|
||||
>
|
||||
<div class="px-8 pt-3">
|
||||
<div class="d-flex align-center mt-4 pb-6">
|
||||
<div>
|
||||
<h6 class="text-h6 mb-n1">{{ currentUser.displayName }}</h6>
|
||||
<div class="d-flex align-center mt-2">
|
||||
<IconMail
|
||||
:size="18"
|
||||
:stroke-width="1.5"
|
||||
/>
|
||||
|
||||
<span class="text-subtitle-1 font-weight-regular textSecondary ml-2">{{
|
||||
currentUser.email
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
|
||||
<v-list
|
||||
class="py-0 theme-list"
|
||||
lines="two"
|
||||
>
|
||||
<v-list-item
|
||||
class="py-4 px-8 custom-text-primary"
|
||||
:to="{
|
||||
name: 'ProfilePage',
|
||||
}"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar color="info">
|
||||
<IconUserCircle></IconUserCircle>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div>
|
||||
<h6 class="text-subtitle-1 font-weight-semibold mb-2 custom-title">My Profile</h6>
|
||||
</div>
|
||||
<p class="text-subtitle-1 font-weight-regular textSecondary">Manage your information</p>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="isSystemAdmin"
|
||||
class="py-4 px-8 custom-text-primary"
|
||||
:to="{
|
||||
name: 'administration/AdministrationDashboardPage',
|
||||
}"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar color="warning">
|
||||
<IconSettings></IconSettings>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div>
|
||||
<h6 class="text-subtitle-1 font-weight-semibold mb-2 custom-title">
|
||||
ROTYR System Administration
|
||||
</h6>
|
||||
</div>
|
||||
<p class="text-subtitle-1 font-weight-regular textSecondary">Manage this application</p>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="isSystemAdmin"
|
||||
class="py-4 px-8 custom-text-primary"
|
||||
:to="{
|
||||
name: 'StatusPage',
|
||||
}"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar color="secondary">
|
||||
<IconClock />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div>
|
||||
<h6 class="text-subtitle-1 font-weight-semibold mb-2 custom-title">Version</h6>
|
||||
</div>
|
||||
<p class="text-subtitle-1 font-weight-regular textSecondary">
|
||||
{{ releaseTag || "2024.08.29" }}
|
||||
</p>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div class="pt-4 pb-6 px-8 text-center">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
block
|
||||
@click="signOut"
|
||||
>Logout</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { IconClock, IconMail, IconSettings, IconUserCircle } from "@tabler/icons-vue"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
import { isEmpty, isNil } from "lodash"
|
||||
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useStatus from "@/use/use-status"
|
||||
|
||||
const { logout } = useAuth0()
|
||||
const { currentUser, isSystemAdmin, reset: resetCurrentUser } = useCurrentUser<true>()
|
||||
const showMenu = ref(false)
|
||||
|
||||
const { releaseTag } = useStatus()
|
||||
|
||||
const currentUserInitials = computed(() => {
|
||||
if (isNil(currentUser.value)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const { firstName, lastName, email } = currentUser.value
|
||||
if (!isNil(firstName) && !isEmpty(firstName) && !isNil(lastName) && !isEmpty(lastName)) {
|
||||
const initials = [firstName, lastName].map((name) => name[0]?.toUpperCase()).join("")
|
||||
return initials
|
||||
}
|
||||
|
||||
const initials = email
|
||||
.split(".")
|
||||
.map((part) => part[0]?.toUpperCase())
|
||||
.join("")
|
||||
return initials
|
||||
})
|
||||
|
||||
function closeMenu() {
|
||||
showMenu.value = false
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
resetCurrentUser()
|
||||
|
||||
const returnTo = encodeURI(window.location.origin)
|
||||
return logout({ logoutParams: { returnTo } })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<v-menu
|
||||
v-model="showMenu"
|
||||
:close-on-content-click="false"
|
||||
class="search_popup"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="custom-hover-primary mr-2"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
>
|
||||
<IconSearch :size="26" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet
|
||||
width="360"
|
||||
elevation="11"
|
||||
rounded="md"
|
||||
>
|
||||
<v-form class="d-flex flex-column pa-5">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
placeholder="Search"
|
||||
color="primary"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
@click:clear="search = ''"
|
||||
/>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
color="primary"
|
||||
type="submit"
|
||||
block
|
||||
>
|
||||
<v-icon start>mdi-magnify</v-icon> Search
|
||||
</v-btn>
|
||||
</v-form>
|
||||
|
||||
<v-divider />
|
||||
<h5 class="text-h5 mt-4 px-5 pb-4">Recently Viewed</h5>
|
||||
<v-divider />
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
import { IconSearch } from "@tabler/icons-vue"
|
||||
|
||||
const showMenu = ref(false)
|
||||
|
||||
const search = ref("")
|
||||
</script>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<v-skeleton-loader
|
||||
v-if="isNil(user)"
|
||||
type="card"
|
||||
/>
|
||||
<HeaderActionsFormCard
|
||||
v-else
|
||||
ref="headerActionsFormCard"
|
||||
title="User Details"
|
||||
elevation="10"
|
||||
@submit.prevent="saveWrapper"
|
||||
>
|
||||
<template #header-actions> </template>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-label class="mb-2">First name *</v-label>
|
||||
<v-text-field
|
||||
v-model="user.firstName"
|
||||
hide-details="auto"
|
||||
:rules="[required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-label class="mb-2">Last name *</v-label>
|
||||
<v-text-field
|
||||
v-model="user.lastName"
|
||||
hide-details="auto"
|
||||
:rules="[required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-label class="mb-2">Display name *</v-label>
|
||||
<v-text-field
|
||||
v-model="user.displayName"
|
||||
hide-details="auto"
|
||||
:rules="[required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-label class="mb-2">Email</v-label>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
type="email"
|
||||
hide-details="auto"
|
||||
:rules="[required, email]"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-label class="mb-2">Roles</v-label>
|
||||
<UserRoleSelect
|
||||
v-model="user.roles"
|
||||
hide-details="auto"
|
||||
:rules="[required]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex mt-8">
|
||||
<v-btn
|
||||
:loading="isLoading"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save User
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="warning"
|
||||
class="ml-4"
|
||||
variant="outlined"
|
||||
:loading="isLoading"
|
||||
:to="{ name: 'administration/UsersPage' }"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="canDelete"
|
||||
color="error"
|
||||
class="ml-4"
|
||||
variant="outlined"
|
||||
:loading="isDeleting"
|
||||
@click="showDeleteDialog = true"
|
||||
>
|
||||
Delete
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</HeaderActionsFormCard>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model="showDeleteDialog"
|
||||
title="Delete user?"
|
||||
:message="`Permanently delete ${user?.displayName || user?.email}? This removes the user from all organizations and roles and cannot be undone.`"
|
||||
confirm-text="Delete"
|
||||
confirm-color="error"
|
||||
@confirm="handleDelete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, toRefs } from "vue"
|
||||
import { RouteLocationRaw, useRouter } from "vue-router"
|
||||
import { isNil } from "lodash"
|
||||
|
||||
import { email, required } from "@/utils/validators"
|
||||
|
||||
import usersApi from "@/api/users-api"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useSnack from "@/use/use-snack"
|
||||
import useUser from "@/use/use-user"
|
||||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog.vue"
|
||||
import HeaderActionsFormCard from "@/components/common/HeaderActionsFormCard.vue"
|
||||
import UserRoleSelect from "@/components/users/UserRoleSelect.vue"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
userId: number
|
||||
returnTo?: RouteLocationRaw
|
||||
}>(),
|
||||
{
|
||||
returnTo: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
saved: [userId: number]
|
||||
}>()
|
||||
|
||||
const { userId } = toRefs(props)
|
||||
const { user, policy, isLoading, save } = useUser(userId)
|
||||
|
||||
const { currentUser } = useCurrentUser()
|
||||
const router = useRouter()
|
||||
const headerActionsFormCard = ref<InstanceType<typeof HeaderActionsFormCard> | null>(null)
|
||||
const snack = useSnack()
|
||||
|
||||
const showDeleteDialog = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
|
||||
const canDelete = computed(() => {
|
||||
if (!policy.value?.destroy) return false
|
||||
if (currentUser.value?.id === userId.value) return false
|
||||
return true
|
||||
})
|
||||
|
||||
async function handleDelete() {
|
||||
if (isNil(user.value)) return
|
||||
|
||||
isDeleting.value = true
|
||||
try {
|
||||
await usersApi.delete(user.value.id)
|
||||
snack.success("User deleted.")
|
||||
await router.push({ name: "administration/UsersPage" })
|
||||
} catch (error) {
|
||||
snack.error(`Failed to delete user: ${error}`)
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWrapper() {
|
||||
if (isNil(user.value)) return
|
||||
if (headerActionsFormCard.value === null) return
|
||||
|
||||
const { valid } = await headerActionsFormCard.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
try {
|
||||
await save()
|
||||
snack.success("User saved!")
|
||||
emit("saved", user.value.id)
|
||||
} catch (error) {
|
||||
snack.error(`Failed to save user: ${error}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<v-skeleton-loader
|
||||
v-if="isNil(user)"
|
||||
type="card"
|
||||
/>
|
||||
<HeaderActionsCard
|
||||
v-else
|
||||
title="User Details"
|
||||
elevation="10"
|
||||
>
|
||||
<template #header-actions>
|
||||
<v-btn
|
||||
:to="{
|
||||
name: 'profile/ProfileEditPage',
|
||||
}"
|
||||
color="primary"
|
||||
>
|
||||
Edit
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<DescriptionElement
|
||||
label="First name"
|
||||
:model-value="user.firstName || '<blank>'"
|
||||
vertical
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<DescriptionElement
|
||||
label="Last name"
|
||||
:model-value="user.lastName || '<blank>'"
|
||||
vertical
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<DescriptionElement
|
||||
label="Email"
|
||||
:model-value="user.email"
|
||||
vertical
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<DescriptionElement
|
||||
label="Display name"
|
||||
:model-value="user.displayName || '<blank>'"
|
||||
vertical
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
</HeaderActionsCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from "vue"
|
||||
import { isNil } from "lodash"
|
||||
|
||||
import useUser from "@/use/use-user"
|
||||
|
||||
import HeaderActionsCard from "@/components/common/HeaderActionsCard.vue"
|
||||
import DescriptionElement from "@/components/common/DescriptionElement.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
userId: number
|
||||
}>()
|
||||
|
||||
const { userId } = toRefs(props)
|
||||
const { user } = useUser(userId)
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<v-select
|
||||
v-model="selectedRoles"
|
||||
:items="roleItems"
|
||||
label="Roles"
|
||||
chips
|
||||
multiple
|
||||
closable-chips
|
||||
v-bind="$attrs"
|
||||
></v-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n"
|
||||
|
||||
import { UserRoles } from "@/api/users-api"
|
||||
|
||||
const selectedRoles = defineModel<UserRoles[]>({
|
||||
default: [],
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const ORDERED_ROLES = [UserRoles.USER, UserRoles.SYSTEM_ADMIN]
|
||||
|
||||
const roleItems = Object.values(ORDERED_ROLES).map((value) => ({
|
||||
title: t(`user.roles.${value}`),
|
||||
value,
|
||||
}))
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<v-data-table-server
|
||||
v-model:items-per-page="perPage"
|
||||
:page="page"
|
||||
:headers="headers"
|
||||
:items="users"
|
||||
:items-length="totalCount"
|
||||
:loading="isLoading"
|
||||
@click:row="rowClicked"
|
||||
@update:page="updatePage"
|
||||
>
|
||||
<template #item.roles="{ item }"> {{ item.roles?.join(", ") }} affaafafa </template>
|
||||
<template #item.updatedAt="{ value }">{{ formatDate(value) }}</template>
|
||||
<template #item.createdAt="{ value }">{{ formatDate(value) }}</template>
|
||||
<template
|
||||
v-for="(_, name) in $slots"
|
||||
:key="name"
|
||||
#[name]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="name"
|
||||
v-bind="slotProps"
|
||||
></slot>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export const DEFAULT_HEADERS = [
|
||||
{
|
||||
title: "Name",
|
||||
key: "displayName",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
key: "roles",
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
import { formatDate } from "@/utils/formatters"
|
||||
|
||||
import useUsers, {
|
||||
User,
|
||||
UserFiltersOptions,
|
||||
UserQueryOptions,
|
||||
UserWhereOptions,
|
||||
} from "@/use/use-users"
|
||||
import useRouteQueryPagination from "@/use/utils/use-route-query-pagination"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
headers?: { title: string; key: string }[]
|
||||
filters?: UserFiltersOptions
|
||||
where?: UserWhereOptions
|
||||
waiting?: boolean
|
||||
routeQuerySuffix?: string
|
||||
}>(),
|
||||
{
|
||||
headers: () => DEFAULT_HEADERS,
|
||||
filters: () => ({}),
|
||||
where: () => ({}),
|
||||
waiting: false,
|
||||
routeQuerySuffix: "Users",
|
||||
}
|
||||
)
|
||||
|
||||
const { page, perPage } = useRouteQueryPagination({ routeQuerySuffix: "Users", perPage: 100 })
|
||||
|
||||
const userQueryOptions = computed<UserQueryOptions>(() => {
|
||||
return {
|
||||
where: props.where,
|
||||
filters: props.filters,
|
||||
perPage: perPage.value,
|
||||
page: page.value,
|
||||
}
|
||||
})
|
||||
|
||||
const { users, totalCount, isLoading, refresh } = useUsers(userQueryOptions, {
|
||||
skipWatchIf: () => props.waiting,
|
||||
})
|
||||
|
||||
type UserTableRow = {
|
||||
item: User
|
||||
}
|
||||
|
||||
const emit = defineEmits<{ clicked: [userId: User] }>()
|
||||
|
||||
function rowClicked(_event: unknown, row: UserTableRow) {
|
||||
emit("clicked", row.item)
|
||||
}
|
||||
|
||||
function updatePage(newPage: number) {
|
||||
if (isLoading.value || props.waiting) return
|
||||
|
||||
page.value = newPage
|
||||
}
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { stripTrailingSlash } from "@/utils/strip-trailing-slash"
|
||||
|
||||
export const ENVIRONMENT = import.meta.env.MODE
|
||||
|
||||
const prodConfig = {
|
||||
domain: "https://dev-7mdjzcgwirhocfwm.ca.auth0.com",
|
||||
clientId: "TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J",
|
||||
audience: "alphane-api",
|
||||
apiBaseUrl: "",
|
||||
webSocketBaseUrl: "",
|
||||
applicationName: "ALPHANE",
|
||||
}
|
||||
|
||||
const devConfig = {
|
||||
domain: "https://dev-7mdjzcgwirhocfwm.ca.auth0.com",
|
||||
clientId: "TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J",
|
||||
audience: "alphane-api",
|
||||
apiBaseUrl: "http://localhost:3000",
|
||||
webSocketBaseUrl: "ws://localhost:3000",
|
||||
applicationName: "ALPHANE",
|
||||
}
|
||||
|
||||
const localProductionConfig = {
|
||||
domain: "https://dev-7mdjzcgwirhocfwm.ca.auth0.com",
|
||||
clientId: "TRlKzdNBynpo9tU1RSmnF0p8d3IEam4J",
|
||||
audience: "alphane-api",
|
||||
apiBaseUrl: "http://localhost:8080",
|
||||
webSocketBaseUrl: "ws://localhost:8080",
|
||||
applicationName: "ALPHANE (production)",
|
||||
}
|
||||
|
||||
let config = prodConfig
|
||||
|
||||
if (ENVIRONMENT === "production" && window.location.host === "localhost:8080") {
|
||||
config = localProductionConfig
|
||||
} else if (window.location.host === "localhost:8080") {
|
||||
config = devConfig
|
||||
}
|
||||
|
||||
export const APPLICATION_NAME = config.applicationName
|
||||
|
||||
export const API_BASE_URL = config.apiBaseUrl
|
||||
export const WEB_SOCKET_BASE_URL = config.webSocketBaseUrl
|
||||
|
||||
export const AUTH0_DOMAIN = stripTrailingSlash(config.domain)
|
||||
export const AUTH0_AUDIENCE = config.audience
|
||||
export const AUTH0_CLIENT_ID = config.clientId
|
||||
@@ -0,0 +1,5 @@
|
||||
import { visible } from "./visible"
|
||||
|
||||
export default {
|
||||
visible,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { DirectiveBinding } from "vue"
|
||||
|
||||
/**
|
||||
* Shows or hides an element using visibility property based on a boolean value.
|
||||
*
|
||||
* This makes helps loaders avoid downshift flickering when they trigger.
|
||||
*
|
||||
* See https://vuejs.org/guide/reusability/custom-directives.html#function-shorthand
|
||||
*
|
||||
* @usage
|
||||
* ```html
|
||||
* <v-progress-linear
|
||||
* v-visible="isAnyDashboardLoading"
|
||||
* indeterminate
|
||||
* height="2"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function visible(el: HTMLElement, binding: DirectiveBinding) {
|
||||
if (binding.value) {
|
||||
el.style.visibility = "visible"
|
||||
} else {
|
||||
el.style.visibility = "hidden"
|
||||
}
|
||||
}
|
||||
|
||||
export default visible
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<v-layout>
|
||||
<DefaultAppBar @toggle-ai-panel="showAiPanel = !showAiPanel" />
|
||||
<!--
|
||||
<LeftSidebarNavigationDrawer /> -->
|
||||
|
||||
<v-main>
|
||||
<!-- Provides the application the proper gutter -->
|
||||
<v-container
|
||||
fluid
|
||||
class="h-100"
|
||||
>
|
||||
<h2 class="text-h3 mt-3">{{ title }}</h2>
|
||||
<ExactingBreadcrumbs
|
||||
class="pl-0 pt-2 mb-1"
|
||||
:items="breadcrumbs"
|
||||
/>
|
||||
|
||||
<router-view />
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<AiChatPanel
|
||||
v-if="showAiPanel"
|
||||
v-model="showAiPanel"
|
||||
/>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
|
||||
import DefaultAppBar from "@/components/layout/DefaultAppBar.vue"
|
||||
import ExactingBreadcrumbs from "@/components/layout/ExactingBreadcrumbs.vue"
|
||||
//import LeftSidebarNavigationDrawer from "@/components/administration-layout/LeftSidebarNavigationDrawer.vue"
|
||||
|
||||
const { title, breadcrumbs } = useBreadcrumbs(undefined, undefined, {
|
||||
baseCrumb: {
|
||||
title: "Administration Dashboard",
|
||||
to: {
|
||||
name: "administration/AdministrationDashboardPage",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const showAiPanel = ref(false)
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<DefaultAppBar />
|
||||
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
:class="mobile ? 'pa-2' : 'pa-4'"
|
||||
>
|
||||
<router-view />
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDisplay } from "vuetify"
|
||||
|
||||
import DefaultAppBar from "@/components/layout/DefaultAppBar.vue"
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col style="position: relative">
|
||||
<div class="d-flex align-start justify-space-between">
|
||||
<div class="flex-grow-1">
|
||||
<h2 class="text-h3 mb-n2">
|
||||
{{ title }}
|
||||
</h2>
|
||||
|
||||
<ExactingBreadcrumbs
|
||||
class="pl-0 pt-2 mb-1"
|
||||
:items="breadcrumbs"
|
||||
/>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="showBackButton && !mobile"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-arrow-left"
|
||||
size="small"
|
||||
class="ml-2 mt-4"
|
||||
color="warning"
|
||||
@click="router.back()"
|
||||
>
|
||||
Back
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
v-if="showBackButton && mobile"
|
||||
class="mt-n5"
|
||||
>
|
||||
<a
|
||||
class="d-inline-flex align-center text-primary text-decoration-none"
|
||||
href="#"
|
||||
@click.prevent="router.back()"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-arrow-left
|
||||
</v-icon>
|
||||
Back
|
||||
</a>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ExactingBreadcrumbs from "@/components/layout/ExactingBreadcrumbs.vue"
|
||||
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
|
||||
import { useDisplay } from "vuetify"
|
||||
import { useRouter } from "vue-router"
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
const router = useRouter()
|
||||
|
||||
const { title, breadcrumbs, showBackButton } = useBreadcrumbs(undefined, undefined, {
|
||||
baseCrumb: {
|
||||
title: "Dashboard",
|
||||
to: {
|
||||
name: "DashboardPage",
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<UserProfileCard :user-id="currentUser.id" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<router-view />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
import UserProfileCard from "@/components/users/UserProfileCard.vue"
|
||||
|
||||
const { currentUser } = useCurrentUser<true>()
|
||||
|
||||
useBreadcrumbs("My Profile", [
|
||||
{
|
||||
title: "My Profile",
|
||||
to: {
|
||||
name: "ProfilePage",
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<RouterLink
|
||||
to="/dashboard"
|
||||
class="d-flex"
|
||||
>
|
||||
<img
|
||||
class="ml-0 mt-1"
|
||||
style="height: 36px; transform: rotate(-12deg)"
|
||||
:src="AlphaneLogo"
|
||||
/>
|
||||
<div v-if="sidebarMini || mdAndDown"></div>
|
||||
<div
|
||||
v-else
|
||||
class="d-flex"
|
||||
style="width: 200px"
|
||||
>
|
||||
<div
|
||||
class="mt-1 ml-3"
|
||||
style="font-size: 26px; color: #505682"
|
||||
>
|
||||
ALPHANE
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AlphaneLogo from "@/assets/alphane_logo_small.png"
|
||||
|
||||
import { useDisplay } from "vuetify"
|
||||
import useInterface from "@/use/use-interface"
|
||||
import { watch } from "vue"
|
||||
|
||||
const { sidebarMini, setSidebarMini } = useInterface()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
watch(mdAndDown, (newVal) => {
|
||||
if (newVal === true) setSidebarMini(false)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.logo a {
|
||||
text-decoration: none !important;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
user: {
|
||||
roles: {
|
||||
system_admin: "System Admin",
|
||||
user: "User",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createApp } from "vue"
|
||||
|
||||
// Plugins
|
||||
import vuetify from "@/plugins/vuetify-plugin"
|
||||
import auth0 from "@/plugins/auth0-plugin"
|
||||
import vueI18nPlugin from "@/plugins/vue-i18n-plugin"
|
||||
|
||||
import "@/scss/style.scss"
|
||||
import VueScrollTo from "vue-scrollto"
|
||||
import directives from "@/directives"
|
||||
import router from "@/router"
|
||||
|
||||
import App from "@/App.vue"
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router).use(vuetify).use(auth0).use(vueI18nPlugin)
|
||||
|
||||
app.directive("visible", directives.visible)
|
||||
|
||||
app.mount("#app")
|
||||
|
||||
app.use(VueScrollTo, {
|
||||
duration: 1000,
|
||||
easing: "ease",
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<PageLoader message="ALPHANE" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
|
||||
import PageLoader from "@/components/common/PageLoader.vue"
|
||||
|
||||
const router = useRouter()
|
||||
const timeout = ref<number>()
|
||||
|
||||
onMounted(() => {
|
||||
timeout.value = setTimeout(() => {
|
||||
router.push({ name: "DashboardPage" })
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timeout.value)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div v-if="isSystemAdmin">
|
||||
<AppCard :to="{ name: 'administration/DashboardPage' }">You are a system admin</AppCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
import AppCard from "@/components/common/AppCard.vue"
|
||||
|
||||
const { isSystemAdmin } = useCurrentUser<true>()
|
||||
|
||||
useBreadcrumbs()
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<UserProfileCard :user-id="currentUser.id" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
import UserProfileCard from "@/components/users/UserProfileCard.vue"
|
||||
|
||||
const { currentUser } = useCurrentUser<true>()
|
||||
|
||||
useBreadcrumbs("My Profile", [
|
||||
{
|
||||
title: "My Profile",
|
||||
to: {
|
||||
name: "ProfilePage",
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="pa-3">
|
||||
<v-row class="h-100vh auth">
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="5"
|
||||
xl="4"
|
||||
class="d-flex align-center justify-center bg-surface"
|
||||
>
|
||||
<div class="mt-xl-0 mt-5 mw-100 text-center">
|
||||
<div
|
||||
class="px-8"
|
||||
style="max-width: 500px"
|
||||
>
|
||||
<img
|
||||
class="d-inline-block d-lg-none"
|
||||
src="@/assets/app_logo_small.png"
|
||||
style="height: 66px; transform: rotate(-12deg)"
|
||||
/>
|
||||
<h2
|
||||
class="text-h1 textPrimary font-weight-semibold mb-0"
|
||||
style="font-size: 2.5rem !important"
|
||||
>
|
||||
ALPHANE
|
||||
</h2>
|
||||
<div
|
||||
class="card-subtitle mb-6"
|
||||
style="font-size: 1.2rem"
|
||||
>
|
||||
Helicopter Maintenance Tracking Platform
|
||||
</div>
|
||||
<p>
|
||||
This application is will streamline maintenance control decision making and unlock
|
||||
operational constraints.
|
||||
</p>
|
||||
<h6 class="text-h6 text-medium-emphasis d-flex align-center mt-6 font-weight-medium">
|
||||
<v-btn
|
||||
block
|
||||
color="primary"
|
||||
@click="doLogin"
|
||||
>Sign in</v-btn
|
||||
>
|
||||
</h6>
|
||||
|
||||
<div class="my-6 text-medium-emphasis">
|
||||
If you don't have an account, create one to get started using ROTYR. It's free to sign
|
||||
up and only takes a few seconds.
|
||||
</div>
|
||||
|
||||
<!-- <v-btn
|
||||
block
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
class="mt-3"
|
||||
:to="{ name: 'SignUpPage' }"
|
||||
text="Sign Up"
|
||||
/> -->
|
||||
<div
|
||||
v-if="isAuthenticated"
|
||||
class="mt-5 text-center"
|
||||
>
|
||||
<v-btn
|
||||
color="warning"
|
||||
variant="text"
|
||||
@click="logoutWrapper"
|
||||
>Sign out</v-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
lg="7"
|
||||
xl="8"
|
||||
class="d-none d-lg-flex align-center justify-center authentication position-relative"
|
||||
>
|
||||
<div class="text-center">
|
||||
<img
|
||||
src="@/assets/app_logo_splash.png"
|
||||
class="position-relative"
|
||||
style="opacity: 0.15; width: 80%"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
const { reset: resetCurrentUser } = useCurrentUser()
|
||||
const { isAuthenticated, loginWithRedirect, logout } = useAuth0()
|
||||
|
||||
onMounted(() => {
|
||||
resetCurrentUser()
|
||||
})
|
||||
|
||||
function doLogin() {
|
||||
loginWithRedirect({
|
||||
appState: { target: "/dashboard" },
|
||||
})
|
||||
}
|
||||
async function logoutWrapper() {
|
||||
await logout({
|
||||
logoutParams: {
|
||||
returnTo: window.location.origin,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-container>
|
||||
Return to <router-link :to="{ name: returnTo.name }">{{ returnTo.title }}</router-link>
|
||||
|
||||
<v-row class="mt-5">
|
||||
<v-col cols="12">
|
||||
<v-card
|
||||
outlined
|
||||
class="pa-3"
|
||||
:loading="isLoading"
|
||||
>
|
||||
<v-card-title
|
||||
>Environment Information
|
||||
<v-btn
|
||||
class="ma-0 ml-1"
|
||||
icon
|
||||
size="small"
|
||||
color="success"
|
||||
title="refresh"
|
||||
@click="refresh"
|
||||
>
|
||||
<v-icon>mdi-cached</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-list dense>
|
||||
<v-list-item> Release Tag: {{ environment.releaseTag }} </v-list-item>
|
||||
<v-list-item> Git Commit Hash: {{ environment.gitCommitHash }} </v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import http from "@/api/http-client"
|
||||
|
||||
const { isAuthenticated } = useAuth0()
|
||||
|
||||
const returnTo = computed<{ name: string; title: string }>(() => {
|
||||
if (isAuthenticated.value) {
|
||||
return {
|
||||
name: "DashboardPage",
|
||||
title: "Dashboard",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "SignInPage",
|
||||
title: "Sign In",
|
||||
}
|
||||
})
|
||||
|
||||
const environment = reactive({
|
||||
releaseTag: "not-set",
|
||||
gitCommitHash: "not-set",
|
||||
})
|
||||
|
||||
const isLoading = ref(true)
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh()
|
||||
})
|
||||
|
||||
async function fetchVersion() {
|
||||
return http
|
||||
.get("/_status")
|
||||
.then(({ data }) => {
|
||||
environment.releaseTag = data.RELEASE_TAG
|
||||
environment.gitCommitHash = data.GIT_COMMIT_HASH
|
||||
return data
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(`Error fetching version: ${error}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
await fetchVersion()
|
||||
} catch (error) {
|
||||
console.error(`Error fetching version: ${error}`)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<!-- Users Card -->
|
||||
<v-col
|
||||
v-if="isSystemAdmin"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card
|
||||
elevation="10"
|
||||
:to="{ name: 'administration/UsersPage' }"
|
||||
hover
|
||||
>
|
||||
<v-card-item>
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
color="success"
|
||||
size="56"
|
||||
class="mr-4"
|
||||
>
|
||||
<v-icon
|
||||
size="32"
|
||||
color="white"
|
||||
>
|
||||
mdi-account-group
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<v-card-title class="text-h5">Users</v-card-title>
|
||||
<v-card-subtitle>Manage users and permissions</v-card-subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span class="text-h3 font-weight-bold">{{ usersCount }}</span>
|
||||
<v-icon
|
||||
size="large"
|
||||
color="success"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">Total Users</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<v-col
|
||||
v-if="isSystemAdmin"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card
|
||||
elevation="10"
|
||||
:to="{ name: 'administration/SettingsPage' }"
|
||||
hover
|
||||
>
|
||||
<v-card-item>
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
color="info"
|
||||
size="56"
|
||||
class="mr-4"
|
||||
>
|
||||
<v-icon
|
||||
size="32"
|
||||
color="white"
|
||||
>
|
||||
mdi-cog
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<v-card-title class="text-h5">Settings</v-card-title>
|
||||
<v-card-subtitle>System configuration</v-card-subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span class="text-h3 font-weight-bold"> </span>
|
||||
|
||||
<v-icon
|
||||
size="large"
|
||||
color="info"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis mt-1">Configure System</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useUsers from "@/use/use-users"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
const { isSystemAdmin } = useCurrentUser()
|
||||
|
||||
const { totalCount: usersCount } = useUsers()
|
||||
|
||||
useBreadcrumbs("Administration Dashboard", [])
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template><div></div></template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
|
||||
useBreadcrumbs("Settings", [
|
||||
{
|
||||
title: "Settings",
|
||||
to: {
|
||||
name: "administration/SettingsPage",
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<HeaderActionsCard
|
||||
title="Users"
|
||||
elevation="10"
|
||||
>
|
||||
<template
|
||||
v-if="isSystemAdmin"
|
||||
#header-actions
|
||||
>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:to="{
|
||||
name: 'administration/users/UserNewPage',
|
||||
}"
|
||||
>
|
||||
<v-icon class="mr-3">mdi-plus</v-icon>
|
||||
Add User
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<UsersDataTableServer />
|
||||
</HeaderActionsCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
|
||||
import HeaderActionsCard from "@/components/common/HeaderActionsCard.vue"
|
||||
import UsersDataTableServer from "@/components/users/UsersDataTableServer.vue"
|
||||
|
||||
const { isSystemAdmin } = useCurrentUser()
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{
|
||||
title: "Users",
|
||||
to: {
|
||||
name: "administration/UsersPage",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
useBreadcrumbs("Users", breadcrumbs)
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<UserEditCardForm
|
||||
:user-id="userId"
|
||||
:return-to="{
|
||||
name: 'administration/users/UserPage',
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
}"
|
||||
@saved="refreshUserOrCurrentUser"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useUser from "@/use/use-user"
|
||||
import useSnack from "@/use/use-snack"
|
||||
|
||||
import UserEditCardForm from "@/components/users/UserEditCardForm.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
userId: string
|
||||
}>()
|
||||
|
||||
const userId = computed(() => parseInt(props.userId))
|
||||
const { user, refresh } = useUser(userId)
|
||||
|
||||
const { currentUser, refresh: refreshCurrentUser } = useCurrentUser<true>()
|
||||
const snack = useSnack()
|
||||
|
||||
async function refreshUserOrCurrentUser() {
|
||||
if (userId.value === currentUser.value.id) {
|
||||
await refreshCurrentUser()
|
||||
snack.info("Logged-in user updated. App refreshed.")
|
||||
} else {
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
return [
|
||||
{
|
||||
title: "Users",
|
||||
to: {
|
||||
name: "administration/UsersPage",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: user.value?.displayName || user.value?.email,
|
||||
to: {
|
||||
name: "administration/users/UserPage",
|
||||
params: {
|
||||
userId: props.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
useBreadcrumbs("User Details", breadcrumbs)
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<HeaderActionsFormCard
|
||||
ref="headerActionsFormCard"
|
||||
title="New User"
|
||||
@submit.prevent="createUser"
|
||||
>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="userAttributes.firstName"
|
||||
label="First name *"
|
||||
required
|
||||
:rules="[required]"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="userAttributes.lastName"
|
||||
label="Last name *"
|
||||
required
|
||||
:rules="[required]"
|
||||
@update:focused="autoFillDependentFields"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="userAttributes.displayName"
|
||||
label="Display name *"
|
||||
required
|
||||
:rules="[required]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<UserEmailUniqueTextField
|
||||
v-model="userAttributes.email"
|
||||
label="Email *"
|
||||
:rules="[required, minimum(2), email]"
|
||||
required
|
||||
validate-on="blur"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="userAttributes.title"
|
||||
label="Title"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="userAttributes.pilotLicense"
|
||||
label="Pilot License"
|
||||
placeholder="e.g., CPL-123456"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="userAttributes.ameLicense"
|
||||
label="AME License"
|
||||
placeholder="e.g., AME-789012"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<h4 class="mt-3">Images</h4>
|
||||
<v-divider class="mt-1 mb-2" />
|
||||
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-file-input
|
||||
v-model="profileImageFile"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-camera"
|
||||
label="Upload Profile Image"
|
||||
clearable
|
||||
@change="handleProfileImageUpload"
|
||||
/>
|
||||
<div
|
||||
v-if="profileImagePreview"
|
||||
class="mt-4 d-flex justify-center"
|
||||
>
|
||||
<v-img
|
||||
:src="profileImagePreview"
|
||||
max-width="200"
|
||||
max-height="200"
|
||||
class="rounded elevation-2"
|
||||
cover
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-file-input
|
||||
v-model="signatureImageFile"
|
||||
accept="image/*"
|
||||
prepend-icon="mdi-draw"
|
||||
label="Upload Signature"
|
||||
clearable
|
||||
@change="handleSignatureImageUpload"
|
||||
/>
|
||||
<div
|
||||
v-if="signatureImagePreview"
|
||||
class="mt-4 d-flex justify-center"
|
||||
>
|
||||
<v-img
|
||||
:src="signatureImagePreview"
|
||||
max-width="300"
|
||||
max-height="150"
|
||||
class="rounded elevation-2"
|
||||
contain
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<h4 class="mt-3">Notification Details</h4>
|
||||
<v-divider class="mt-1 mb-2" />
|
||||
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-switch
|
||||
v-model="userAttributes.emailNotificationsEnabled"
|
||||
label="Email notifications enabled?"
|
||||
inset
|
||||
center-affix
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex">
|
||||
<v-btn
|
||||
:loading="isLoading"
|
||||
type="submit"
|
||||
>
|
||||
Create User
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="warning"
|
||||
class="ml-4"
|
||||
variant="outlined"
|
||||
:loading="isLoading"
|
||||
:to="{
|
||||
name: 'administration/UsersPage',
|
||||
}"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</HeaderActionsFormCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { isEmpty, isNil } from "lodash"
|
||||
|
||||
import { required, minimum, email } from "@/utils/validators"
|
||||
import { resizeToStandard } from "@/utils/image-resizer"
|
||||
import usersApi, { User } from "@/api/users-api"
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useSnack from "@/use/use-snack"
|
||||
|
||||
import HeaderActionsFormCard from "@/components/shared/cards/HeaderActionsFormCard.vue"
|
||||
import UserEmailUniqueTextField from "@/components/users/UserEmailUniqueTextField.vue"
|
||||
|
||||
const userAttributes = ref<Partial<User>>({
|
||||
email: "",
|
||||
displayName: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
title: null,
|
||||
pilotLicense: null,
|
||||
ameLicense: null,
|
||||
emailNotificationsEnabled: true,
|
||||
})
|
||||
|
||||
// Image handling
|
||||
const profileImageFile = ref<File[] | File | null>(null)
|
||||
const signatureImageFile = ref<File[] | File | null>(null)
|
||||
const profileImagePreview = ref<string | null>(null)
|
||||
const signatureImagePreview = ref<string | null>(null)
|
||||
|
||||
async function handleProfileImageUpload() {
|
||||
if (!profileImageFile.value) {
|
||||
profileImagePreview.value = null
|
||||
userAttributes.value.profileImage = null
|
||||
return
|
||||
}
|
||||
|
||||
// Handle both array and single file
|
||||
const file = Array.isArray(profileImageFile.value)
|
||||
? profileImageFile.value[0]
|
||||
: profileImageFile.value
|
||||
|
||||
if (!file) {
|
||||
profileImagePreview.value = null
|
||||
userAttributes.value.profileImage = null
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure we have a valid File object
|
||||
if (!(file instanceof File)) {
|
||||
console.error("Invalid file type:", file)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Resize image to 512x512 before upload
|
||||
const base64DataUrl = await resizeToStandard(file)
|
||||
profileImagePreview.value = base64DataUrl
|
||||
userAttributes.value.profileImage = base64DataUrl
|
||||
} catch (error) {
|
||||
console.error("Error resizing profile image:", error)
|
||||
snack.error("Failed to process profile image")
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSignatureImageUpload() {
|
||||
if (!signatureImageFile.value) {
|
||||
signatureImagePreview.value = null
|
||||
userAttributes.value.signatureImage = null
|
||||
return
|
||||
}
|
||||
|
||||
// Handle both array and single file
|
||||
const file = Array.isArray(signatureImageFile.value)
|
||||
? signatureImageFile.value[0]
|
||||
: signatureImageFile.value
|
||||
|
||||
if (!file) {
|
||||
signatureImagePreview.value = null
|
||||
userAttributes.value.signatureImage = null
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure we have a valid File object
|
||||
if (!(file instanceof File)) {
|
||||
console.error("Invalid file type:", file)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Resize image to 512x512 before upload
|
||||
const base64DataUrl = await resizeToStandard(file)
|
||||
signatureImagePreview.value = base64DataUrl
|
||||
userAttributes.value.signatureImage = base64DataUrl
|
||||
} catch (error) {
|
||||
console.error("Error resizing signature image:", error)
|
||||
snack.error("Failed to process signature image")
|
||||
}
|
||||
}
|
||||
|
||||
function autoFillDependentFields(focused: boolean) {
|
||||
if (focused) return
|
||||
|
||||
const { email, displayName, firstName, lastName } = userAttributes.value
|
||||
if ((isNil(firstName) || isEmpty(firstName)) && (isNil(lastName) || isEmpty(lastName))) return
|
||||
|
||||
if (isNil(displayName) || isEmpty(displayName)) {
|
||||
userAttributes.value.displayName = [firstName, lastName].filter(Boolean).join(" ")
|
||||
}
|
||||
|
||||
if (isNil(email) || isEmpty(email)) {
|
||||
userAttributes.value.email = `${firstName}.${lastName}@unknown.ca`
|
||||
}
|
||||
}
|
||||
|
||||
const headerActionsFormCard = ref<InstanceType<typeof HeaderActionsFormCard> | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const snack = useSnack()
|
||||
const router = useRouter()
|
||||
|
||||
async function createUser() {
|
||||
if (headerActionsFormCard.value === null) return
|
||||
|
||||
const { valid } = await headerActionsFormCard.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
const userAttributesWithAuthSubject = {
|
||||
...userAttributes.value,
|
||||
authSubject: userAttributes.value.email,
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { user } = await usersApi.create(userAttributesWithAuthSubject)
|
||||
snack.success("User created!")
|
||||
return router.push({
|
||||
name: "administration/users/UserPage",
|
||||
params: {
|
||||
userId: user.id,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
snack.error(`Failed to create user: ${error}`)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
useBreadcrumbs("User Creation", [
|
||||
{
|
||||
title: "Users",
|
||||
to: {
|
||||
name: "administration/UsersPage",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "New User",
|
||||
to: {
|
||||
name: "administration/users/UserNewPage",
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-empty-state
|
||||
headline="Whoops, 403"
|
||||
title="Access Forbidden"
|
||||
text="You do not have permission to access that page"
|
||||
:image="SplashImage"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
@click="goBack"
|
||||
>Back</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<!-- href="/" performs a more aggressive refresh than using to="xxx" -->
|
||||
<v-btn
|
||||
href="/dashboard"
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
>Home</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
width="200"
|
||||
@click="signOut"
|
||||
>Logout</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider
|
||||
class="mt-10 mb-6"
|
||||
thickness="1"
|
||||
/>
|
||||
<p>Site: {{ APPLICATION_NAME }}</p>
|
||||
<p>Version: {{ releaseTag }}</p>
|
||||
<p>Commit Hash: {{ gitCommitHash }}</p>
|
||||
</v-empty-state>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useStatus from "@/use/use-status"
|
||||
|
||||
const { logout } = useAuth0()
|
||||
const { reset: resetCurrentUser } = useCurrentUser()
|
||||
|
||||
const { releaseTag, gitCommitHash } = useStatus()
|
||||
|
||||
function goBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
resetCurrentUser()
|
||||
|
||||
const returnTo = encodeURI(window.location.origin + "/sign-in")
|
||||
return logout({
|
||||
logoutParams: {
|
||||
returnTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-empty-state
|
||||
headline="Whoops, 500"
|
||||
title="Internal Server Error"
|
||||
:text="'Oops! The server encountered an unexpected error. Please\u00a0contact\u00a0support.'"
|
||||
:image="SplashImage"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
@click="goBack"
|
||||
>Back</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<!-- href="/" performs a more aggressive refresh than using to="xxx" -->
|
||||
<v-btn
|
||||
href="/dashboard"
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
>Home</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
width="200"
|
||||
@click="signOut"
|
||||
>Logout</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider
|
||||
class="mt-10 mb-6"
|
||||
thickness="1"
|
||||
/>
|
||||
<p>Site: {{ APPLICATION_NAME }}</p>
|
||||
<p>Version: {{ releaseTag }}</p>
|
||||
<p>Commit Hash: {{ gitCommitHash }}</p>
|
||||
</v-empty-state>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useStatus from "@/use/use-status"
|
||||
|
||||
const { logout } = useAuth0()
|
||||
const { reset: resetCurrentUser } = useCurrentUser()
|
||||
|
||||
const { releaseTag, gitCommitHash } = useStatus()
|
||||
|
||||
function goBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
resetCurrentUser()
|
||||
|
||||
const returnTo = encodeURI(window.location.origin + "/sign-in")
|
||||
return logout({
|
||||
logoutParams: {
|
||||
returnTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-empty-state
|
||||
headline="Whoops, 404"
|
||||
title="Page Not Found"
|
||||
text="Oops! The page you're looking doesn't exist."
|
||||
:image="SplashImage"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
@click="goBack"
|
||||
>Back</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<!-- href="/" performs a more aggressive refresh than using to="xxx" -->
|
||||
<v-btn
|
||||
href="/dashboard"
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
>Home</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
width="200"
|
||||
@click="signOut"
|
||||
>Logout</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider
|
||||
class="mt-10 mb-6"
|
||||
thickness="1"
|
||||
/>
|
||||
<p>Site: {{ APPLICATION_NAME }}</p>
|
||||
<p>Version: {{ releaseTag }}</p>
|
||||
<p>Commit Hash: {{ gitCommitHash }}</p>
|
||||
</v-empty-state>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useStatus from "@/use/use-status"
|
||||
|
||||
const { logout } = useAuth0()
|
||||
const { reset: resetCurrentUser } = useCurrentUser()
|
||||
|
||||
const { releaseTag, gitCommitHash } = useStatus()
|
||||
|
||||
function goBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
resetCurrentUser()
|
||||
|
||||
const returnTo = encodeURI(window.location.origin + "/sign-in")
|
||||
return logout({
|
||||
logoutParams: {
|
||||
returnTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-empty-state
|
||||
headline="Whoops, 401"
|
||||
title="Unauthorized"
|
||||
text="If you think this is an error, please contact support. Alternatively, try logging out and signing back in."
|
||||
:image="SplashImage"
|
||||
>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
@click="goBack"
|
||||
>Back</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<!-- href="/" performs a more aggressive refresh than using to="xxx" -->
|
||||
<v-btn
|
||||
href="/dashboard"
|
||||
color="primary"
|
||||
width="200"
|
||||
variant="outlined"
|
||||
>Home</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="d-flex justify-center">
|
||||
<v-btn
|
||||
width="200"
|
||||
@click="signOut"
|
||||
>Logout</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider
|
||||
class="mt-10 mb-6"
|
||||
thickness="1"
|
||||
/>
|
||||
<p>Site: {{ APPLICATION_NAME }}</p>
|
||||
<p>Version: {{ releaseTag }}</p>
|
||||
<p>Commit Hash: {{ gitCommitHash }}</p>
|
||||
</v-empty-state>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SplashImage from "@/assets/SplashImage.png"
|
||||
import { useAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useStatus from "@/use/use-status"
|
||||
|
||||
const { logout } = useAuth0()
|
||||
const { reset: resetCurrentUser } = useCurrentUser()
|
||||
|
||||
const { releaseTag, gitCommitHash } = useStatus()
|
||||
|
||||
function goBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
resetCurrentUser()
|
||||
|
||||
const returnTo = encodeURI(window.location.origin + "/sign-in")
|
||||
return logout({
|
||||
logoutParams: {
|
||||
returnTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<UserEditCardForm
|
||||
:user-id="currentUser.id"
|
||||
:return-to="{
|
||||
name: 'ProfilePage',
|
||||
}"
|
||||
@saved="refreshWrapper"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useBreadcrumbs from "@/use/use-breadcrumbs"
|
||||
import useCurrentUser from "@/use/use-current-user"
|
||||
import useSnack from "@/use/use-snack"
|
||||
|
||||
import UserEditCardForm from "@/components/users/UserEditCardForm.vue"
|
||||
|
||||
const { currentUser, refresh } = useCurrentUser<true>()
|
||||
const snack = useSnack()
|
||||
|
||||
async function refreshWrapper() {
|
||||
await refresh()
|
||||
snack.info("Logged-in user updated. App refreshed.")
|
||||
}
|
||||
|
||||
useBreadcrumbs("Edit My Profile", [
|
||||
{
|
||||
title: "My Profile",
|
||||
to: {
|
||||
name: "ProfilePage",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Edit",
|
||||
to: {
|
||||
name: "profile/ProfileEditPage",
|
||||
},
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createAuth0 } from "@auth0/auth0-vue"
|
||||
|
||||
import { AUTH0_AUDIENCE, AUTH0_CLIENT_ID, AUTH0_DOMAIN, ENVIRONMENT } from "@/config"
|
||||
|
||||
// See https://auth0.github.io/auth0-vue/#md:add-login-to-your-application
|
||||
export default createAuth0({
|
||||
domain: AUTH0_DOMAIN,
|
||||
clientId: AUTH0_CLIENT_ID,
|
||||
authorizationParams: {
|
||||
audience: AUTH0_AUDIENCE,
|
||||
redirect_uri: `${window.location.origin}/callback`,
|
||||
},
|
||||
cacheLocation: ENVIRONMENT === "development" ? "localstorage" : "memory",
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createI18n } from "vue-i18n"
|
||||
|
||||
// I'd prefer to use yaml, or even json, but I can't get them to import at the moment
|
||||
// This might be a TypeScript issue, or I might need a yaml plugin.
|
||||
import en from "@/locales/en.js"
|
||||
|
||||
export default createI18n({
|
||||
legacy: false, // support composition api
|
||||
locale: "en",
|
||||
messages: {
|
||||
en,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* plugins/vuetify.js
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import "@mdi/font/css/materialdesignicons.css"
|
||||
import "vuetify/styles"
|
||||
|
||||
// ComposablesF
|
||||
import { createVuetify } from "vuetify"
|
||||
import * as components from "vuetify/components"
|
||||
import * as directives from "vuetify/directives"
|
||||
import * as labsComponents from "vuetify/labs/components"
|
||||
|
||||
import {
|
||||
DARK_BLUE_THEME,
|
||||
DARK_AQUA_THEME,
|
||||
DARK_ORANGE_THEME,
|
||||
DARK_PURPLE_THEME,
|
||||
DARK_GREEN_THEME,
|
||||
DARK_CYAN_THEME,
|
||||
} from "@/theme/DarkTheme"
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
components: {
|
||||
...components,
|
||||
...labsComponents,
|
||||
},
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: "DARK_AQUA_THEME",
|
||||
themes: {
|
||||
DARK_BLUE_THEME,
|
||||
DARK_AQUA_THEME,
|
||||
DARK_ORANGE_THEME,
|
||||
DARK_PURPLE_THEME,
|
||||
DARK_GREEN_THEME,
|
||||
DARK_CYAN_THEME,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
VCard: {
|
||||
rounded: "md",
|
||||
},
|
||||
VTextField: {
|
||||
variant: "outlined",
|
||||
density: "comfortable",
|
||||
color: "primary",
|
||||
},
|
||||
VTextarea: {
|
||||
variant: "outlined",
|
||||
density: "comfortable",
|
||||
color: "primary",
|
||||
},
|
||||
VFileInput: {
|
||||
variant: "outlined",
|
||||
density: "comfortable",
|
||||
color: "primary",
|
||||
prependIcon: null,
|
||||
},
|
||||
VSelect: {
|
||||
variant: "outlined",
|
||||
density: "comfortable",
|
||||
color: "primary",
|
||||
},
|
||||
VCombobox: {
|
||||
variant: "outlined",
|
||||
density: "comfortable",
|
||||
color: "primary",
|
||||
},
|
||||
VAutocomplete: {
|
||||
variant: "outlined",
|
||||
density: "comfortable",
|
||||
color: "primary",
|
||||
},
|
||||
VListItem: {
|
||||
minHeight: "45px",
|
||||
},
|
||||
VTooltip: {
|
||||
location: "top",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"
|
||||
import { authGuard } from "@auth0/auth0-vue"
|
||||
|
||||
import { APPLICATION_NAME } from "@/config"
|
||||
import administrationRoutes from "@/routes/administration-routes"
|
||||
import { authorizationGuard } from "@/utils/authorization-guards"
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/",
|
||||
name: "SignInPage",
|
||||
component: () => import("@/pages/SignInPage.vue"),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: "/callback",
|
||||
name: "CallbackPage",
|
||||
component: () => import("@/pages/CallbackPage.vue"),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("@/layouts/DefaultLayout.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
redirect: "sign-in",
|
||||
},
|
||||
{
|
||||
name: "DashboardPage",
|
||||
path: "dashboard",
|
||||
component: () => import("@/pages/DashboardPage.vue"),
|
||||
meta: {
|
||||
title: "Dashboard",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: () => import("@/layouts/LayoutWithBreadcrumbs.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "profile",
|
||||
component: () => import("@/layouts/ProfileLayout.vue"),
|
||||
meta: {
|
||||
title: "Profile",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "ProfilePage",
|
||||
redirect: { name: "profile/ProfileEditPage" },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "profile/edit",
|
||||
name: "profile/ProfileEditPage",
|
||||
component: () => import("@/pages/profile/ProfileEditPage.vue"),
|
||||
meta: {
|
||||
title: "Edit Profile",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...administrationRoutes,
|
||||
{
|
||||
name: "StatusPage",
|
||||
path: "/status",
|
||||
component: () => import("@/pages/StatusPage.vue"),
|
||||
meta: {
|
||||
title: "Status",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/errors/unauthorized",
|
||||
name: "errors/UnauthorizedPage",
|
||||
component: () => import("@/pages/errors/UnauthorizedPage.vue"),
|
||||
meta: {
|
||||
title: "Unauthorized",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/errors/forbidden",
|
||||
name: "errors/ForbiddenPage",
|
||||
component: () => import("@/pages/errors/ForbiddenPage.vue"),
|
||||
meta: {
|
||||
title: "Forbidden",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/errors/internal-server-error",
|
||||
name: "errors/InternalServerErrorPage",
|
||||
component: () => import("@/pages/errors/InternalServerErrorPage.vue"),
|
||||
meta: {
|
||||
title: "Internal Server Error",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/errors/not-found",
|
||||
name: "errors/NotFoundPage",
|
||||
component: () => import("@/pages/errors/NotFoundPage.vue"),
|
||||
meta: {
|
||||
title: "Not Found",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
redirect: "/errors/not-found",
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.onError((error, to) => {
|
||||
if (
|
||||
error.message.includes("Failed to fetch dynamically imported module") ||
|
||||
error.message.includes("Importing a module script failed")
|
||||
) {
|
||||
window.location.href = to.fullPath
|
||||
}
|
||||
})
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
if (to.meta && to.meta.title) {
|
||||
document.title = `${APPLICATION_NAME} - ${to.meta.title}`
|
||||
} else {
|
||||
document.title = APPLICATION_NAME
|
||||
}
|
||||
|
||||
if (to.meta.requiresAuth === false) return true
|
||||
|
||||
const isAuthenticated = await authGuard(to)
|
||||
if (!isAuthenticated) return false
|
||||
|
||||
const isAuthorized = await authorizationGuard(to)
|
||||
if (!isAuthorized) return "/errors/forbidden"
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,50 @@
|
||||
import { RouteRecordRaw } from "vue-router"
|
||||
|
||||
import { isSystemAdmin } from "@/utils/authorization-guards"
|
||||
|
||||
export const administrationRoutes: Readonly<RouteRecordRaw[]> = [
|
||||
{
|
||||
path: "/administration",
|
||||
component: () => import("@/layouts/AdministrationLayout.vue"),
|
||||
meta: {
|
||||
guards: [isSystemAdmin],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "administration/AdministrationDashboardPage",
|
||||
component: () => import("@/pages/administration/AdministrationDashboardPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
name: "administration/UsersPage",
|
||||
component: () => import("@/pages/administration/UsersPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "users/new",
|
||||
name: "administration/users/UserNewPage",
|
||||
component: () => import("@/pages/administration/users/UserNewPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "users/:userId",
|
||||
name: "administration/users/UserPage",
|
||||
component: () => import("@/pages/administration/users/UserEditPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "users/:userId/edit",
|
||||
name: "administration/users/UserEditPage",
|
||||
component: () => import("@/pages/administration/users/UserEditPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
name: "administration/SettingsPage",
|
||||
component: () => import("@/pages/administration/SettingsPage.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default administrationRoutes
|
||||
@@ -0,0 +1,204 @@
|
||||
@use "./variables" as *;
|
||||
|
||||
html {
|
||||
.bg-success {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.bg-secondary-gradient {
|
||||
background: linear-gradient(287deg, rgb(var(--v-theme-primary)) .54%, #1bcaff 100.84%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.border,
|
||||
.v-divider {
|
||||
border-color: rgba(var(--v-border-color)) !important;
|
||||
}
|
||||
|
||||
.avtar-border {
|
||||
border: 2px solid rgb(var(--v-theme-surface)) !important;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
font-size: $font-size-root;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.v-dialog {
|
||||
&.dialog-mw {
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
.round-40 {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.round-56 {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.round-48 {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.round-30 {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.lh-0 {
|
||||
line-height: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.lh-28 {
|
||||
line-height: 28px !important;
|
||||
}
|
||||
|
||||
.lh-32 {
|
||||
line-height: 32px !important;
|
||||
}
|
||||
|
||||
.space-p-96 {
|
||||
padding: 96px 0 !important;
|
||||
|
||||
}
|
||||
|
||||
.ps-96 {
|
||||
padding-inline-start: 96px !important;
|
||||
}
|
||||
|
||||
.pt-96 {
|
||||
padding-top: 96px !important;
|
||||
}
|
||||
|
||||
.end-0 {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
height: calc(100vh - 350px);
|
||||
}
|
||||
|
||||
.msg-chat-height {
|
||||
height: calc(-500px + 100vh);
|
||||
}
|
||||
|
||||
@media screen and (max-width:991px) {
|
||||
.overflow-x-reposive {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.border-m-none {
|
||||
border: 0 !important
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height:767px) {
|
||||
.msg-chat-height {
|
||||
height: calc(-315px + 100vh);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.max-h-600 {
|
||||
max-height: 600px;
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
|
||||
.custom-hover-primary {
|
||||
.iconify {
|
||||
color: rgb(255, 255, 255) !important;
|
||||
|
||||
@media screen and (max-width:991px) {
|
||||
color: rgba(var(--v-theme-textPrimary), 0.8) !important
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-lightprimary), 0.1);
|
||||
|
||||
.iconify {
|
||||
@media screen and (max-width:991px) {
|
||||
color: rgb(var(--v-theme-primary)) !important
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-hover-primary-white {
|
||||
.iconify {
|
||||
color: rgb(255, 255, 255) !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--v-theme-lightprimary), 0.1);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.no-icon {
|
||||
|
||||
.v-input__prepend,
|
||||
.v-input__append {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: rgb(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
.v-badge {
|
||||
&.x-small-badge {
|
||||
.v-badge__badge {
|
||||
height: 6px !important;
|
||||
width: 6px !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.one-line {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.two-line {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.z-1 {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
@use "sass:math";
|
||||
@use "sass:map";
|
||||
@use "sass:meta";
|
||||
@use "vuetify/lib/styles/tools/functions" as *;
|
||||
|
||||
// Custom Variables
|
||||
// colors
|
||||
$white: #fff !default;
|
||||
|
||||
// cards
|
||||
$card-title-size: 18px !default;
|
||||
|
||||
$body-font-family: "Overpass", sans-serif !default;
|
||||
$border-radius-root: 10px;
|
||||
$btn-font-weight: 400 !default;
|
||||
$btn-letter-spacing: 0 !default;
|
||||
|
||||
// Global Shadow
|
||||
$box-shadow:
|
||||
rgba(145 158 171 / 30%) 0px 0px 2px 0px,
|
||||
rgba(145 158 171 / 12%) 0px 12px 24px -4px;
|
||||
|
||||
// Global Radius as per breakeven point
|
||||
|
||||
@forward "vuetify/settings" with (
|
||||
$color-pack: false !default,
|
||||
// Global font size and border radius
|
||||
$font-size-root: 1rem,
|
||||
$border-radius-root: $border-radius-root,
|
||||
$body-font-family: $body-font-family,
|
||||
$heading-font-family: $body-font-family !default,
|
||||
$button-height: 40px,
|
||||
// 👉 Typography
|
||||
$typography: (
|
||||
"h1": (
|
||||
"size": 2.25rem,
|
||||
"weight": 600,
|
||||
"line-height": 2.75rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"h2": (
|
||||
"size": 1.875rem,
|
||||
"weight": 500,
|
||||
"line-height": 2.25rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"h3": (
|
||||
"size": 1.5rem,
|
||||
"weight": 500,
|
||||
"line-height": 2rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"h4": (
|
||||
"size": 1.3125rem,
|
||||
"weight": 500,
|
||||
"line-height": 1.6rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"h5": (
|
||||
"size": 1.125rem,
|
||||
"weight": 500,
|
||||
"line-height": 1.6rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"h6": (
|
||||
"size": 1rem,
|
||||
"weight": 500,
|
||||
"line-height": 1.2rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"subtitle-1": (
|
||||
"size": 0.875rem,
|
||||
"weight": 400,
|
||||
"line-height": 1.1rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"subtitle-2": (
|
||||
"size": 0.75rem,
|
||||
"weight": 400,
|
||||
"line-height": 1rem,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"body-1": (
|
||||
"size": 0.875rem,
|
||||
"weight": 400,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"body-2": (
|
||||
"size": 0.75rem,
|
||||
"weight": 400,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"button": (
|
||||
"size": 0.875rem,
|
||||
"weight": 500,
|
||||
"font-family": inherit,
|
||||
"text-transform": capitalize,
|
||||
),
|
||||
"caption": (
|
||||
"size": 0.75rem,
|
||||
"weight": 400,
|
||||
"font-family": inherit,
|
||||
),
|
||||
"overline": (
|
||||
"size": 0.75rem,
|
||||
"weight": 500,
|
||||
"font-family": inherit,
|
||||
"text-transform": uppercase,
|
||||
),
|
||||
)
|
||||
!default,
|
||||
// 👉 Button
|
||||
$button-border-radius: $border-radius-root !default,
|
||||
$button-text-letter-spacing: 0 !default,
|
||||
$button-text-transform: capitalize,
|
||||
$button-elevation: (
|
||||
"default": 0,
|
||||
"hover": 4,
|
||||
"active": 8,
|
||||
)
|
||||
!default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color: #212121 !default,
|
||||
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
$tooltip-font-size: 0.75rem !default,
|
||||
$tooltip-border-radius: 4px !default,
|
||||
$tooltip-padding: 4px 8px !default,
|
||||
|
||||
// 👉 Rounded
|
||||
$rounded: (
|
||||
0: 0,
|
||||
"sm": $border-radius-root * 0.5,
|
||||
null: $border-radius-root,
|
||||
"md": $border-radius-root * 1,
|
||||
"lg": $border-radius-root * 2,
|
||||
"xl": $border-radius-root * 6,
|
||||
"pill": 9999px,
|
||||
"circle": 50%,
|
||||
),
|
||||
|
||||
// 👉 Card
|
||||
// $card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 10 !default,
|
||||
$card-title-line-height: 1.6 !default,
|
||||
$card-text-padding: 24px !default,
|
||||
$card-item-padding: 30px 30px 24px !default,
|
||||
$card-actions-padding: 10px 24px 24px !default,
|
||||
$card-subtitle-opacity: 1 !default // $card-border-color $border-color-root
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
.single-line-alert {
|
||||
.v-alert__close,
|
||||
.v-alert__prepend {
|
||||
align-self: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.single-line-alert {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.v-alert__append {
|
||||
margin-inline-start: 0px;
|
||||
}
|
||||
.v-alert__close {
|
||||
margin-left: auto;
|
||||
}
|
||||
.v-alert__content {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
.v-breadcrumbs{
|
||||
.v-breadcrumbs-divider{
|
||||
padding: 0 0 !important;
|
||||
}
|
||||
.v-breadcrumbs-item--link{
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.v-btn-group .v-btn {
|
||||
height: inherit !important;
|
||||
}
|
||||
|
||||
.v-btn-group {
|
||||
border-color: rgb(var(--v-theme-borderColor)) !important;
|
||||
}
|
||||
.v-btn{
|
||||
text-transform: capitalize;
|
||||
letter-spacing: 0;
|
||||
border-radius: 30px;
|
||||
|
||||
&.v-btn--variant-elevated{
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.v-btn--slim{
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
.v-btn--elevated:hover{
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Outline Card
|
||||
.v-card--variant-outlined {
|
||||
border-color: rgba(var(--v-theme-borderColor)) !important;
|
||||
}
|
||||
|
||||
.v-card--variant-elevated,
|
||||
.v-card--variant-flat {
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
&:hover {
|
||||
scale: 1.01;
|
||||
transition: all 0.1s ease-in 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.v-card {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
.color-inherits {
|
||||
color: inherit;
|
||||
}
|
||||
.feature-card {
|
||||
.v-responsive__content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.v-timeline-divider__before,.v-timeline-divider__after {
|
||||
background: rgba(var(--v-border-color), 1);
|
||||
}
|
||||
.v-card-text{
|
||||
padding: 24px 24px;
|
||||
}
|
||||
.v-card-item{
|
||||
padding: 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme cards
|
||||
.cardBordered {
|
||||
.v-card {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid rgb(var(--v-theme-borderColor));
|
||||
}
|
||||
}
|
||||
|
||||
.elevation-o-card{
|
||||
.v-card-item{
|
||||
padding: 0.625rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title{
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color:rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
.card-subtitle{
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color:rgb(var(--v-theme-textSecondary));
|
||||
}
|
||||
.dark-card-title{
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color:rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.theme-carousel .v-carousel__progress {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.v-pagination__item--is-active .v-btn__overlay {
|
||||
opacity: 0.15 !important;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
.v-table {
|
||||
|
||||
&.datatabels {
|
||||
|
||||
&.productlist {
|
||||
.v-data-table-header__content span {
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
|
||||
.v-toolbar {
|
||||
.v-input__control {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.v-toolbar__content {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
thead tr th:first-child {
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
tbody tr td:first-child {
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
tbody tr td{
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.v-selection-control--dirty .v-selection-control__input>.v-icon {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width:1368px) {
|
||||
|
||||
.v-table {
|
||||
|
||||
&.datatabels {
|
||||
|
||||
&.productlist {
|
||||
.v-data-table-header__content span {
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
|
||||
td {
|
||||
padding: 14px 5px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
padding: 14px 5px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.v-pagination {
|
||||
.v-pagination__list {
|
||||
.v-pagination__item--is-active {
|
||||
.v-btn {
|
||||
.v-btn__overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
background-color: rgb(var(--v-theme-grey100)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.v-expansion-panel-title__overlay{
|
||||
background: rgba(var(--v-theme-primary));
|
||||
}
|
||||
.v-expansion-panel:not(:first-child)::after {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
.v-field--variant-outlined .v-field__outline__start.v-locale--is-ltr,
|
||||
.v-locale--is-ltr .v-field--variant-outlined .v-field__outline__start {
|
||||
border-radius: $border-radius-root 0 0 $border-radius-root;
|
||||
}
|
||||
|
||||
.v-field--variant-outlined .v-field__outline__end.v-locale--is-ltr,
|
||||
.v-locale--is-ltr .v-field--variant-outlined .v-field__outline__end {
|
||||
border-radius: 0 $border-radius-root $border-radius-root 0;
|
||||
}
|
||||
|
||||
.v-field {
|
||||
font-size: 14px;
|
||||
color: rgba(var(--v-theme-textPrimary));
|
||||
}
|
||||
|
||||
// select outlined
|
||||
.v-field--variant-outlined .v-field__outline__start,
|
||||
.v-field--variant-outlined .v-field__outline__notch::before,
|
||||
.v-field--variant-outlined .v-field__outline__notch::after,
|
||||
.v-field--variant-outlined .v-field__outline__end {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.v-field--active .v-label.v-field-label{
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// variant
|
||||
.v-input--density-default,
|
||||
.v-field--variant-solo,
|
||||
.v-field--variant-filled {
|
||||
--v-input-control-height: 51px;
|
||||
--v-input-padding-top: 14px;
|
||||
}
|
||||
|
||||
// comfortable
|
||||
.v-input--density-comfortable {
|
||||
--v-input-control-height: 44px;
|
||||
}
|
||||
|
||||
// compact
|
||||
.v-input--density-compact {
|
||||
--v-input-padding-top: 10px;
|
||||
}
|
||||
.v-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
.v-switch .v-label,
|
||||
.v-checkbox .v-label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.v-text-field__suffix {
|
||||
opacity: 1;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.shadow-none .v-field--variant-solo {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.v-time-picker-clock{
|
||||
background: rgb(var(--v-theme-grey100)) ;
|
||||
}
|
||||
.v-time-picker-controls__ampm__btn.v-btn.v-btn--density-default{
|
||||
border: 0 !important;
|
||||
}
|
||||
.v-stepper-header,.v-stepper.v-sheet{
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.v-time-picker-controls__time__btn.v-btn--density-default.v-btn {
|
||||
width: 55px !important;
|
||||
height: 55px !important;
|
||||
font-size: 30px;
|
||||
}
|
||||
.v-time-picker-controls__time__separator {
|
||||
font-size: 36px !important;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.v-list.theme-list {
|
||||
.v-list-item:hover > .v-list-item__overlay {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
.v-list-item--variant-text {
|
||||
.v-list-item__overlay {
|
||||
background: rgb(var(--v-theme-hoverColor));
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item__prepend,
|
||||
.v-list-item__content {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.v-list-item__overlay {
|
||||
background-color: rgb(var(--v-theme-hoverColor));
|
||||
}
|
||||
.v-list-item.v-list-item--active{
|
||||
.v-list-item__overlay{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mail-items{
|
||||
min-height: 40px !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item-title{
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.v-navigation-drawer__scrim.fade-transition-leave-to {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// For checkbox & radios
|
||||
.v-selection-control__input > .v-icon.mdi-checkbox-blank-outline,
|
||||
.v-selection-control__input > .v-icon.mdi-radiobox-blank {
|
||||
color: rgb(var(--v-theme-inputBorder));
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
.elevation-9 {
|
||||
box-shadow: rgb(0 0 0 / 5%) 0px 9px 17.5px !important;
|
||||
}
|
||||
|
||||
.elevation-10 {
|
||||
box-shadow: $box-shadow !important;
|
||||
}
|
||||
.elevation-1 {
|
||||
box-shadow:0px 12px 30px -2px rgba(58,75,116,0.14) !important
|
||||
}
|
||||
.elevation-2 {
|
||||
box-shadow:0px 24px 24px -12px rgba(0, 0, 0, .05) !important
|
||||
}
|
||||
.elevation-3 {
|
||||
box-shadow: rgba(145,158,171,0.2) 0px 0px 2px 0px, rgba(145,158,171,0.12) 0px 12px 24px -4px !important;
|
||||
}
|
||||
.elevation-4{
|
||||
box-shadow: 0px 12px 12px -6px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
.elevation-5
|
||||
{
|
||||
box-shadow: 1px 0 7px rgba(0, 0, 0, .05)!important;
|
||||
}
|
||||
|
||||
.primary-shadow {
|
||||
box-shadow: rgba(var(--v-theme-primary), 0.30) 0px 12px 14px 0px;
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.v-stepper-item--selected .v-stepper-item__avatar.v-avatar, .v-stepper-item--complete .v-stepper-item__avatar.v-avatar {
|
||||
background: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.v-stepper-item__avatar.v-avatar {
|
||||
background: rgba(var(--v-theme-primary), var(--v-medium-emphasis-opacity)) !important;
|
||||
color: rgb(var(--v-theme-on-primary)) !important;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.v-selection-control.v-selection-control--density-default {
|
||||
.v-switch__track,
|
||||
.v-switch__thumb {
|
||||
background-color: rgb(var(--v-theme-grey200));
|
||||
}
|
||||
&.v-selection-control--dirty {
|
||||
.v-selection-control__wrapper.text-primary {
|
||||
.v-switch__track {
|
||||
background-color: rgba(var(--v-theme-primary), 0.6);
|
||||
}
|
||||
.v-switch__thumb {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
.v-selection-control__wrapper.text-secondary {
|
||||
.v-switch__track {
|
||||
background-color: rgba(var(--v-theme-secondary), 0.6);
|
||||
}
|
||||
.v-switch__thumb {
|
||||
background-color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
.v-selection-control__wrapper.text-warning {
|
||||
.v-switch__track {
|
||||
background-color: rgba(var(--v-theme-warning), 0.6);
|
||||
}
|
||||
.v-switch__thumb {
|
||||
background-color: rgb(var(--v-theme-warning));
|
||||
}
|
||||
}
|
||||
.v-selection-control__wrapper.text-error {
|
||||
.v-switch__track {
|
||||
background-color: rgba(var(--v-theme-error), 0.6);
|
||||
}
|
||||
.v-switch__thumb {
|
||||
background-color: rgb(var(--v-theme-error));
|
||||
}
|
||||
}
|
||||
.v-selection-control__wrapper.text-success {
|
||||
.v-switch__track {
|
||||
background-color: rgba(var(--v-theme-success), 0.6);
|
||||
}
|
||||
.v-switch__thumb {
|
||||
background-color: rgb(var(--v-theme-success));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
.v-table .v-table__wrapper > table > tbody > tr:not(:last-child) > td,
|
||||
.v-table .v-table__wrapper > table > tbody > tr:not(:last-child) > th,
|
||||
.v-table .v-table__wrapper > table > thead > tr:last-child > th {
|
||||
border-bottom: thin solid rgba(var(--v-border-color)) !important;
|
||||
}
|
||||
|
||||
.v-data-table{
|
||||
th.v-data-table__th{
|
||||
font-size:16px;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
td.v-data-table__td{
|
||||
font-size: 14px;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
.v-data-table-footer{
|
||||
padding: 15px 8px;
|
||||
}
|
||||
.v-data-table-header__sort-badge{
|
||||
background-color:rgb(var(--v-theme-borderColor)) !important;
|
||||
}
|
||||
.tdhead{
|
||||
font-size:16px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width:767px) {
|
||||
.v-data-table-footer{
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.v-table {
|
||||
|
||||
|
||||
&.ticket-table {
|
||||
|
||||
table {
|
||||
thead {
|
||||
th {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
|
||||
td {
|
||||
padding: 16px 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.invoice-table {
|
||||
.v-table__wrapper {
|
||||
table {
|
||||
thead {
|
||||
th {
|
||||
font-weight: 600 !important;
|
||||
padding: 0px 24px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
|
||||
td {
|
||||
padding: 8px 24px !important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
.theme-tab {
|
||||
&.v-tabs {
|
||||
.v-tab {
|
||||
border-radius: $border-radius-root !important;
|
||||
min-width: auto !important;
|
||||
&.v-slide-group-item--active {
|
||||
background: rgb(var(--v-theme-primary));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.v-text-field input {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.v-field__outline {
|
||||
color: rgb(var(--v-theme-inputBorder));
|
||||
--v-field-border-opacity: 1 !important;
|
||||
}
|
||||
.input {
|
||||
.v-field--variant-outlined {
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.v-textarea input {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
@keyframes slideup {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0px, -100%, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
.animateDown {
|
||||
animation: 35s linear 0s infinite normal none running slideDown;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0px, 0, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
// OfferBar
|
||||
.offerbar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
|
||||
.white-btn {
|
||||
background-color: rgba(var(--v-theme-surface), 0.15);
|
||||
font-weight: 700;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
&:before {
|
||||
background-repeat: no-repeat;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-image: url('@/assets/images/front-pages/background/left-shape.png');
|
||||
bottom: 0;
|
||||
height: 40px;
|
||||
left: 0;
|
||||
width: 325px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
background-repeat: no-repeat;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-image: url('@/assets/images/front-pages/background/right-shape.png');
|
||||
bottom: 0;
|
||||
right: 17%;
|
||||
width: 325px;
|
||||
top: 0;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// frameworks
|
||||
//
|
||||
.slider-group {
|
||||
animation: slide 45s linear infinite;
|
||||
}
|
||||
|
||||
.marquee1-group {
|
||||
animation: marquee 45s linear infinite;
|
||||
}
|
||||
|
||||
.marquee2-group {
|
||||
animation: marquee2 45s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(-2086px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee2 {
|
||||
0% {
|
||||
transform: translate3d(-2086px, 0, 0)
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
.front-wraper {
|
||||
overflow: hidden;
|
||||
|
||||
.underline-link {
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
.underline-link-6 {
|
||||
text-underline-offset: 6px;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
.main-banner {
|
||||
min-width: 1300px;
|
||||
overflow: hidden;
|
||||
max-height: 700px;
|
||||
height: calc(100vh - 100px);
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.team {
|
||||
&:hover {
|
||||
.intro {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.intro {
|
||||
opacity: 0;
|
||||
bottom: 16px;
|
||||
inset-inline-start: .75rem;
|
||||
inset-inline-end: .75rem;
|
||||
transition: 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
// Revenue Products
|
||||
.feature-tabs {
|
||||
.v-slide-group__content {
|
||||
gap: 16px;
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.v-btn {
|
||||
background-color: rgba(var(--v-theme-surface));
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px !important;
|
||||
font-size: 16px;
|
||||
box-shadow: 0px 24px 24px -12px rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
.v-tab--selected {
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
box-shadow: 0px 24px 24px -12px rgba(99, 91, 255, .15);
|
||||
|
||||
.v-btn__content {
|
||||
color: #fff;
|
||||
|
||||
.v-tab__slider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-container {
|
||||
&.max-width-1218 {
|
||||
max-width: 1218px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.v-container {
|
||||
&.max-width-800 {
|
||||
max-width: 800px !important;
|
||||
}
|
||||
|
||||
&.max-width-1000 {
|
||||
max-width: 1000px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.max-w-600 {
|
||||
max-width: 600px !important;
|
||||
}
|
||||
|
||||
.template {
|
||||
.left-widget {
|
||||
position: absolute;
|
||||
top: 96px;
|
||||
inset-inline-start: -40px;
|
||||
max-height: 400px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.right-widget {
|
||||
position: absolute;
|
||||
top: 96px;
|
||||
inset-inline-end: -40px;
|
||||
max-height: 400px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.feature-tabs {
|
||||
.v-tab__slider {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
&.v-tabs--density-default {
|
||||
--v-tabs-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-tabs-expansion {
|
||||
|
||||
.v-expansion-panel-text__wrapper {
|
||||
padding: 0px 0px 16px;
|
||||
}
|
||||
|
||||
.v-expansion-panel-title {
|
||||
padding: 16px 0px;
|
||||
}
|
||||
|
||||
.v-expansion-panel {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.v-expansion-panel--active:not(:first-child),
|
||||
.v-expansion-panel--active+.v-expansion-panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leader-slider {
|
||||
.carousel__slide {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.carousel__viewport {
|
||||
margin: 0 -15px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.carousel__prev,
|
||||
.carousel__next {
|
||||
top: -85px;
|
||||
width: 100%;
|
||||
justify-content: end;
|
||||
margin: 0;
|
||||
transform: none;
|
||||
display: flex;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.carousel__next {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgb(var(--v-theme-lightprimary));
|
||||
|
||||
}
|
||||
|
||||
.carousel__prev {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
border-radius: 50%;
|
||||
background: rgb(var(--v-theme-lightprimary));
|
||||
right: 65px;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.our-template {
|
||||
.carousel__slide {
|
||||
padding: 0 15px 40px;
|
||||
}
|
||||
|
||||
.carousel__viewport {
|
||||
margin: 0 -106px;
|
||||
}
|
||||
}
|
||||
|
||||
.testimonials {
|
||||
|
||||
.carousel__prev,
|
||||
.carousel__next {
|
||||
width: 100%;
|
||||
justify-content: end;
|
||||
margin: 0;
|
||||
transform: translateY(75px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.carousel__next {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgb(var(--v-theme-background));
|
||||
left: -60%;
|
||||
}
|
||||
|
||||
.carousel__prev {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgb(var(--v-theme-background));
|
||||
right: 65px;
|
||||
left: -74%;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
.slide-counter {
|
||||
position: relative;
|
||||
bottom: -4px;
|
||||
left: 50px;
|
||||
z-index: 2;
|
||||
font-size: 15px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
svg {
|
||||
path {
|
||||
fill: rgb(255, 255, 255);
|
||||
|
||||
&:hover {
|
||||
fill: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.package {
|
||||
.v-list-item {
|
||||
min-height: 35px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lp-faq {
|
||||
.v-expansion-panel-title__icon {
|
||||
.v-icon {
|
||||
font-size: 20px;
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
.v-expansion-panels:not(.v-expansion-panels--variant-accordion)> :first-child:not(:last-child):not(.v-expansion-panel--active):not(.v-expansion-panel--before-active) {
|
||||
border-bottom-left-radius: 8px !important;
|
||||
border-bottom-right-radius: 8px !important;
|
||||
}
|
||||
|
||||
.v-expansion-panels:not(.v-expansion-panels--variant-accordion)> :not(:first-child):not(:last-child):not(.v-expansion-panel--active):not(.v-expansion-panel--after-active) {
|
||||
border-top-left-radius: 8px !important;
|
||||
border-top-right-radius: 8px !important;
|
||||
}
|
||||
|
||||
.v-expansion-panels:not(.v-expansion-panels--variant-accordion)> :not(:first-child):not(:last-child):not(.v-expansion-panel--active):not(.v-expansion-panel--before-active) {
|
||||
border-bottom-left-radius: 8px !important;
|
||||
border-bottom-right-radius: 8px !important;
|
||||
}
|
||||
|
||||
.v-expansion-panel--active:not(:first-child),
|
||||
.v-expansion-panel--active+.v-expansion-panel {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animted-img {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 0%;
|
||||
animation: mover 5s infinite alternate;
|
||||
}
|
||||
|
||||
.animted-img-2 {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: -35px;
|
||||
inset-inline-end: 15px;
|
||||
animation: mover 5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes mover {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel__pagination {
|
||||
.carousel__pagination-button {
|
||||
padding: 6px;
|
||||
|
||||
&::after {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
background-color: rgb(var(--v-theme-textPrimary));
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
background-color: #000;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel__pagination-button--active {
|
||||
&::after {
|
||||
background-color: #000;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.v-btn--size-default {
|
||||
&.nav-links {
|
||||
font-size: $font-size-root !important;
|
||||
|
||||
.v-btn__overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.light-primary {
|
||||
background-color: rgb(var(--v-theme-primary), 0.1);
|
||||
}
|
||||
|
||||
.announce-close {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
|
||||
}
|
||||
|
||||
.text-align-start{
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
@media screen and (max-width:1199px) {
|
||||
.ps-96 {
|
||||
padding-inline-start: 60px !important;
|
||||
}
|
||||
|
||||
.space-p-96 {
|
||||
padding: 55px 0 !important;
|
||||
}
|
||||
|
||||
.pt-96 {
|
||||
padding-top: 55px !important;
|
||||
}
|
||||
|
||||
.offerbar {
|
||||
&:after {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
.offerbar:after,
|
||||
.offerbar:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media screen and (max-width:1024px) {
|
||||
.front-wraper .testimonials .slide-counter {
|
||||
left: 67px;
|
||||
}
|
||||
|
||||
.space-p-96 {
|
||||
padding: 40px 0 !important;
|
||||
}
|
||||
|
||||
.pt-96 {
|
||||
padding-top: 40px !important;
|
||||
}
|
||||
|
||||
.front-wraper .bg-collection {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.front-wraper .our-template .carousel__viewport {
|
||||
margin: 0 0px;
|
||||
}
|
||||
|
||||
.ps-96 {
|
||||
padding-inline-start: 20px !important;
|
||||
padding-inline-end: 20px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media screen and (max-width:991px) {
|
||||
.text-align-start{
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:767px) {
|
||||
.technology {
|
||||
.round-54 {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
|
||||
img {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.front-wraper {
|
||||
.display-2 {
|
||||
font-size: 32px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.display-1 {
|
||||
font-size: 32px;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.front-wraper .leader-slider .carousel__viewport {
|
||||
margin: 0 0px;
|
||||
}
|
||||
|
||||
.announce-close {
|
||||
bottom: 8px;
|
||||
}
|
||||
|
||||
.text-48 {
|
||||
font-size: 30px !important;
|
||||
line-height: 40px !important;
|
||||
}
|
||||
|
||||
.text-56 {
|
||||
font-size: 35px !important;
|
||||
line-height: 40px !important;
|
||||
}
|
||||
|
||||
.team {
|
||||
.intro {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
.front-lp-header {
|
||||
|
||||
.v-toolbar{
|
||||
background: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
&.v-app-bar .v-toolbar__content {
|
||||
padding: 0;
|
||||
|
||||
}
|
||||
|
||||
.v-toolbar__content {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.v-toolbar {
|
||||
background: transparent !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
.v-toolbar {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
&.sticky-header {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
transition: 0.5s;
|
||||
background-color: rgba(var(--v-theme-surface)) !important;
|
||||
box-shadow: 0 4px 29px -11px #3a4b7424 !important;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
//
|
||||
// mega menu
|
||||
//
|
||||
.white-btn{
|
||||
background-color: #769CFF;
|
||||
}
|
||||
.front_wrapper {
|
||||
|
||||
&.v-menu .v-overlay__content {
|
||||
margin: 0 auto;
|
||||
left: 0 !important;
|
||||
right: 0;
|
||||
}
|
||||
.megamenu {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 96%;
|
||||
background-color: rgba(55, 114, 255, 0.2);
|
||||
border-radius: 7px;
|
||||
opacity: 0;
|
||||
}
|
||||
.v-btn {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 100px;
|
||||
opacity: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
&:hover {
|
||||
&::before,
|
||||
.v-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.lp-drawer {
|
||||
&.v-navigation-drawer {
|
||||
top: 0 !important;
|
||||
height: 100% !important;
|
||||
z-index: 1007 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lp-mobile-sidebar {
|
||||
.v-list {
|
||||
.v-list-item__content {
|
||||
overflow: inherit;
|
||||
}
|
||||
}
|
||||
.v-list-group__items .v-list-item {
|
||||
padding-inline-start: 25px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-btn--size-default {
|
||||
&.nav-links {
|
||||
font-size: $font-size-root !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
html {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.v-main{
|
||||
background:rgb(var(--v-theme-background)) !important; ;
|
||||
}
|
||||
@media (max-width: 1279px) {
|
||||
.v-main {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
min-height: calc(100vh - 100px);
|
||||
padding: 24px;
|
||||
// border-radius: $border-radius-root;
|
||||
@media screen and (max-width: 767px) {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.maxWidth {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.fixed-width {
|
||||
max-width: 1300px;
|
||||
}
|
||||
|
||||
.right-pos-img {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
.v-btn.customizer-btn {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
border-radius: 50%;
|
||||
// .icon-tabler-settings {
|
||||
// animation: progress-circular-rotate 1.4s linear infinite;
|
||||
// transform-origin: center center;
|
||||
// transition: all 0.2s ease-in-out;
|
||||
// }
|
||||
}
|
||||
|
||||
.btn-group-custom {
|
||||
&.v-btn-group {
|
||||
height: 66px !important;
|
||||
overflow: unset !important;
|
||||
.v-btn {
|
||||
height: 66px !important;
|
||||
padding: 0 20px;
|
||||
border: 1px solid rgb(var(--v-theme-borderColor), 0.7) !important;
|
||||
transition: all 0.1s ease-in 0s;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
&.text-primary {
|
||||
.v-btn__overlay {
|
||||
background: transparent !important;
|
||||
}
|
||||
.icon {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
fill: rgb(var(--v-theme-primary), 0.2);
|
||||
}
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hover-btns {
|
||||
transition: all 0.1s ease-in 0s;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
// all theme colors
|
||||
.v-avatar.themeBlue,
|
||||
.v-avatar.themeDarkBlue {
|
||||
background: #1e88e5;
|
||||
}
|
||||
.v-avatar.themeAqua,
|
||||
.v-avatar.themeDarkAqua {
|
||||
background: #0074ba;
|
||||
}
|
||||
|
||||
.v-avatar.themePurple,
|
||||
.v-avatar.themeDarkPurple {
|
||||
background: #763ebd;
|
||||
}
|
||||
.v-avatar.themeGreen,
|
||||
.v-avatar.themeDarkGreen {
|
||||
background: #0a7ea4;
|
||||
}
|
||||
|
||||
.v-avatar.themeCyan,
|
||||
.v-avatar.themeDarkCyan {
|
||||
background: #01c0c8;
|
||||
}
|
||||
|
||||
.v-avatar.themeOrange,
|
||||
.v-avatar.themeDarkOrange {
|
||||
background: #fa896b;
|
||||
}
|
||||
|
||||
|
||||
.DARK_BLUE_THEME, .DARK_AQUA_THEME, .DARK_ORANGE_THEME, .DARK_PURPLE_THEME, .DARK_GREEN_THEME, .DARK_CYAN_THEME {
|
||||
.togglethemeBlue {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.togglethemeDarkBlue {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.BLUE_THEME, .AQUA_THEME, .ORANGE_THEME, .PURPLE_THEME, .GREEN_THEME, .CYAN_THEME {
|
||||
.togglethemeDarkBlue {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.togglethemeBlue {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// theme : dark
|
||||
div[class*='v-theme--DARK_'] {
|
||||
.smallCap {
|
||||
color: rgb(var(--v-theme-textSecondary));
|
||||
}
|
||||
|
||||
.elevation-10 {
|
||||
box-shadow: rgb(145 158 171 / 30%) 0px 0px 2px 0px, rgb(145 158 171 / 2%) 0px 12px 24px -4px !important;
|
||||
}
|
||||
.v-field__outline{
|
||||
--v-field-border-opacity: 0.38 !important;
|
||||
}
|
||||
|
||||
.front-wraper{
|
||||
.bg-background{
|
||||
background-color: rgb(var(--v-theme-hoverColor)) !important;
|
||||
}
|
||||
|
||||
.front-dark{
|
||||
&.bg-textPrimary{
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
}
|
||||
}
|
||||
.bg-textPrimary{
|
||||
background-color: rgb(var(--v-theme-textSecondary)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#vector-map .dxm-layers path {
|
||||
fill: #7C8FAC !important;
|
||||
}
|
||||
|
||||
.svgMap-map-wrapper {
|
||||
.svgMap-country {
|
||||
stroke: #878585;
|
||||
fill: #1A2537 !important;
|
||||
|
||||
&#svgMap-map-country-IN {
|
||||
fill: rgb(var(--v-theme-secondary)) !important;
|
||||
}
|
||||
|
||||
&#svgMap-map-country-AF {
|
||||
fill: rgb(var(--v-theme-purple)) !important;
|
||||
}
|
||||
|
||||
&#svgMap-map-country-US {
|
||||
fill: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.svgMap-map-controls-zoom {
|
||||
background: #c9d6de !important;
|
||||
}
|
||||
.svgMap-control-button{
|
||||
background-color: rgb(var(--v-theme-textSecondary)) !important;
|
||||
}
|
||||
|
||||
}
|
||||
.dark-card-title{
|
||||
color:rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
.fc{
|
||||
.fc-button-primary:not(:disabled).fc-button-active {
|
||||
background-color: rgb(var(--v-theme-grey100));
|
||||
}
|
||||
.fc-button-group {
|
||||
>.fc-button {
|
||||
&:hover,&:focus{
|
||||
background-color: rgba(var(--v-theme-grey100));
|
||||
color: #fff;
|
||||
.fc-icon{
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
.horizontalLayout {
|
||||
.v-main {
|
||||
margin: 0 16px !important;
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
margin: 0 10px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-header {
|
||||
&.v-app-bar .v-toolbar__content {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.maxWidth {
|
||||
@media screen and (max-width: 1199px) {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ddMenu {
|
||||
&.ddLevel-1 {
|
||||
.navItem {
|
||||
.navItemLink {
|
||||
.dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: rgb(var(--v-theme-textSecondary));
|
||||
border-radius: 50%;
|
||||
margin-inline-end: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dot {
|
||||
background-color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ddLevel-2 {
|
||||
.navItem {
|
||||
.navItemLink {
|
||||
.dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: rgb(var(--v-theme-textSecondary));
|
||||
border-radius: 50%;
|
||||
margin-inline-end: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dot {
|
||||
background-color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.horizontalMenu {
|
||||
.v-toolbar__content {
|
||||
max-width: 1270px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.navItem:has(.ddMenu.ddLevel-1 li a.router-link-active) {
|
||||
background-color: rgb(var(--v-theme-secondary)) !important;
|
||||
border-radius: 9999px;
|
||||
|
||||
.navcollapse {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
.v-navigation-drawer {
|
||||
margin-top: -70px !important;
|
||||
height: 100vh !important;
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.horizontalMenu {
|
||||
margin-top: 65px;
|
||||
margin-bottom: -70px;
|
||||
|
||||
.maxWidth {
|
||||
.horizontal-navbar {
|
||||
max-width: 1160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-navbar {
|
||||
padding: 16px 0;
|
||||
margin: 0px auto;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
z-index: 11;
|
||||
font-size: 0.875rem;
|
||||
position: relative;
|
||||
|
||||
ul {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.ddMenu {
|
||||
li {
|
||||
a {
|
||||
color: rgb(var(--v-theme-textPrimary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 13px;
|
||||
height: 40px;
|
||||
|
||||
.navIcon {
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ddIcon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
background-color: transparent;
|
||||
color: rgba(var(--v-theme-secondary)) !important;
|
||||
|
||||
.dot {
|
||||
background-color: rgb(var(--v-theme-secondary)) !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.navItem {
|
||||
position: relative;
|
||||
|
||||
.single-link {
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-secondary)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ddMenu {
|
||||
.navItem {
|
||||
.navcollapse {
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-secondary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.ddMenu {
|
||||
position: absolute;
|
||||
width: 230px;
|
||||
display: none;
|
||||
top: 40px;
|
||||
padding: 10px;
|
||||
z-index: 1;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
box-shadow: $box-shadow;
|
||||
border-radius: $border-radius-root;
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.ddLevel-2,
|
||||
.ddLevel-3 {
|
||||
top: -5px;
|
||||
left: 212px;
|
||||
}
|
||||
|
||||
.navItem:hover {
|
||||
|
||||
>.ddMenu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
>li:hover {
|
||||
background-color: rgb(var(--v-theme-lightprimary));
|
||||
border-radius: 9999px;
|
||||
|
||||
>.navItemLink {
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.router-link-exact-active {
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
font-weight: 500;
|
||||
background-color: rgb(var(--v-theme-lightprimary));
|
||||
border-radius: $border-radius-root;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-100vh {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: rgb(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
// border
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
.opacity-1 {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.z-auto.v-card {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.obj-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cursor-move {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
body {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]):not([type="range"]):not([type="color"]),
|
||||
textarea,
|
||||
[contenteditable="true"] {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
//Date time picker
|
||||
input[type="date"],
|
||||
input[type="time"] {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-calendar-picker-indicator,
|
||||
input[type="time"]::-webkit-calendar-picker-indicator {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.upload-btn-wrapper {
|
||||
width: 150px;
|
||||
height: 140px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);
|
||||
|
||||
input[type=file] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.bg-dark{
|
||||
background-color: rgba(0, 0, 0, .08);
|
||||
}
|
||||
.bg-white-opacity{
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@media screen and (max-width:1368px) and (min-width:1200px) {
|
||||
.space-20 {
|
||||
padding: 30px 20px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
.v-locale--is-rtl {
|
||||
.customizer-btn {
|
||||
left: 30px;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.horizontal-navbar .icon-box {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bg-img-1 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: unset !important;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: unset !important;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: unset !important;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mr-sm-2 {
|
||||
margin-right: unset !important;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: unset !important;
|
||||
margin-left: 12px !important;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: unset !important;
|
||||
margin-left: 16px !important;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
|
||||
.mr-auto {
|
||||
margin-left: auto !important;
|
||||
margin-right: unset !important;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.ml-sm-4 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.ml-md-4 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.ml-sm-3 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
|
||||
.ml-5 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: unset !important;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: unset !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: unset !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
|
||||
.pr-4 {
|
||||
padding-left: 16px !important;
|
||||
padding-right: unset !important;
|
||||
}
|
||||
|
||||
.pl-4 {
|
||||
padding-left: unset !important;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
|
||||
.right-pos-img {
|
||||
right: unset;
|
||||
left: 0;
|
||||
transform: scaleX(-1);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.badg-dotDetail {
|
||||
left: 0;
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.text-sm-right,
|
||||
.text-md-right {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.text-sm-left {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.ml-auto,
|
||||
.ml-sm-auto {
|
||||
margin-left: unset !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.vertical-table .v-table>.v-table__wrapper>table>tbody>tr>th {
|
||||
border-left: thin solid rgba(var(--v-border-color), 1) !important;
|
||||
}
|
||||
|
||||
.authentication .auth-header {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.horizontal-navbar li a {
|
||||
padding: 10px 13px;
|
||||
}
|
||||
|
||||
.horizontal-navbar {
|
||||
li {
|
||||
margin-right: 0;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// &.v-menu .v-overlay__content,
|
||||
// &.search_popup .v-overlay__content,
|
||||
// &.language_dropdown .v-overlay__content,
|
||||
// &.notification_popup .v-overlay__content,
|
||||
// &.profile_popup .v-overlay__content {
|
||||
// left: inherit;
|
||||
// }
|
||||
|
||||
.related-Product {
|
||||
.carousel__prev.navarrow {
|
||||
top: 0;
|
||||
right: unset !important;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.carousel__next.navarrow {
|
||||
top: 0;
|
||||
right: unset !important;
|
||||
left: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
//RTL mode minisidebar hover to active scrollbar
|
||||
.ps--active-y>.ps__rail-y {
|
||||
right: unset !important;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
//RTL mode sidebar scrollbar on right side
|
||||
.left-customizer {
|
||||
.ps__rail-y {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-navbar .ddMenu {
|
||||
padding: 10px 15px 10px 0px;
|
||||
}
|
||||
|
||||
.v-list-group__items {
|
||||
.iconClass {
|
||||
position: relative;
|
||||
left: unset;
|
||||
right: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
|
||||
.horizontal-navbar .ddLevel-2,
|
||||
.horizontal-navbar .ddLevel-3 {
|
||||
top: -5px;
|
||||
right: 212px;
|
||||
}
|
||||
|
||||
.horizontal-navbar li a .navIcon {
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.leftSidebar .profile-name h5 {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.horizontal-navbar {
|
||||
.ddMenu {
|
||||
.navItemLink {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:1279px) {
|
||||
.mini-sidebar {
|
||||
.v-navigation-drawer.v-navigation-drawer--right {
|
||||
width: 270px !important;
|
||||
}
|
||||
|
||||
.v-navigation-drawer.v-navigation-drawer--left {
|
||||
width: 320px !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.rtlImg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.marquee1-group {
|
||||
animation: marquee-rtl 45s linear infinite;
|
||||
}
|
||||
|
||||
.marquee2-group {
|
||||
animation: marquee2-rtl 45s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes marquee-rtl {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(2086px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee2-rtl {
|
||||
0% {
|
||||
transform: translate3d(2086px, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.front-wraper {
|
||||
.testimonials {
|
||||
.slide-counter {
|
||||
left: -50px;
|
||||
}
|
||||
|
||||
.carousel__prev {
|
||||
right: -74%;
|
||||
left: unset;
|
||||
|
||||
.rtlnav {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel__next {
|
||||
right: -60%;
|
||||
left: unset;
|
||||
|
||||
.rtlnav {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//For Rtl Chart
|
||||
.rtl-me-n7 {
|
||||
margin-inline-start: -28px !important;
|
||||
margin-inline-end: unset !important;
|
||||
}
|
||||
|
||||
.profile-img::before {
|
||||
left: unset;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
@use "../variables" as *;
|
||||
|
||||
/*This is for the logo*/
|
||||
.leftSidebar {
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, .03), 0 0 1px 0 rgba(0, 0, 0, .1);
|
||||
|
||||
.logo {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.mini-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mini-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile {
|
||||
background: url("@/assets/images/backgrounds/user-info.jpg") no-repeat;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
margin-top: -6px;
|
||||
height: 35px;
|
||||
|
||||
h5 {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list--density-default .v-list-subheader {
|
||||
padding-inline-start: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.verticalLayout {
|
||||
.logo {
|
||||
width: 250px;
|
||||
|
||||
@media screen and (max-width:1024px) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*This is for the Vertical sidebar*/
|
||||
.scrollnavbar {
|
||||
height: 100%;
|
||||
|
||||
.userbottom {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.smallCap {
|
||||
padding: 3px 12px 12px 0px !important;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-top: 24px;
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*General Menu css*/
|
||||
.v-list-group__items .v-list-item,
|
||||
.v-list-item {
|
||||
border-radius: $border-radius-root;
|
||||
padding-inline-start: calc(14px + var(--indent-padding) / 10) !important;
|
||||
|
||||
margin: 0 0 2px;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.v-list-item__prepend {
|
||||
margin-inline-end: 13px;
|
||||
}
|
||||
|
||||
.v-list-item__append {
|
||||
font-size: 0.875rem;
|
||||
|
||||
.v-icon {
|
||||
margin-inline-start: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item-title {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-group__items {
|
||||
.v-list-item {
|
||||
min-height: 35px !important;
|
||||
padding-inline-start: calc(12px + var(--indent-padding) / 10) !important;
|
||||
|
||||
.v-list-item__prepend .dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: rgb(var(--v-theme-textSecondary));
|
||||
border-radius: 50%;
|
||||
margin-inline-end: 8px !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.v-list-item-title {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
|
||||
.v-list-item__prepend .dot {
|
||||
background-color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
&.v-list-item--active {
|
||||
.v-list-item__prepend .dot {
|
||||
background-color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*This is for the dropdown*/
|
||||
.v-list {
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
|
||||
>.v-list-item.v-list-item--active,
|
||||
.v-list-item--active>.v-list-item__overlay {
|
||||
background: rgb(var(--v-theme-secondary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
>.v-list-group {
|
||||
position: relative;
|
||||
|
||||
>.v-list-item--active,
|
||||
>.v-list-item--active:hover {
|
||||
background: rgb(var(--v-theme-secondary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.v-list-group__items .v-list-item.v-list-item--active,
|
||||
.v-list-group__items .v-list-item.v-list-item--active>.v-list-item__overlay {
|
||||
background: transparent;
|
||||
color: rgb(var(--v-theme-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-navigation-drawer--rail {
|
||||
|
||||
.scrollnavbar .v-list .v-list-group__items,
|
||||
.hide-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.leftPadding {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1170px) {
|
||||
.mini-sidebar {
|
||||
.logo {
|
||||
width: 40px;
|
||||
overflow: hidden;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.profile-logout {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.scrollnavbar {
|
||||
.smallCap {
|
||||
padding: 3px 12px 12px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.leftSidebar .v-list--density-default .v-list-subheader {
|
||||
padding-inline-start: 15px !important;
|
||||
}
|
||||
|
||||
.mini-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebarchip.hide-menu {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mini-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-list {
|
||||
padding: 14px !important;
|
||||
}
|
||||
|
||||
.v-list-group__items {
|
||||
.iconClass {
|
||||
position: relative;
|
||||
left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.leftSidebar:hover {
|
||||
box-shadow: $box-shadow !important;
|
||||
|
||||
.mini-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebarchip.hide-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mini-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-logout {
|
||||
opacity: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.scrollnavbar {
|
||||
.smallCap {
|
||||
padding: 3px 12px 12px 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-group__items {
|
||||
.iconClass {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list--density-default .v-list-subheader {
|
||||
padding-inline-start: 0px !important;
|
||||
}
|
||||
.v-list-group__items {
|
||||
.v-list-item {
|
||||
.v-list-item__prepend .dot {
|
||||
opacity:0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-navigation-drawer--expand-on-hover:hover {
|
||||
.logo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.v-list .v-list-group__items,
|
||||
.hide-menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
margin-left: 0;
|
||||
|
||||
&::before {
|
||||
left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.v-list-group__items {
|
||||
.v-list-item {
|
||||
.v-list-item__prepend .dot {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// scrollbar
|
||||
.ps__rail-y {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
margin-left: 14px;
|
||||
|
||||
&::before {
|
||||
-webkit-animation: 2.5s blow 0s linear infinite;
|
||||
animation: 2.5s blow 0s linear infinite;
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
top: 40px;
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
left: 26px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes blow {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1, 1, 0.5);
|
||||
transform: scale3d(1, 1, 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1, 1, 0.5);
|
||||
transform: scale3d(1, 1, 0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0;
|
||||
-webkit-transform: scale3d(1, 1, 0.5);
|
||||
transform: scale3d(1, 1, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blow {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1, 1, 0.5);
|
||||
transform: scale3d(1, 1, 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1, 1, 0.5);
|
||||
transform: scale3d(1, 1, 0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0;
|
||||
-webkit-transform: scale3d(1, 1, 0.5);
|
||||
transform: scale3d(1, 1, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
$sizes: (
|
||||
'display-1': 44px,
|
||||
'display-2': 40px,
|
||||
'display-3': 30px,
|
||||
'h1': 36px,
|
||||
'h2': 30px,
|
||||
'h3': 21px,
|
||||
'h4': 18px,
|
||||
'h5': 16px,
|
||||
'h6': 14px,
|
||||
'text-10': 10px,
|
||||
'text-12': 12px,
|
||||
'text-13': 13px,
|
||||
'text-14': 14px,
|
||||
'text-15': 15px,
|
||||
'text-16': 16px,
|
||||
'text-17': 17px,
|
||||
'text-18': 18px,
|
||||
'text-20': 20px,
|
||||
'text-22': 22px,
|
||||
'text-24': 24px,
|
||||
'text-28': 28px,
|
||||
'text-34': 34px,
|
||||
'text-40': 40px,
|
||||
'text-44': 44px,
|
||||
'text-48': 48px,
|
||||
'text-50': 50px,
|
||||
'text-52': 52px,
|
||||
'text-56': 56px,
|
||||
'text-64': 64px,
|
||||
'body-text-1': 10px
|
||||
);
|
||||
|
||||
@each $pixel, $size in $sizes {
|
||||
.#{$pixel} {
|
||||
font-size: $size;
|
||||
line-height: $size + 10;
|
||||
}
|
||||
}
|
||||
|
||||
$height: (
|
||||
'h-10': 10px,
|
||||
'h-12': 12px,
|
||||
'h-15': 15px,
|
||||
);
|
||||
|
||||
@each $pixel, $size in $height {
|
||||
.#{$pixel} {
|
||||
height: $size;
|
||||
width: $size ;
|
||||
}
|
||||
}
|
||||
|
||||
.textSecondary {
|
||||
color: rgb(var(--v-theme-textSecondary)) !important;
|
||||
}
|
||||
|
||||
.textPrimary {
|
||||
color: rgb(var(--v-theme-textPrimary)) !important;
|
||||
}
|
||||
|
||||
// line height
|
||||
|
||||
.lh-md {
|
||||
line-height: 1.57;
|
||||
}
|
||||
.lh-normal{
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.font-weight-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// hover text
|
||||
.text-hover-primary {
|
||||
color: rgb(var(--v-theme-textPrimary));
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: rgb(var(--v-theme-textSecondary));
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
|
||||
.hover-primary {
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
.v-app-bar {
|
||||
.v-toolbar__content {
|
||||
padding: 0 15px;
|
||||
> .v-btn:first-child {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
.v-btn {
|
||||
color: rgba(var(--v-theme-textsurface)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-text-primary {
|
||||
&.v-list-item:hover > .v-list-item__overlay {
|
||||
display: none;
|
||||
}
|
||||
.custom-title {
|
||||
color: rgb(var(--v-theme-textPrimary)) !important;
|
||||
}
|
||||
&:hover {
|
||||
.custom-title {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width:1279px) {
|
||||
.mini-sidebar {
|
||||
.v-navigation-drawer.v-navigation-drawer--left {
|
||||
width: 270px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.notify {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
right: -8px;
|
||||
|
||||
.heartbit {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -2px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
z-index: 10;
|
||||
border: 2px solid rgb(var(--v-theme-error));
|
||||
border-radius: 70px;
|
||||
animation: heartbit 1s ease-out;
|
||||
-moz-animation: heartbit 1s ease-out;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-o-animation: heartbit 1s ease-out;
|
||||
-o-animation-iteration-count: infinite;
|
||||
-webkit-animation: heartbit 1s ease-out;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.point {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 30px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 2px;
|
||||
background-color: rgb(var(--v-theme-error));
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartbit {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(0.1);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.v-menu.mobile_popup .v-overlay__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.main-head{
|
||||
&.v-app-bar .v-toolbar__content{
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
//
|
||||
// common
|
||||
//
|
||||
|
||||
.inside-left-sidebar {
|
||||
.left-part {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//Full Calendar
|
||||
.fc {
|
||||
.fc-button-group {
|
||||
>.fc-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px 10px;
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
background-color: rgba(var(--v-theme-grey200));
|
||||
color: rgba(var(--v-theme-textPrimary));
|
||||
.fc-icon{
|
||||
color: rgba(var(--v-theme-textPrimary));
|
||||
font-size: 20px;
|
||||
}
|
||||
&:hover,&:focus{
|
||||
background-color: rgba(var(--v-theme-textPrimary));
|
||||
color: #fff;
|
||||
.fc-icon{
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fc-button-primary:not(:disabled):active{
|
||||
background-color: rgba(var(--v-theme-textPrimary));
|
||||
}
|
||||
.fc-button-primary:not(:disabled).fc-button-active {
|
||||
background-color: rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
}
|
||||
.fc-today-button{
|
||||
padding: 7px 20px;
|
||||
border: 0;
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.fc-prev-button{
|
||||
border-radius: 30px 0 0 30px;
|
||||
}
|
||||
.fc-toolbar-title{
|
||||
font-weight: 600;
|
||||
}
|
||||
.fc-event-title{
|
||||
font-size: 14px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.fc-button {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
|
||||
.fc-icon {
|
||||
font-size: 1.5em;
|
||||
vertical-align: unset;
|
||||
}
|
||||
}
|
||||
.fc-daygrid-day-number{
|
||||
color: rgba(var(--v-theme-textSecondary));
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.fc-button-primary {
|
||||
background: rgb(var(--v-theme-primary));
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
&:not(:disabled).fc-button-active {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&:active {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-col-header-cell-cushion {
|
||||
display: inline-block;
|
||||
padding: 10px 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.fc-button-primary:not(:disabled).fc-button-active{
|
||||
background-color: rgb(var(--v-theme-textPrimary));
|
||||
}
|
||||
}
|
||||
|
||||
.fc-theme-standard {
|
||||
td {
|
||||
border: 1px solid rgba(var(--v-border-color), 1) !important;
|
||||
}
|
||||
|
||||
th {
|
||||
border: 1px solid rgba(var(--v-border-color), 1) !important;
|
||||
border-bottom: 0 !important;
|
||||
background-color: rgba(var(--v-theme-grey200));
|
||||
height: 56px;
|
||||
vertical-align: middle;
|
||||
color: rgba(var(--v-theme-textSecondary));
|
||||
}
|
||||
|
||||
.fc-scrollgrid {
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-h-event {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fc-direction-ltr {
|
||||
.fc-button-group {
|
||||
>.fc-button {
|
||||
&:not(:last-child) {
|
||||
border-bottom-left-radius: 30px;
|
||||
border-top-left-radius: 30px;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-bottom-right-radius: 30px;
|
||||
border-top-right-radius: 30px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-button-group {
|
||||
.fc-dayGridMonth-button {
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-top-right-radius: 0px !important;
|
||||
padding: 7px 20px !important;
|
||||
}
|
||||
|
||||
.fc-timeGridDay-button {
|
||||
border-bottom-left-radius: 0px !important;
|
||||
border-top-left-radius: 0px !important;
|
||||
padding: 7px 20px !important;
|
||||
}
|
||||
|
||||
.fc-timeGridWeek-button {
|
||||
border-radius: 0 !important;
|
||||
padding: 7px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-today-button {
|
||||
border-radius: 30px !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media screen and (max-width:600px) {
|
||||
.fc {
|
||||
.fc-toolbar {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-toolbar-chunk {
|
||||
.fc-toolbar-title {
|
||||
margin: 15px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customTab {
|
||||
.v-btn {
|
||||
&.v-tab-item--selected {
|
||||
background-color: rgb(var(--v-theme-lightprimary)) !important;
|
||||
|
||||
.icon {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-items {
|
||||
padding: 0px 24px 12px;
|
||||
|
||||
&.selected-email {
|
||||
.email-title {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(var(--v-theme-hoverColor));
|
||||
|
||||
.email-title {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.email-content {
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
.authentication{
|
||||
&::before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0.3;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(rgb(210, 241, 223), rgb(211, 215, 250), rgb(186, 216, 244)) 0% 0% / 400% 400%;
|
||||
}
|
||||
|
||||
@media screen and (max-width:1280px){
|
||||
.auth-header{
|
||||
position: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
.mw-450{
|
||||
max-width: 450px;
|
||||
width: 100%;;
|
||||
}
|
||||
.auth-header{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.verification{
|
||||
.v-field__input{
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.auth-divider{
|
||||
span{
|
||||
z-index: 1;
|
||||
}
|
||||
&::before{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-top: thin solid rgb(var(--v-theme-borderColor));
|
||||
top: 50%;
|
||||
content: "";
|
||||
transform: translateY(50%);
|
||||
left: 0;
|
||||
}
|
||||
&::after
|
||||
{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-top: thin solid rgb(var(--v-theme-borderColor));
|
||||
top: 50%;
|
||||
content: "";
|
||||
transform: translateY(50%);
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px){
|
||||
.auth{
|
||||
.v-col-lg-7{
|
||||
flex: 0 0 66.66%;
|
||||
max-width: 66.66%;
|
||||
}
|
||||
.v-col-lg-5{
|
||||
flex: 0 0 33.33%;
|
||||
max-width: 33.33%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@media screen and (max-width:1280px){
|
||||
.mh-100{
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:600px){
|
||||
.mw-100{
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth{
|
||||
.v-switch .v-label, .v-checkbox .v-label {
|
||||
opacity: 0.7;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
.month-table {
|
||||
&.custom-px-0 {
|
||||
thead {
|
||||
tr {
|
||||
th:first-child {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr.month-item {
|
||||
td:first-child {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr.month-item {
|
||||
td {
|
||||
padding-top: 16px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
tr.month-item-0 {
|
||||
td {
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
tr.month-item-hover {
|
||||
|
||||
td {
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
}
|
||||
|
||||
border-left: 4px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-left: 4px solid rgba(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.no-line {
|
||||
|
||||
.v-table .v-table__wrapper>table>tbody>tr:not(:last-child)>td {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.v-table .v-table__wrapper>table>tbody>tr>td {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-transaction {
|
||||
.line {
|
||||
width: 2px;
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chip-label {
|
||||
width: 80px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
//
|
||||
// Apex Chart
|
||||
//
|
||||
|
||||
body {
|
||||
.apexcharts-tooltip {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-marker {
|
||||
border-radius: 4px;
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip.apexcharts-theme-dark {
|
||||
background: rgba(17, 28, 45, 0.8);
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
border-bottom: 0;
|
||||
background: rgba(17, 28, 45, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-activity {
|
||||
|
||||
.v-tab {
|
||||
|
||||
&.v-btn {
|
||||
border: 1px dashed rgba(var(--v-theme-borderColor));
|
||||
border-radius: 8px;
|
||||
min-width: 90px;
|
||||
overflow: hidden;
|
||||
padding: 15px 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.v-btn__content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.v-tab--selected{
|
||||
border: 1px solid rgba(var(--v-theme-borderColor));
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
.v-tab__slider{
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.v-slide-group__content {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-borderColor));
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.comment-action {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.comment-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-borderColor));
|
||||
padding-bottom: 25px;
|
||||
margin-bottom: 25px;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.progress-cards {
|
||||
border-inline-end: 1px solid rgba(var(--v-theme-borderColor));
|
||||
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width:991px) {
|
||||
border-inline-end: 0 !important
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-borderColor));
|
||||
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.earning-cards {
|
||||
@media screen and (max-width:991px){
|
||||
.w-25{
|
||||
width: 100% !important;
|
||||
&.border-e{
|
||||
border: 0 !important;
|
||||
}
|
||||
.mobile-border{
|
||||
border-bottom:1px solid rgba(var(--v-theme-borderColor));
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user