Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,442 @@
<template>
<VCard class="h-100 overflow-visible position-relative" :style="cardBackgroundStyle">
<VCardText class="pa-6 h-100 d-flex flex-column">
<div class="d-flex justify-center align-center mb-4 flex-wrap gap-2">
<VChip color="primary" variant="tonal" size="small">
<VIcon icon="tabler-calendar" size="14" class="me-1" />
{{ getCurrentDate() }}
</VChip>
<VChip color="secondary" variant="outlined" size="small" v-if="chartName">
<VIcon icon="tabler-chart-line" size="14" class="me-1" />
{{ chartName }}
</VChip>
</div>
<div class="text-center mb-6">
<div class="progress-circle-container mb-4">
<VProgressCircular
:model-value="completionPercent"
:size="120"
:width="8"
color="success"
class="progress-main"
>
<div class="text-center">
<h3 class="text-h3 font-weight-bold">{{ completionPercent }}%</h3>
<span class="text-caption text-medium-emphasis">Completed</span>
</div>
</VProgressCircular>
</div>
</div>
<div class="mb-4">
<VBtn
block
color="primary"
variant="elevated"
size="large"
class="btn-analyze"
:loading="loading"
@click="onAnalyzeClick"
>
<VIcon
:icon="loading ? 'tabler-loader-2' : 'tabler-chart-bar'"
class="me-2"
:class="{ 'rotating': loading }"
/>
{{ loading ? "Analyzing..." : "Analyze" }}
</VBtn>
</div>
<div class="flex-grow-1 d-flex flex-column" style="min-height: 0; height: 250px;">
<VCard
variant="tonal"
color="surface"
class="analysis-container d-flex flex-column h-100"
>
<VCardText class="pa-4 d-flex flex-column h-100" style="min-height: 0;">
<div class="d-flex align-center mb-3">
<VIcon icon="tabler-report-analytics" class="me-2" />
<h6 class="text-h6">Analysis Results {{ chartName ? `- ${chartName}` : '' }}</h6>
</div>
<div
class="analysis-content custom-scrollbar"
style="overflow-y: auto; height: 1px; flex: 1; color: white;"
v-html="analysisResult"
></div>
</VCardText>
</VCard>
</div>
</VCardText>
</VCard>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
// Define props
const props = defineProps({
chartName: {
type: String,
default: '',
required: false
}
})
const vuetifyTheme = useTheme()
const loading = ref(false)
const completionPercent = ref(25)
const analysisResult = ref(`
<div class="text-center text-medium-emphasis py-8">
<div class="mb-3">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 3v18h18"/>
<path d="M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"/>
</svg>
</div>
<p class="mb-0">Click 'Analyze ${props.chartName || 'Projects'}' to generate detailed insights</p>
</div>
`)
const getCurrentDate = () => {
const today = new Date()
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}
return today.toLocaleDateString('en-US', options)
}
const cardBackgroundStyle = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
const createGradientColor = (color, opacity = 0.08) =>
color.startsWith('#') ? `rgba(${hexToRgb(color)}, ${opacity})` : `rgba(${hexToRgb(color)}, ${opacity})`
return {
background: `linear-gradient(135deg,
${createGradientColor(currentTheme.primary, 0.12)} 0%,
${createGradientColor(currentTheme.success, 0.06)} 50%,
${createGradientColor(currentTheme.primary, 0.04)} 100%)`
}
})
function onAnalyzeClick() {
loading.value = true
analysisResult.value = `
<div class="text-center py-4">
<div class="mb-3">
<svg class="rotating" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 11-6.219-8.56"/>
</svg>
</div>
<p class="text-medium-emphasis">Analyzing ${props.chartName || 'project'} data...</p>
</div>
`
setTimeout(() => {
loading.value = false
analysisResult.value = `
<div class="analysis-results">
<div class="result-section mb-4">
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
<span class="result-icon success me-2">📊</span>
${props.chartName || 'Project'} Overview
</h6>
<div class="result-grid">
<div class="result-item">
<span class="result-label">Avg. Completion Time</span>
<span class="result-value text-primary">14 days</span>
</div>
<div class="result-item">
<span class="result-label">Success Rate</span>
<span class="result-value text-success">85%</span>
</div>
</div>
</div>
<div class="result-section mb-4">
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
<span class="result-icon warning me-2">📈</span>
Current Status
</h6>
<div class="status-list">
<div class="status-item">
<div class="status-dot success"></div>
<span>Completed ${props.chartName ? props.chartName.toLowerCase() : 'projects'}: <strong>1</strong></span>
</div>
<div class="status-item">
<div class="status-dot warning"></div>
<span>Pending ${props.chartName ? props.chartName.toLowerCase() : 'projects'}: <strong>2</strong></span>
</div>
<div class="status-item">
<div class="status-dot primary"></div>
<span>Average Progress: <strong>40%</strong></span>
</div>
</div>
</div>
<div class="result-section mb-4">
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
<span class="result-icon info me-2">💡</span>
Recommendations
</h6>
<ul class="recommendation-list">
<li>Focus on completing pending ${props.chartName ? props.chartName.toLowerCase() : 'projects'}</li>
<li>Optimize resource allocation for better efficiency</li>
<li>Set up automated progress tracking</li>
<li>Review ${props.chartName ? props.chartName.toLowerCase() : 'project'} timelines and deadlines</li>
<li>Implement better team communication</li>
<li>Consider hiring additional resources</li>
</ul>
</div>
<div class="result-section mb-4">
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
<span class="result-icon me-2">📅</span>
Detailed Timeline
</h6>
<div class="timeline-items">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<strong>Alpha ${props.chartName || 'Project'}</strong> - Started 10 days ago, 80% complete
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<strong>Beta ${props.chartName || 'Project'}</strong> - Started 5 days ago, 30% complete
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<strong>Gamma ${props.chartName || 'Project'}</strong> - Starting next week, 0% complete
</div>
</div>
</div>
</div>
<div class="result-section">
<h6 class="text-subtitle-1 mb-3 d-flex align-center">
<span class="result-icon me-2">⚡</span>
Performance Metrics
</h6>
<div class="metrics-grid">
<div class="metric-item">
<span class="metric-label">Tasks per Day</span>
<span class="metric-value">3.2</span>
</div>
<div class="metric-item">
<span class="metric-label">Efficiency Score</span>
<span class="metric-value">92%</span>
</div>
<div class="metric-item">
<span class="metric-label">Team Velocity</span>
<span class="metric-value">High</span>
</div>
<div class="metric-item">
<span class="metric-label">Quality Score</span>
<span class="metric-value">8.7/10</span>
</div>
</div>
</div>
</div>
`
}, 2000)
}
</script>
<style scoped>
.progress-circle-container {
position: relative;
display: inline-block;
}
.floating-stats {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
}
.stat-chip {
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.btn-analyze {
box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.3);
transition: all 0.3s ease;
}
.btn-analyze:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(var(--v-theme-primary), 0.4);
}
.analysis-container {
backdrop-filter: blur(10px);
border: 1px solid rgba(var(--v-border-color), 0.1);
}
.analysis-content {
scrollbar-width: thin;
scrollbar-color: rgba(var(--v-theme-primary), 0.3) transparent;
color: white !important;
}
.analysis-content * {
color: white !important;
}
.analysis-content .text-medium-emphasis {
color: rgba(255, 255, 255, 0.7) !important;
}
.analysis-content .text-primary {
color: rgb(var(--v-theme-primary)) !important;
}
.analysis-content .text-success {
color: rgb(var(--v-theme-success)) !important;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(var(--v-theme-primary), 0.3);
border-radius: 4px;
}
.rotating {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.result-section {
background: rgba(var(--v-theme-surface), 0.5);
border-radius: 8px;
padding: 16px;
border: 1px solid rgba(var(--v-border-color), 0.1);
}
.result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.result-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.result-label {
font-size: 0.75rem;
opacity: 0.7;
}
.result-value {
font-weight: 600;
font-size: 0.875rem;
}
.status-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.success { background-color: rgb(var(--v-theme-success)); }
.status-dot.warning { background-color: rgb(var(--v-theme-warning)); }
.status-dot.primary { background-color: rgb(var(--v-theme-primary)); }
.result-icon {
font-size: 1.2rem;
}
.recommendation-list {
margin: 0;
padding-left: 16px;
}
.recommendation-list li {
margin-bottom: 6px;
font-size: 0.875rem;
opacity: 0.8;
}
.timeline-items {
display: flex;
flex-direction: column;
gap: 12px;
}
.timeline-item {
display: flex;
align-items: center;
gap: 12px;
}
.timeline-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgb(var(--v-theme-primary));
flex-shrink: 0;
}
.timeline-content {
font-size: 0.875rem;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.metric-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric-label {
font-size: 0.75rem;
opacity: 0.7;
}
.metric-value {
font-weight: 600;
font-size: 0.875rem;
color: rgb(var(--v-theme-primary));
}
</style>

View File

@@ -0,0 +1,63 @@
<script setup>
const bufferValue = ref(20)
const progressValue = ref(10)
const isFallbackState = ref(false)
const interval = ref()
const showProgress = ref(false)
watch([
progressValue,
isFallbackState,
], () => {
if (progressValue.value > 80 && isFallbackState.value)
progressValue.value = 82
startBuffer()
})
function startBuffer() {
clearInterval(interval.value)
interval.value = setInterval(() => {
progressValue.value += Math.random() * (15 - 5) + 5
bufferValue.value += Math.random() * (15 - 5) + 6
}, 800)
}
const fallbackHandle = () => {
showProgress.value = true
progressValue.value = 10
isFallbackState.value = true
startBuffer()
}
const resolveHandle = () => {
isFallbackState.value = false
progressValue.value = 100
setTimeout(() => {
clearInterval(interval.value)
progressValue.value = 0
bufferValue.value = 20
showProgress.value = false
}, 300)
}
defineExpose({
fallbackHandle,
resolveHandle,
})
</script>
<template>
<!-- loading state via #fallback slot -->
<div
v-if="showProgress"
class="position-fixed"
style="z-index: 9999; inset-block-start: 0; inset-inline: 0 0;"
>
<VProgressLinear
v-model="progressValue"
:buffer-value="bufferValue"
color="primary"
height="2"
bg-color="background"
/>
</div>
</template>

View File

@@ -0,0 +1,278 @@
<script setup>
import safeBoxWithGoldenCoin from '@images/misc/3d-safe-box-with-golden-dollar-coins.png'
import spaceRocket from '@images/misc/3d-space-rocket-with-smoke.png'
import dollarCoinPiggyBank from '@images/misc/dollar-coins-flying-pink-piggy-bank.png'
const props = defineProps({
title: {
type: String,
required: false,
},
xs: {
type: [
Number,
String,
],
required: false,
},
sm: {
type: [
Number,
String,
],
required: false,
},
md: {
type: [
String,
Number,
],
required: false,
},
lg: {
type: [
String,
Number,
],
required: false,
},
xl: {
type: [
String,
Number,
],
required: false,
},
})
const annualMonthlyPlanPriceToggler = ref(true)
const pricingPlans = [
{
name: 'Basic',
tagLine: 'A simple start for everyone',
logo: dollarCoinPiggyBank,
monthlyPrice: 0,
yearlyPrice: 0,
isPopular: false,
current: true,
features: [
'100 responses a month',
'Unlimited forms and surveys',
'Unlimited fields',
'Basic form creation tools',
'Up to 2 subdomains',
],
},
{
name: 'Standard',
tagLine: 'For small to medium businesses',
logo: safeBoxWithGoldenCoin,
monthlyPrice: 49,
yearlyPrice: 499,
isPopular: true,
current: false,
features: [
'Unlimited responses',
'Unlimited forms and surveys',
'Instagram profile page',
'Google Docs integration',
'Custom “Thank you” page',
],
},
{
name: 'Enterprise',
tagLine: 'Solution for big organizations',
logo: spaceRocket,
monthlyPrice: 99,
yearlyPrice: 999,
isPopular: false,
current: false,
features: [
'PayPal payments',
'Logic Jumps',
'File upload with 5GB storage',
'Custom domain support',
'Stripe integration',
],
},
]
</script>
<template>
<!-- 👉 Title and subtitle -->
<div class="text-center">
<h3 class="text-h3 pricing-title mb-2">
{{ props.title ? props.title : 'Pricing Plans' }}
</h3>
<p class="mb-0">
All plans include 40+ advanced tools and features to boost your product.
</p>
<p class="mb-2">
Choose the best plan to fit your needs.
</p>
</div>
<!-- 👉 Annual and monthly price toggler -->
<div class="d-flex font-weight-medium text-body-1 align-center justify-center mx-auto mt-12 mb-6">
<VLabel
for="pricing-plan-toggle"
class="me-3"
>
Monthly
</VLabel>
<div class="position-relative">
<VSwitch
id="pricing-plan-toggle"
v-model="annualMonthlyPlanPriceToggler"
>
<template #label>
<div class="text-body-1 font-weight-medium">
Annually
</div>
</template>
</VSwitch>
<div class="save-upto-chip position-absolute align-center d-none d-md-flex gap-1">
<VIcon
icon="tabler-corner-left-down"
size="24"
class="flip-in-rtl mt-2 text-disabled"
/>
<VChip
label
color="primary"
size="small"
>
Save up to 10%
</VChip>
</div>
</div>
</div>
<!-- SECTION pricing plans -->
<VRow>
<VCol
v-for="plan in pricingPlans"
:key="plan.logo"
v-bind="props"
cols="12"
>
<!-- 👉 Card -->
<VCard
flat
border
:class="plan.isPopular ? 'border-primary border-opacity-100' : ''"
>
<VCardText
style="block-size: 3.75rem;"
class="text-end"
>
<!-- 👉 Popular -->
<VChip
v-show="plan.isPopular"
label
color="primary"
size="small"
>
Popular
</VChip>
</VCardText>
<!-- 👉 Plan logo -->
<VCardText>
<VImg
:height="120"
:width="120"
:src="plan.logo"
class="mx-auto mb-5"
/>
<!-- 👉 Plan name -->
<h4 class="text-h4 mb-1 text-center">
{{ plan.name }}
</h4>
<p class="mb-0 text-body-1 text-center">
{{ plan.tagLine }}
</p>
<!-- 👉 Plan price -->
<div class="position-relative">
<div class="d-flex justify-center pt-5 pb-10">
<div class="text-body-1 align-self-start font-weight-medium">
$
</div>
<h1 class="text-h1 font-weight-medium text-primary">
{{ annualMonthlyPlanPriceToggler ? Math.floor(Number(plan.yearlyPrice) / 12) : plan.monthlyPrice }}
</h1>
<div class="text-body-1 font-weight-medium align-self-end">
/month
</div>
</div>
<!-- 👉 Annual Price -->
<span
v-show="annualMonthlyPlanPriceToggler"
class="annual-price-text position-absolute text-caption text-disabled pb-4"
>
{{ plan.yearlyPrice === 0 ? 'free' : `USD ${plan.yearlyPrice}/Year` }}
</span>
</div>
<!-- 👉 Plan features -->
<VList class="card-list mb-4">
<VListItem
v-for="feature in plan.features"
:key="feature"
>
<template #prepend>
<VIcon
size="8"
icon="tabler-circle-filled"
color="rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))"
/>
</template>
<VListItemTitle class="text-body-1">
{{ feature }}
</VListItemTitle>
</VListItem>
</VList>
<!-- 👉 Plan actions -->
<VBtn
block
:color="plan.current ? 'success' : 'primary'"
:variant="plan.isPopular ? 'elevated' : 'tonal'"
:to="{ name: 'front-pages-payment' }"
:active="false"
>
{{ plan.yearlyPrice === 0 ? 'Your Current Plan' : 'Upgrade' }}
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- !SECTION -->
</template>
<style lang="scss" scoped>
.card-list {
--v-card-list-gap: 1rem;
}
.save-upto-chip {
inset-block-start: -2.4rem;
inset-inline-end: -6rem;
}
.annual-price-text {
inset-block-end: 3%;
inset-inline-start: 50%;
transform: translateX(-50%);
}
</style>

View File

@@ -0,0 +1,92 @@
<script setup>
import AppSearchHeaderBg from '@images/pages/app-search-header-bg.png'
const props = defineProps({
title: {
type: String,
required: false,
},
subtitle: {
type: String,
required: false,
},
customClass: {
type: String,
required: false,
},
placeholder: {
type: String,
required: false,
},
density: {
type: String,
required: false,
default: 'comfortable',
},
isReverse: {
type: Boolean,
required: false,
default: false,
},
})
defineOptions({
inheritAttrs: false,
})
</script>
<template>
<!-- 👉 Search Banner -->
<VCard
flat
class="text-center search-header"
:class="props.customClass"
:style="`background: url(${AppSearchHeaderBg});`"
>
<VCardText>
<slot name="title">
<h4 class="text-h4 mb-2 font-weight-medium">
{{ props.title }}
</h4>
</slot>
<div
class="d-flex"
:class="isReverse ? 'flex-column' : 'flex-column-reverse' "
>
<p class="mb-0">
{{ props.subtitle }}
</p>
<!-- 👉 Search Input -->
<div>
<AppTextField
v-bind="$attrs"
class="search-header-input mx-auto my-4"
:placeholder="props.placeholder"
:density="props.density"
prepend-inner-icon="tabler-search"
/>
</div>
</div>
</VCardText>
</VCard>
</template>
<style lang="scss">
.search-header {
padding: 4rem !important;
background-size: cover !important;
}
// search input
.search-header-input {
border-radius: 0.375rem !important;
background-color: rgb(var(--v-theme-surface));
max-inline-size: 28.125rem !important;
}
@media (max-width: 37.5rem) {
.search-header {
padding: 1.5rem !important;
}
}
</style>

View File

@@ -0,0 +1,779 @@
<template>
<div class="components-library-container">
<!-- Toggle Button -->
<VBtn
v-if="!isLibraryOpen"
@click="toggleLibrary"
class="library-toggle-btn"
color="primary"
variant="elevated"
size="large"
>
<VIcon icon="tabler-layout-dashboard" size="20" class="me-2" />
Components Library
<VIcon icon="tabler-chevron-up" size="16" class="ms-2" />
</VBtn>
<!-- Components Library Panel -->
<VCard
v-if="isLibraryOpen"
class="components-library-panel"
:class="{ 'panel-open': isLibraryOpen }"
elevation="8"
>
<VCardTitle class="library-header">
<div class="header-content">
<div class="header-left">
<VIcon icon="tabler-layout-dashboard" size="24" class="me-2" />
<span>Dashboard Components</span>
</div>
<VBtn
@click="toggleLibrary"
variant="text"
size="small"
icon
class="close-btn"
>
<VIcon icon="tabler-x" size="20" />
</VBtn>
</div>
</VCardTitle>
<VCardText class="library-content">
<div class="search-section">
<VTextField
v-model="searchQuery"
placeholder="Search components..."
variant="outlined"
density="compact"
prepend-inner-icon="tabler-search"
clearable
class="search-input"
/>
</div>
<div class="categories-tabs">
<VChipGroup
v-model="selectedCategory"
selected-class="text-primary"
class="category-chips"
>
<VChip
v-for="category in categories"
:key="category"
:value="category"
variant="outlined"
size="small"
>
{{ category }}
</VChip>
</VChipGroup>
</div>
<div class="components-grid">
<div
v-for="component in filteredComponents"
:key="component.id"
class="component-item"
:draggable="isEditMode"
@dragstart="handleDragStart($event, component)"
@dragend="handleDragEnd"
:class="{ 'draggable': isEditMode, 'disabled': !isEditMode }"
>
<div class="component-preview">
<VIcon :icon="component.icon" size="32" :color="component.color" />
<div v-if="isEditMode" class="component-overlay">
<VIcon icon="tabler-plus" size="20" color="white" />
</div>
<div v-if="!isEditMode" class="disabled-overlay">
<VIcon icon="tabler-lock" size="16" color="grey" />
</div>
</div>
<div class="component-info">
<h4 class="component-title">{{ component.title }}</h4>
<p class="component-description">{{ component.description }}</p>
<div class="component-meta">
<VChip
:color="component.color"
size="x-small"
variant="outlined"
class="component-type-chip"
>
{{ component.category }}
</VChip>
<VChip
size="x-small"
variant="outlined"
color="grey"
class="size-chip"
>
{{ component.defaultSize.cols }}×{{ Math.round(component.defaultSize.height) }}
</VChip>
</div>
</div>
</div>
</div>
<div v-if="filteredComponents.length === 0" class="no-results">
<VIcon icon="tabler-layout-dashboard" size="48" color="grey-lighten-1" />
<h3>No components found</h3>
<p>Try adjusting your search or category filter</p>
</div>
<div v-if="!isEditMode" class="edit-mode-notice">
<VAlert
type="info"
variant="tonal"
class="ma-2"
>
<template #prepend>
<VIcon icon="tabler-info-circle" />
</template>
<div>
<strong>Edit Mode Required</strong>
<br>
Enable edit mode to add components to your dashboard
</div>
</VAlert>
</div>
</VCardText>
</VCard>
<!-- Drop Zone Overlay -->
<div
v-if="isDragging"
class="drop-zone-overlay"
@dragover="handleDragOver"
@drop="handleDrop"
@dragleave="handleDragLeave"
>
<div class="drop-zone-content">
<VIcon icon="tabler-download" size="64" color="primary" />
<h2>Drop component here to add to dashboard</h2>
<p>Release to add the component to your dashboard</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
isEditMode: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['add-component'])
const isLibraryOpen = ref(false)
const isDragging = ref(false)
const searchQuery = ref('')
const selectedCategory = ref('All')
const categories = ['All', 'CRM', 'Ecommerce', 'Analytics', 'Reports', 'Activity']
const availableComponents = ref([
{
id: 'crm-active-project',
title: 'Active Project',
description: 'Shows current active project details',
icon: 'tabler-folder-open',
color: 'primary',
category: 'CRM',
component: 'CrmActiveProject',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-activity-timeline',
title: 'Activity Timeline',
description: 'Recent activities and updates timeline',
icon: 'tabler-timeline',
color: 'info',
category: 'Activity',
component: 'CrmActivityTimeline',
defaultSize: { cols: 6, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-analytics-sales',
title: 'Sales Analytics',
description: 'Sales performance and analytics',
icon: 'tabler-chart-line',
color: 'success',
category: 'Analytics',
component: 'CrmAnalyticsSales',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-earning-reports',
title: 'Earning Reports',
description: 'Yearly earning overview and reports',
icon: 'tabler-report-money',
color: 'warning',
category: 'Reports',
component: 'CrmEarningReportsYearlyOverview',
defaultSize: { cols: 8, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-project-status',
title: 'Project Status',
description: 'Current status of all projects',
icon: 'tabler-checkup-list',
color: 'deep-purple',
category: 'CRM',
component: 'CrmProjectStatus',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-recent-transactions',
title: 'Recent Transactions',
description: 'Latest financial transactions',
icon: 'tabler-credit-card',
color: 'teal',
category: 'CRM',
component: 'CrmRecentTransactions',
defaultSize: { cols: 6, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-sales-countries',
title: 'Sales by Countries',
description: 'Geographic sales distribution',
icon: 'tabler-world',
color: 'indigo',
category: 'Analytics',
component: 'CrmSalesByCountries',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'project-activity-bar',
title: 'Project Activity Chart',
description: 'Bar chart showing project activities',
icon: 'tabler-chart-bar',
color: 'cyan',
category: 'Analytics',
component: 'ProjectActivityBarChart',
defaultSize: { cols: 8, height: 33.33 },
defaultProps: {}
},
{
id: 'analysis-card-1',
title: 'Analysis Card (Projects)',
description: 'Active projects progress analysis',
icon: 'tabler-analytics',
color: 'pink',
category: 'Analytics',
component: 'AnalysisCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { chartName: 'Active Projects Progress' }
},
{
id: 'analysis-card-2',
title: 'Analysis Card (Cost)',
description: 'Cost overview analysis',
icon: 'tabler-analytics',
color: 'pink',
category: 'Analytics',
component: 'AnalysisCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { chartName: 'Cost overview' }
},
{
id: 'cost-overview',
title: 'Cost Overview',
description: 'Detailed cost breakdown and overview',
icon: 'tabler-coin',
color: 'orange',
category: 'Reports',
component: 'CostOverview',
defaultSize: { cols: 8, height: 33.33 },
defaultProps: {}
},
{
id: 'generated-leads-1',
title: 'Generated Leads (Primary)',
description: 'Lead generation progress - Primary theme',
icon: 'tabler-users',
color: 'primary',
category: 'Ecommerce',
component: 'GeneratedLeadsCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { progress: 33 }
},
{
id: 'generated-leads-2',
title: 'Generated Leads (Success)',
description: 'Lead generation progress - Success theme',
icon: 'tabler-users',
color: 'success',
category: 'Ecommerce',
component: 'GeneratedLeadsCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { donutColors: ["primary"], progress: 71 }
},
{
id: 'generated-leads-3',
title: 'Generated Leads (Warning)',
description: 'Lead generation progress - Warning theme',
icon: 'tabler-users',
color: 'warning',
category: 'Ecommerce',
component: 'GeneratedLeadsCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { donutColors: ["warning"], progress: 56 }
}
])
const filteredComponents = computed(() => {
let components = availableComponents.value
// Filter by category
if (selectedCategory.value !== 'All') {
components = components.filter(comp => comp.category === selectedCategory.value)
}
// Filter by search query
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
components = components.filter(comp =>
comp.title.toLowerCase().includes(query) ||
comp.description.toLowerCase().includes(query) ||
comp.category.toLowerCase().includes(query)
)
}
return components
})
const toggleLibrary = () => {
isLibraryOpen.value = !isLibraryOpen.value
}
const handleDragStart = (event, component) => {
if (!props.isEditMode) {
event.preventDefault()
return
}
isDragging.value = true
event.dataTransfer.setData('application/json', JSON.stringify(component))
event.dataTransfer.effectAllowed = 'copy'
// Create drag image
const dragImage = event.target.cloneNode(true)
dragImage.style.transform = 'rotate(5deg) scale(0.8)'
dragImage.style.opacity = '0.8'
dragImage.style.zIndex = '9999'
document.body.appendChild(dragImage)
event.dataTransfer.setDragImage(dragImage, 50, 50)
setTimeout(() => {
if (document.body.contains(dragImage)) {
document.body.removeChild(dragImage)
}
}, 0)
}
const handleDragEnd = () => {
isDragging.value = false
}
const handleDragOver = (event) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'copy'
}
const handleDrop = (event) => {
event.preventDefault()
isDragging.value = false
try {
const componentData = JSON.parse(event.dataTransfer.getData('application/json'))
emit('add-component', componentData)
// Show success feedback
console.log('Component added successfully:', componentData.title)
} catch (error) {
console.error('Error adding component:', error)
}
}
const handleDragLeave = (event) => {
// Only hide if we're leaving the drop zone entirely
if (!event.currentTarget.contains(event.relatedTarget)) {
isDragging.value = false
}
}
// Close library when clicking outside
const handleClickOutside = (event) => {
if (isLibraryOpen.value && !event.target.closest('.components-library-panel, .library-toggle-btn')) {
isLibraryOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.components-library-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
}
.library-toggle-btn {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(var(--v-theme-primary), 0.2) !important;
border-radius: 12px !important;
padding: 12px 20px !important;
font-weight: 600 !important;
text-transform: none !important;
backdrop-filter: blur(10px);
animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.library-toggle-btn:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15), 0 12px 40px rgba(var(--v-theme-primary), 0.3) !important;
}
.components-library-panel {
position: absolute;
bottom: 80px;
right: 0;
width: 450px;
max-height: 600px;
border-radius: 16px !important;
backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.95) !important;
border: 1px solid rgba(255, 255, 255, 0.2);
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.library-header {
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.1), rgba(var(--v-theme-secondary), 0.05));
border-radius: 16px 16px 0 0;
padding: 1rem 1.5rem;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.header-left {
display: flex;
align-items: center;
font-weight: 600;
font-size: 1.1rem;
}
.close-btn {
opacity: 0.7;
transition: all 0.2s ease;
}
.close-btn:hover {
opacity: 1;
transform: scale(1.1);
}
.library-content {
max-height: 500px;
overflow-y: auto;
padding: 1rem 1.5rem;
}
.search-section {
margin-bottom: 1rem;
}
.search-input {
border-radius: 12px;
}
.categories-tabs {
margin-bottom: 1.5rem;
}
.category-chips {
gap: 0.5rem;
}
.components-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
.component-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.7);
transition: all 0.3s ease;
position: relative;
}
.component-item.draggable {
cursor: grab;
}
.component-item.draggable:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-color: rgba(var(--v-theme-primary), 0.3);
background: rgba(var(--v-theme-primary), 0.02);
}
.component-item.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.component-item:active.draggable {
cursor: grabbing;
transform: scale(0.98);
}
.component-preview {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background: rgba(var(--v-theme-surface), 0.8);
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.component-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--v-theme-primary), 0.9);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s ease;
}
.disabled-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(128, 128, 128, 0.3);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.component-item.draggable:hover .component-overlay {
opacity: 1;
}
.component-info {
flex: 1;
min-width: 0;
}
.component-title {
font-size: 0.95rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
color: rgba(0, 0, 0, 0.87);
line-height: 1.2;
}
.component-description {
font-size: 0.8rem;
color: rgba(0, 0, 0, 0.6);
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.component-meta {
display: flex;
gap: 0.5rem;
align-items: center;
}
.component-type-chip,
.size-chip {
font-size: 0.7rem !important;
height: 20px !important;
}
.no-results {
text-align: center;
padding: 2rem 1rem;
color: rgba(0, 0, 0, 0.6);
}
.no-results h3 {
margin: 1rem 0 0.5rem 0;
font-size: 1.1rem;
}
.no-results p {
margin: 0;
font-size: 0.9rem;
}
.edit-mode-notice {
margin-top: 1rem;
}
.drop-zone-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--v-theme-primary), 0.1);
backdrop-filter: blur(4px);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
.drop-zone-content {
text-align: center;
padding: 3rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
border: 3px dashed rgba(var(--v-theme-primary), 0.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
animation: dropZonePulse 1.5s ease-in-out infinite;
}
.drop-zone-content h2 {
margin: 1rem 0 0.5rem 0;
color: rgba(var(--v-theme-primary), 0.9);
font-size: 1.5rem;
font-weight: 600;
}
.drop-zone-content p {
margin: 0;
color: rgba(0, 0, 0, 0.7);
font-size: 1rem;
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3) translateY(100px);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dropZonePulse {
0%, 100% {
transform: scale(1);
border-color: rgba(var(--v-theme-primary), 0.5);
}
50% {
transform: scale(1.02);
border-color: rgba(var(--v-theme-primary), 0.8);
}
}
@media (max-width: 768px) {
.components-library-container {
bottom: 1rem;
right: 1rem;
left: 1rem;
}
.components-library-panel {
width: 100%;
max-width: none;
right: 0;
left: 0;
}
.library-toggle-btn {
width: 100%;
justify-content: center;
}
.component-item {
padding: 0.75rem;
}
.component-preview {
width: 50px;
height: 50px;
}
.component-title {
font-size: 0.9rem;
}
.component-description {
font-size: 0.75rem;
}
}
</style>

View File

@@ -0,0 +1,346 @@
<template>
<VCard
class="overflow-visible"
:style="cardBackgroundStyle"
>
<VCardText class="d-flex flex-column align-center pa-4">
<!-- Header Section - Responsive -->
<div class="d-flex flex-column flex-sm-row justify-space-between align-start align-sm-center w-100 mb-4 gap-3">
<div class="text-center text-sm-start">
<h5 class="text-h5 text-wrap mb-2">
Cost Overview
</h5>
<div class="text-body-1 mb-2 mb-sm-4 text-medium-emphasis">
Monthly Breakdown
</div>
</div>
<div class="text-center text-sm-end">
<h3 class="text-h4 text-sm-h3 mb-1">
{{ totalCostFormatted }}
</h3>
<div class="d-flex align-center justify-center justify-sm-end">
<VIcon
icon="tabler-trending-up"
color="success"
size="20"
class="me-1"
/>
<span class="text-success font-weight-medium text-sm">
+12.5%
</span>
</div>
</div>
</div>
<!-- Chart Container - Responsive -->
<div
class="d-flex justify-center position-relative chart-container"
>
<VueApexCharts
:options="chartOptions"
:series="series"
type="donut"
width="100%"
height="100%"
class="chart-responsive"
/>
<!-- Center Content -->
<div class="chart-center-content">
<div class="text-center">
<h4 class="text-h5 text-sm-h4 mb-1">{{ totalCostFormatted }}</h4>
<span class="text-body-2 text-medium-emphasis">Total Cost</span>
</div>
</div>
</div>
<!-- Stats Cards - Responsive Grid -->
<div class="stats-grid w-100 mt-4">
<div
v-for="(item, index) in costBreakdown"
:key="index"
class="stat-item pa-3 rounded-lg"
:style="getItemCardStyle(index)"
>
<div class="d-flex align-center justify-center mb-2">
<div
class="dot me-2"
:style="{ backgroundColor: chartColors[index] }"
></div>
<span class="text-body-2 text-medium-emphasis text-center">{{ item.label }}</span>
</div>
<h6 class="text-h6 text-center">{{ formatCurrency(item.value) }}</h6>
<div class="text-caption text-center">{{ item.percentage }}%</div>
</div>
</div>
</VCardText>
</VCard>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
import VueApexCharts from 'vue3-apexcharts'
const vuetifyTheme = useTheme()
const currentTheme = computed(() => vuetifyTheme.current.value.colors)
const labels = ['Human Resources', 'Materials', 'Equipment']
const series = ref([3000, 5000, 2000])
const totalCost = computed(() =>
series.value.reduce((a, b) => a + b, 0)
)
const totalCostFormatted = computed(() =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(totalCost.value)
)
const formatCurrency = (value) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(value)
}
const costBreakdown = computed(() => {
return labels.map((label, index) => ({
label,
value: series.value[index],
percentage: Math.round((series.value[index] / totalCost.value) * 100)
}))
})
const chartColors = computed(() => [
`rgba(${hexToRgb(currentTheme.value.primary)}, 1)`,
`rgba(${hexToRgb(currentTheme.value.success)}, 1)`,
`rgba(${hexToRgb(currentTheme.value.warning)}, 1)`
])
// Card Background Style
const cardBackgroundStyle = computed(() => {
const primaryColor = currentTheme.value.primary
const createGradientColor = (color, opacity = 0.08) => {
if (color.startsWith('#')) {
return `rgba(${hexToRgb(color)}, ${opacity})`
}
return `rgba(${hexToRgb(color)}, ${opacity})`
}
const gradientColor1 = createGradientColor(primaryColor, 0.1)
const gradientColor2 = createGradientColor(primaryColor, 0.03)
return {
background: `linear-gradient(135deg,
${gradientColor1} 0%,
${gradientColor2} 50%,
${gradientColor1} 100%)`
}
})
const getItemCardStyle = (index) => {
const color = chartColors.value[index]
const bgColor = color.replace('1)', '0.1)')
return {
backgroundColor: bgColor,
border: `1px solid ${color.replace('1)', '0.2)')}`
}
}
const chartOptions = computed(() => {
const labelColor = `rgba(${hexToRgb(currentTheme.value['on-surface'])}, ${vuetifyTheme.current.value.variables['disabled-opacity']})`
return {
labels,
colors: chartColors.value,
chart: {
type: 'donut',
toolbar: { show: false },
sparkline: { enabled: false }
},
stroke: {
colors: ['transparent'],
width: 0
},
legend: {
show: false
},
tooltip: {
theme: vuetifyTheme.current.value.dark ? 'dark' : 'light',
style: { fontFamily: 'inherit' },
y: {
formatter: (val) => formatCurrency(val)
}
},
dataLabels: {
enabled: true,
formatter: (val) => Math.round(val) + '%',
style: {
fontSize: '12px',
fontWeight: 'bold',
colors: ['#fff']
}
},
plotOptions: {
pie: {
donut: {
size: '75%',
labels: {
show: false
}
}
}
},
responsive: [
{
breakpoint: 600,
options: {
chart: {
width: 280,
height: 280
},
dataLabels: {
style: {
fontSize: '10px'
}
}
}
},
{
breakpoint: 480,
options: {
chart: {
width: 250,
height: 250
},
dataLabels: {
style: {
fontSize: '9px'
}
}
}
}
]
}
})
</script>
<style scoped>
/* Chart Container - Responsive */
.chart-container {
width: 100%;
max-width: 330px;
height: 330px;
}
@media (max-width: 599px) {
.chart-container {
max-width: 280px;
height: 280px;
}
}
@media (max-width: 479px) {
.chart-container {
max-width: 250px;
height: 250px;
}
}
.chart-responsive {
width: 100% !important;
height: 100% !important;
}
.chart-center-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
/* Stats Grid - Responsive */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
@media (max-width: 767px) {
.stats-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
}
}
@media (max-width: 599px) and (min-width: 480px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.stat-item {
min-height: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Responsive Typography */
@media (max-width: 599px) {
.text-h5 {
font-size: 1.25rem !important;
}
.text-h4 {
font-size: 1.5rem !important;
}
.text-h6 {
font-size: 1rem !important;
}
}
@media (max-width: 479px) {
.text-h5 {
font-size: 1.125rem !important;
}
.text-h4 {
font-size: 1.25rem !important;
}
.chart-center-content h4 {
font-size: 1.125rem !important;
}
}
/* Card padding adjustments for mobile */
@media (max-width: 599px) {
.pa-4 {
padding: 1rem !important;
}
}
@media (max-width: 479px) {
.pa-4 {
padding: 0.75rem !important;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<script setup>
const props = defineProps({
statusCode: {
type: [
String,
Number,
],
required: false,
},
title: {
type: String,
required: false,
},
description: {
type: String,
required: false,
},
})
</script>
<template>
<div class="text-center">
<!-- 👉 Title and subtitle -->
<h1
v-if="props.statusCode"
class="header-title font-weight-medium mb-2"
>
{{ props.statusCode }}
</h1>
<h4
v-if="props.title"
class="text-h4 font-weight-medium mb-2"
>
{{ props.title }}
</h4>
<p
v-if="props.description"
class="text-body-1 mb-6"
>
{{ props.description }}
</p>
</div>
</template>
<style lang="scss" scoped>
.header-title {
font-size: clamp(3rem, 5vw, 6rem);
line-height: clamp(3rem, 5vw, 6rem);
}
</style>

View File

@@ -0,0 +1,508 @@
<template>
<VCard
class="overflow-visible position-relative"
:style="cardBackgroundStyle"
elevation="8"
>
<!-- Decorative elements -->
<div class="position-absolute decorative-circle-1"></div>
<div class="position-absolute decorative-circle-2"></div>
<div class="position-absolute decorative-dots"></div>
<VCardItem class="pb-4">
<div class="d-flex align-center justify-space-between">
<div>
<VCardTitle class="text-h5 font-weight-bold mb-1">
<VIcon icon="mdi-chart-line" class="me-2 text-primary"></VIcon>
Active Projects Progress
</VCardTitle>
<VCardSubtitle class="text-subtitle-1 opacity-80">This Week Overview</VCardSubtitle>
</div>
<VChip
:color="changePercent >= 0 ? 'success' : 'error'"
variant="tonal"
size="small"
class="font-weight-bold"
>
{{ formattedChange }}
</VChip>
</div>
</VCardItem>
<VCardText class="pb-2">
<!-- Progress Summary -->
<div class="progress-summary-card mb-6">
<div class="d-flex align-center justify-center mb-2">
<div class="progress-circle">
<h2 class="text-h2 font-weight-bold text-primary mb-0">{{ totalProgress }}%</h2>
</div>
</div>
<p class="text-center text-subtitle-2 mb-0 opacity-80">Average Progress</p>
</div>
<VueApexCharts
:options="chartOptions"
:series="series"
:height="330"
width="100%"
class="enhanced-chart"
/>
<!-- Project Stats -->
<!-- <div class="project-stats mt-4">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-subtitle-2 opacity-80">Projects Status</span>
<VChip size="x-small" color="primary" variant="text">
{{ values.length }} Total
</VChip>
</div>
<div class="d-flex flex-wrap gap-2">
<VChip
v-for="(value, index) in values"
:key="index"
:color="getProjectStatusColor(value)"
variant="tonal"
size="small"
class="font-weight-medium"
>
{{ labels[index] }}: {{ value }}%
</VChip>
</div>
</div> -->
</VCardText>
</VCard>
</template>
<script setup>
import { computed } from 'vue'
import { useTheme } from 'vuetify'
import { hexToRgb } from '@layouts/utils'
import VueApexCharts from 'vue3-apexcharts'
const vuetifyTheme = useTheme()
const labels = [
'Project Alpha',
'Project Beta',
'Project Gamma',
'Project Delta',
'Project Epsilon',
]
const values = [45, 70, 30, 85, 55]
const series = [
{
name: 'Progress Percentage',
data: values,
},
]
const totalProgress = computed(() =>
Math.round(values.reduce((sum, v) => sum + v, 0) / values.length)
)
const changePercent = computed(() => {
const last = values[values.length - 1]
const prev = values[values.length - 2] ?? last
return prev === 0 ? 0 : ((last - prev) / prev) * 100
})
const formattedChange = computed(() => {
const sign = changePercent.value >= 0 ? '+' : ''
return `${sign}${changePercent.value.toFixed(1)}%`
})
const getProjectStatusColor = (value) => {
if (value >= 80) return 'success'
if (value >= 60) return 'warning'
if (value >= 40) return 'info'
return 'error'
}
const cardBackgroundStyle = computed(() => {
const currentTheme = vuetifyTheme.current.value.colors
const primaryColor = currentTheme.primary
const createGradientColor = (color, opacity = 0.08) => {
if (color.includes('rgba')) {
return color.replace(/[\d\.]+\)$/g, `${opacity})`)
}
if (color.startsWith('#')) {
return `rgba(${hexToRgb(color)}, ${opacity})`
}
if (color.includes('rgb')) {
return color.replace('rgb', 'rgba').replace(')', `, ${opacity})`)
}
return `rgba(${hexToRgb(color)}, ${opacity})`
}
const gradientColor1 = createGradientColor(primaryColor, 0.15)
const gradientColor2 = createGradientColor(primaryColor, 0.02)
const gradientColor3 = createGradientColor(primaryColor, 0.08)
return {
background: `linear-gradient(135deg,
${gradientColor1} 0%,
${gradientColor2} 25%,
${gradientColor3} 50%,
${gradientColor2} 75%,
${gradientColor1} 100%)`,
backdropFilter: 'blur(10px)',
border: `1px solid ${createGradientColor(primaryColor, 0.2)}`
}
})
const chartOptions = computed(() => ({
chart: {
type: 'bar',
parentHeightOffset: 0,
toolbar: { show: false },
background: 'transparent',
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: true,
delay: 150
},
dynamicAnimation: {
enabled: true,
speed: 350
}
}
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: "35%",
borderRadius: 20,
borderRadiusApplication: 'end',
borderRadiusWhenStacked: 'last',
dataLabels: {
position: 'top'
}
},
},
colors: ['rgba(var(--v-theme-primary), 1)'],
grid: {
show: true,
borderColor: 'rgba(var(--v-theme-on-surface), 0.08)',
strokeDashArray: 3,
position: 'back',
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: true
}
},
row: {
colors: undefined,
opacity: 0.5
},
column: {
colors: undefined,
opacity: 0.5
},
padding: {
top: 0,
right: 10,
bottom: 0,
left: 10
}
},
dataLabels: {
enabled: true,
offsetY: -20,
style: {
fontSize: '12px',
fontWeight: 'bold',
colors: ['rgba(var(--v-theme-primary), 1)']
},
formatter: function (val) {
return val + '%'
}
},
legend: { show: false },
tooltip: {
enabled: true,
theme: 'dark',
style: {
fontSize: '12px',
fontFamily: 'Arial, sans-serif'
},
y: {
formatter: function (val) {
return val + '%'
}
},
marker: {
show: true
}
},
xaxis: {
categories: labels,
axisBorder: {
show: true,
color: 'rgba(var(--v-theme-on-surface), 0.12)'
},
axisTicks: {
show: true,
color: 'rgba(var(--v-theme-on-surface), 0.12)'
},
labels: {
show: true,
rotate: -45,
rotateAlways: false,
hideOverlappingLabels: true,
showDuplicates: false,
trim: false,
style: {
fontSize: '12px',
fontWeight: '500',
colors: 'rgba(var(--v-theme-on-surface), 0.7)',
fontFamily: 'Arial, sans-serif',
},
},
},
yaxis: {
labels: {
show: true,
style: {
fontSize: '11px',
colors: 'rgba(var(--v-theme-on-surface), 0.6)',
fontFamily: 'Arial, sans-serif',
},
formatter: function (val) {
return val + '%'
}
},
min: 0,
max: 100
},
responsive: [
{
breakpoint: 1441,
options: {
plotOptions: {
bar: {
columnWidth: '45%',
borderRadius: 18,
},
},
},
},
{
breakpoint: 1368,
options: {
plotOptions: {
bar: {
columnWidth: '40%',
borderRadius: 16,
}
},
},
},
{
breakpoint: 1264,
options: {
plotOptions: {
bar: {
columnWidth: '35%',
borderRadius: 20,
},
},
},
},
{
breakpoint: 960,
options: {
plotOptions: {
bar: {
columnWidth: '30%',
borderRadius: 18,
},
},
},
},
{
breakpoint: 883,
options: {
plotOptions: {
bar: {
columnWidth: '35%',
borderRadius: 16,
}
}
},
},
{
breakpoint: 768,
options: {
plotOptions: {
bar: {
columnWidth: '40%',
borderRadius: 14,
}
},
dataLabels: {
offsetY: -15
}
},
},
{
breakpoint: 600,
options: {
plotOptions: {
bar: {
columnWidth: '45%',
borderRadius: 16,
},
},
dataLabels: {
enabled: false
}
},
},
{
breakpoint: 479,
options: {
plotOptions: {
bar: {
borderRadius: 12,
},
},
grid: {
padding: { right: 5, left: 5, bottom: 10 },
},
dataLabels: {
enabled: false
}
},
},
{
breakpoint: 400,
options: {
plotOptions: {
bar: {
borderRadius: 10,
}
},
xaxis: {
labels: {
rotate: -90
}
}
},
},
],
}))
</script>
<style scoped>
.decorative-circle-1 {
top: -20px;
right: -20px;
width: 80px;
height: 80px;
background: radial-gradient(circle, rgba(var(--v-theme-primary), 0.1) 0%, transparent 70%);
border-radius: 50%;
z-index: 0;
}
.decorative-circle-2 {
bottom: -30px;
left: -30px;
width: 120px;
height: 120px;
background: radial-gradient(circle, rgba(var(--v-theme-secondary), 0.08) 0%, transparent 70%);
border-radius: 50%;
z-index: 0;
}
.decorative-dots {
top: 50%;
right: 20px;
width: 6px;
height: 40px;
background-image: radial-gradient(circle, rgba(var(--v-theme-primary), 0.3) 1px, transparent 1px);
background-size: 6px 8px;
z-index: 0;
}
.progress-summary-card {
background: rgba(var(--v-theme-surface), 0.6);
border-radius: 16px;
padding: 20px;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(var(--v-theme-primary), 0.1);
position: relative;
overflow: hidden;
}
.progress-summary-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg,
rgba(var(--v-theme-primary), 0.3) 0%,
rgba(var(--v-theme-primary), 0.8) 50%,
rgba(var(--v-theme-primary), 0.3) 100%);
}
.progress-circle {
position: relative;
display: inline-block;
padding: 10px;
}
.progress-circle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
border: 3px solid rgba(var(--v-theme-primary), 0.2);
border-radius: 50%;
z-index: -1;
}
.project-stats {
background: rgba(var(--v-theme-surface), 0.4);
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(5px);
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
}
.enhanced-chart {
position: relative;
z-index: 1;
}
/* Hover effects */
.v-card:hover {
transform: translateY(-2px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.v-card:hover .decorative-circle-1 {
transform: scale(1.1);
transition: transform 0.3s ease;
}
.v-card:hover .decorative-circle-2 {
transform: scale(1.05);
transition: transform 0.3s ease;
}
</style>

View File

@@ -0,0 +1,106 @@
<script setup>
import themeselectionQr from '@images/pages/themeselection-qr.png'
const props = defineProps({
authCode: {
type: String,
required: false,
},
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'submit',
])
const authCode = ref(structuredClone(toRaw(props.authCode)))
const formSubmit = () => {
if (authCode.value) {
emit('submit', authCode.value)
emit('update:isDialogVisible', false)
}
}
const resetAuthCode = () => {
authCode.value = structuredClone(toRaw(props.authCode))
emit('update:isDialogVisible', false)
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 900"
:model-value="props.isDialogVisible"
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-6">
Add Authenticator App
</h4>
<h5 class="text-h5 mb-2">
Authenticator Apps
</h5>
<p class="text-body-1 mb-6">
Using an authenticator app like Google Authenticator, Microsoft Authenticator, Authy, or 1Password, scan the QR code. It will generate a 6 digit code for you to enter below.
</p>
<div class="mb-6">
<VImg
width="150"
:src="themeselectionQr"
class="mx-auto"
/>
</div>
<VAlert
title="ASDLKNASDA9AHS678dGhASD78AB"
text="If you are unable to scan the QR code, you can manually enter the secret key below."
variant="tonal"
color="warning"
/>
<VForm @submit.prevent="() => {}">
<AppTextField
v-model="authCode"
name="auth-code"
label="Enter Authentication Code"
placeholder="123 456"
class="mt-4 mb-6"
/>
<div class="d-flex justify-end flex-wrap gap-4">
<VBtn
color="secondary"
variant="tonal"
@click="resetAuthCode"
>
Cancel
</VBtn>
<VBtn
type="submit"
@click="formSubmit"
>
Continue
<VIcon
end
icon="tabler-arrow-right"
class="flip-in-rtl"
/>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,233 @@
<script setup>
import home from '@images/svg/home.svg'
import office from '@images/svg/office.svg'
const props = defineProps({
billingAddress: {
type: Object,
required: false,
default: () => ({
firstName: '',
lastName: '',
selectedCountry: null,
addressLine1: '',
addressLine2: '',
landmark: '',
contact: '',
country: null,
city: '',
state: '',
zipCode: null,
}),
},
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'submit',
])
const billingAddress = ref(structuredClone(toRaw(props.billingAddress)))
const resetForm = () => {
emit('update:isDialogVisible', false)
billingAddress.value = structuredClone(toRaw(props.billingAddress))
}
const onFormSubmit = () => {
emit('update:isDialogVisible', false)
emit('submit', billingAddress.value)
}
const selectedAddress = ref('Home')
const addressTypes = [
{
icon: {
icon: home,
size: '28',
},
title: 'Home',
desc: 'Delivery Time (9am - 9pm)',
value: 'Home',
},
{
icon: {
icon: office,
size: '28',
},
title: 'Office',
desc: 'Delivery Time (9am - 5pm)',
value: 'Office',
},
]
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 900 "
:model-value="props.isDialogVisible"
@update:model-value="val => $emit('update:isDialogVisible', val)"
>
<!-- 👉 Dialog close btn -->
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
<VCard
v-if="props.billingAddress"
class="pa-sm-10 pa-2"
>
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
{{ (props.billingAddress.addressLine1 || props.billingAddress.addressLine2) ? 'Edit' : 'Add New' }} Address
</h4>
<p class="text-body-1 text-center mb-6">
Add new address for express delivery
</p>
<div class="d-flex mb-6">
<CustomRadiosWithIcon
v-model:selected-radio="selectedAddress"
:radio-content="addressTypes"
:grid-column="{ sm: '6', cols: '12' }"
/>
</div>
<!-- 👉 Form -->
<VForm @submit.prevent="onFormSubmit">
<VRow>
<!-- 👉 First Name -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="billingAddress.firstName"
label="First Name"
placeholder="John"
/>
</VCol>
<!-- 👉 Last Name -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="billingAddress.lastName"
label="Last Name"
placeholder="Doe"
/>
</VCol>
<!-- 👉 Select Country -->
<VCol cols="12">
<AppSelect
v-model="billingAddress.selectedCountry"
label="Select Country"
placeholder="Select Country"
:items="['USA', 'Aus', 'Canada', 'NZ']"
/>
</VCol>
<!-- 👉 Address Line 1 -->
<VCol cols="12">
<AppTextField
v-model="billingAddress.addressLine1"
label="Address Line 1"
placeholder="12, Business Park"
/>
</VCol>
<!-- 👉 Address Line 2 -->
<VCol cols="12">
<AppTextField
v-model="billingAddress.addressLine2"
label="Address Line 2"
placeholder="Mall Road"
/>
</VCol>
<!-- 👉 Landmark -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="billingAddress.landmark"
label="Landmark"
placeholder="Nr. Hard Rock Cafe"
/>
</VCol>
<!-- 👉 City -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="billingAddress.city"
label="City"
placeholder="Los Angeles"
/>
</VCol>
<!-- 👉 State -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="billingAddress.state"
label="State"
placeholder="California"
/>
</VCol>
<!-- 👉 Zip Code -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="billingAddress.zipCode"
label="Zip Code"
placeholder="99950"
type="number"
/>
</VCol>
<VCol cols="12">
<VSwitch label="Use as a billing address?" />
</VCol>
<!-- 👉 Submit and Cancel button -->
<VCol
cols="12"
class="text-center"
>
<VBtn
type="submit"
class="me-3"
>
submit
</VBtn>
<VBtn
variant="tonal"
color="secondary"
@click="resetForm"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,95 @@
<script setup>
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
permissionName: {
type: String,
required: false,
default: '',
},
})
const emit = defineEmits([
'update:isDialogVisible',
'update:permissionName',
])
const currentPermissionName = ref('')
const onReset = () => {
emit('update:isDialogVisible', false)
currentPermissionName.value = ''
}
const onSubmit = () => {
emit('update:isDialogVisible', false)
emit('update:permissionName', currentPermissionName.value)
}
watch(() => props, () => {
currentPermissionName.value = props.permissionName
})
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 600"
:model-value="props.isDialogVisible"
@update:model-value="onReset"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn @click="onReset" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
{{ props.permissionName ? 'Edit' : 'Add' }} Permission
</h4>
<p class="text-body-1 text-center mb-6">
{{ props.permissionName ? 'Edit' : 'Add' }} permission as per your requirements.
</p>
<!-- 👉 Form -->
<VForm>
<VAlert
type="warning"
title="Warning!"
variant="tonal"
class="mb-6"
>
<template #text>
By {{ props.permissionName ? 'editing' : 'adding' }} the permission name, you might break the system permissions functionality.
</template>
</VAlert>
<!-- 👉 Role name -->
<div class="d-flex gap-4 mb-6 flex-wrap flex-column flex-sm-row">
<AppTextField
v-model="currentPermissionName"
placeholder="Enter Permission Name"
/>
<VBtn @click="onSubmit">
{{ props.permissionName ? 'Update' : 'Add' }}
</VBtn>
</div>
<VCheckbox label="Set as core permission" />
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.permission-table {
td {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
padding-block: 0.5rem;
padding-inline: 0;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<script setup>
import { VForm } from 'vuetify/components/VForm'
const props = defineProps({
rolePermissions: {
type: Object,
required: false,
default: () => ({
name: '',
permissions: [],
}),
},
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'update:rolePermissions',
])
const permissions = ref([
{
name: 'User Management',
read: false,
write: false,
create: false,
},
{
name: 'Content Management',
read: false,
write: false,
create: false,
},
{
name: 'Disputes Management',
read: false,
write: false,
create: false,
},
{
name: 'Database Management',
read: false,
write: false,
create: false,
},
{
name: 'Financial Management',
read: false,
write: false,
create: false,
},
{
name: 'Reporting',
read: false,
write: false,
create: false,
},
{
name: 'API Control',
read: false,
write: false,
create: false,
},
{
name: 'Repository Management',
read: false,
write: false,
create: false,
},
{
name: 'Payroll',
read: false,
write: false,
create: false,
},
])
const isSelectAll = ref(false)
const role = ref('')
const refPermissionForm = ref()
const checkedCount = computed(() => {
let counter = 0
permissions.value.forEach(permission => {
Object.entries(permission).forEach(([key, value]) => {
if (key !== 'name' && value)
counter++
})
})
return counter
})
const isIndeterminate = computed(() => checkedCount.value > 0 && checkedCount.value < permissions.value.length * 3)
watch(isSelectAll, val => {
permissions.value = permissions.value.map(permission => ({
...permission,
read: val,
write: val,
create: val,
}))
})
watch(isIndeterminate, () => {
if (!isIndeterminate.value)
isSelectAll.value = false
})
watch(permissions, () => {
if (checkedCount.value === permissions.value.length * 3)
isSelectAll.value = true
}, { deep: true })
watch(() => props, () => {
if (props.rolePermissions && props.rolePermissions.permissions.length) {
role.value = props.rolePermissions.name
permissions.value = permissions.value.map(permission => {
const rolePermission = props.rolePermissions?.permissions.find(item => item.name === permission.name)
if (rolePermission) {
return {
...permission,
...rolePermission,
}
}
return permission
})
}
})
const onSubmit = () => {
const rolePermissions = {
name: role.value,
permissions: permissions.value,
}
emit('update:rolePermissions', rolePermissions)
emit('update:isDialogVisible', false)
isSelectAll.value = false
refPermissionForm.value?.reset()
}
const onReset = () => {
emit('update:isDialogVisible', false)
isSelectAll.value = false
refPermissionForm.value?.reset()
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 900"
:model-value="props.isDialogVisible"
@update:model-value="onReset"
>
<!-- 👉 Dialog close btn -->
<DialogCloseBtn @click="onReset" />
<VCard class="pa-sm-10 pa-2">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
{{ props.rolePermissions.name ? 'Edit' : 'Add New' }} Role
</h4>
<p class="text-body-1 text-center mb-6">
Set Role Permissions
</p>
<!-- 👉 Form -->
<VForm ref="refPermissionForm">
<!-- 👉 Role name -->
<AppTextField
v-model="role"
label="Role Name"
placeholder="Enter Role Name"
/>
<h5 class="text-h5 my-6">
Role Permissions
</h5>
<!-- 👉 Role Permissions -->
<VTable class="permission-table text-no-wrap mb-6">
<!-- 👉 Admin -->
<tr>
<td>
<h6 class="text-h6">
Administrator Access
</h6>
</td>
<td colspan="3">
<div class="d-flex justify-end">
<VCheckbox
v-model="isSelectAll"
v-model:indeterminate="isIndeterminate"
label="Select All"
/>
</div>
</td>
</tr>
<!-- 👉 Other permission loop -->
<template
v-for="permission in permissions"
:key="permission.name"
>
<tr>
<td>
<h6 class="text-h6">
{{ permission.name }}
</h6>
</td>
<td>
<div class="d-flex justify-end">
<VCheckbox
v-model="permission.read"
label="Read"
/>
</div>
</td>
<td>
<div class="d-flex justify-end">
<VCheckbox
v-model="permission.write"
label="Write"
/>
</div>
</td>
<td>
<div class="d-flex justify-end">
<VCheckbox
v-model="permission.create"
label="Create"
/>
</div>
</td>
</tr>
</template>
</VTable>
<!-- 👉 Actions button -->
<div class="d-flex align-center justify-center gap-4">
<VBtn @click="onSubmit">
Submit
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="onReset"
>
Cancel
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.permission-table {
td {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
padding-block: 0.5rem;
.v-checkbox {
min-inline-size: 4.75rem;
}
&:not(:first-child) {
padding-inline: 0.5rem;
}
.v-label {
white-space: nowrap;
}
}
}
</style>

View File

@@ -0,0 +1,117 @@
<script setup>
import americanExDark from '@images/icons/payments/img/ae-dark.png'
import americanExLight from '@images/icons/payments/img/american-express.png'
import dcDark from '@images/icons/payments/img/dc-dark.png'
import dcLight from '@images/icons/payments/img/dc-light.png'
import jcbDark from '@images/icons/payments/img/jcb-dark.png'
import jcbLight from '@images/icons/payments/img/jcb-light.png'
import masterCardDark from '@images/icons/payments/img/master-dark.png'
import masterCardLight from '@images/icons/payments/img/mastercard.png'
import visaDark from '@images/icons/payments/img/visa-dark.png'
import visaLight from '@images/icons/payments/img/visa-light.png'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDialogVisible'])
const visa = useGenerateImageVariant(visaLight, visaDark)
const masterCard = useGenerateImageVariant(masterCardLight, masterCardDark)
const americanEx = useGenerateImageVariant(americanExLight, americanExDark)
const jcb = useGenerateImageVariant(jcbLight, jcbDark)
const dc = useGenerateImageVariant(dcLight, dcDark)
const dialogVisibleUpdate = val => {
emit('update:isDialogVisible', val)
}
const paymentMethodsData = [
{
title: 'Visa',
type: 'Credit Card',
img: visa,
},
{
title: 'American Express',
type: 'Credit Card',
img: americanEx,
},
{
title: 'Mastercard',
type: 'Credit Card',
img: masterCard,
},
{
title: 'JCB',
type: 'Credit Card',
img: jcb,
},
{
title: 'Diners Club',
type: 'Credit Card',
img: dc,
},
]
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
:width="$vuetify.display.smAndDown ? 'auto' : 750"
@update:model-value="dialogVisibleUpdate"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn @click="emit('update:isDialogVisible', false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
Add payment methods
</h4>
<p class="text-body-1 text-center mb-6">
Supported payment methods
</p>
<div
v-for="(item, index) in paymentMethodsData"
:key="index"
>
<div class="d-flex justify-space-between align-center py-4 gap-x-4">
<div class="d-flex align-center">
<VImg
:src="item.img.value"
height="30"
width="50"
class="me-4"
/>
<h6 class="text-h6">
{{ item.title }}
</h6>
</div>
<div class="d-none d-sm-block text-body-1">
{{ item.type }}
</div>
</div>
<VDivider v-if="index !== paymentMethodsData.length - 1" />
</div>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.refer-link-input {
.v-field--appended {
padding-inline-end: 0;
}
.v-field__append-inner {
padding-block-start: 0.125rem;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<script setup>
const props = defineProps({
cardDetails: {
type: Object,
required: false,
default: () => ({
number: '',
name: '',
expiry: '',
cvv: '',
isPrimary: false,
type: '',
}),
},
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'submit',
'update:isDialogVisible',
])
const cardDetails = ref(structuredClone(toRaw(props.cardDetails)))
watch(() => props, () => {
cardDetails.value = structuredClone(toRaw(props.cardDetails))
})
const formSubmit = () => {
emit('submit', cardDetails.value)
}
const dialogModelValueUpdate = val => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 600"
:model-value="props.isDialogVisible"
@update:model-value="dialogModelValueUpdate"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
<VCard class="pa-2 pa-sm-10">
<!-- 👉 Title -->
<VCardItem class="text-center">
<VCardTitle>
<h4 class="text-h4 mb-2">
{{ props.cardDetails.name ? 'Edit Card' : 'Add New Card' }}
</h4>
</VCardTitle>
<p class="text-body-1 mb-0">
{{ props.cardDetails.name ? 'Edit your saved card details' : 'Add card for future billing' }}
</p>
</VCardItem>
<VCardText class="pt-6">
<VForm @submit.prevent="() => {}">
<VRow>
<!-- 👉 Card Number -->
<VCol cols="12">
<AppTextField
v-model="cardDetails.number"
label="Card Number"
placeholder="1356 3215 6548 7898"
type="number"
/>
</VCol>
<!-- 👉 Card Name -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="cardDetails.name"
label="Name"
placeholder="John Doe"
/>
</VCol>
<!-- 👉 Card Expiry -->
<VCol
cols="12"
md="3"
>
<AppTextField
v-model="cardDetails.expiry"
label="Expiry Date"
placeholder="MM/YY"
/>
</VCol>
<!-- 👉 Card CVV -->
<VCol
cols="12"
md="3"
>
<AppTextField
v-model="cardDetails.cvv"
type="number"
label="CVV Code"
placeholder="654"
/>
</VCol>
<!-- 👉 Card Primary Set -->
<VCol cols="12">
<VSwitch
v-model="cardDetails.isPrimary"
label="Save Card for future billing?"
/>
</VCol>
<!-- 👉 Card actions -->
<VCol
cols="12"
class="text-center"
>
<VBtn
class="me-4"
type="submit"
@click="formSubmit"
>
Submit
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="$emit('update:isDialogVisible', false)"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,165 @@
<script setup>
const props = defineProps({
confirmationQuestion: {
type: String,
required: true,
},
isDialogVisible: {
type: Boolean,
required: true,
},
confirmTitle: {
type: String,
required: true,
},
confirmMsg: {
type: String,
required: true,
},
cancelTitle: {
type: String,
required: true,
},
cancelMsg: {
type: String,
required: true,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'confirm',
])
const unsubscribed = ref(false)
const cancelled = ref(false)
const updateModelValue = val => {
emit('update:isDialogVisible', val)
}
const onConfirmation = () => {
emit('confirm', true)
updateModelValue(false)
unsubscribed.value = true
}
const onCancel = () => {
emit('confirm', false)
emit('update:isDialogVisible', false)
cancelled.value = true
}
</script>
<template>
<!-- 👉 Confirm Dialog -->
<VDialog
max-width="500"
:model-value="props.isDialogVisible"
@update:model-value="updateModelValue"
>
<VCard class="text-center px-10 py-6">
<VCardText>
<VBtn
icon
variant="outlined"
color="warning"
class="my-4"
style=" block-size: 88px;inline-size: 88px; pointer-events: none;"
>
<span class="text-5xl">!</span>
</VBtn>
<h6 class="text-lg font-weight-medium">
{{ props.confirmationQuestion }}
</h6>
</VCardText>
<VCardText class="d-flex align-center justify-center gap-2">
<VBtn
variant="elevated"
@click="onConfirmation"
>
Confirm
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="onCancel"
>
Cancel
</VBtn>
</VCardText>
</VCard>
</VDialog>
<!-- Unsubscribed -->
<VDialog
v-model="unsubscribed"
max-width="500"
>
<VCard>
<VCardText class="text-center px-10 py-6">
<VBtn
icon
variant="outlined"
color="success"
class="my-4"
style=" block-size: 88px;inline-size: 88px; pointer-events: none;"
>
<VIcon
icon="tabler-check"
size="38"
/>
</VBtn>
<h1 class="text-h4 mb-4">
{{ props.confirmTitle }}
</h1>
<p>{{ props.confirmMsg }}</p>
<VBtn
color="success"
@click="unsubscribed = false"
>
Ok
</VBtn>
</VCardText>
</VCard>
</VDialog>
<!-- Cancelled -->
<VDialog
v-model="cancelled"
max-width="500"
>
<VCard>
<VCardText class="text-center px-10 py-6">
<VBtn
icon
variant="outlined"
color="error"
class="my-4"
style=" block-size: 88px;inline-size: 88px; pointer-events: none;"
>
<span class="text-5xl font-weight-light">X</span>
</VBtn>
<h1 class="text-h4 mb-4">
{{ props.cancelTitle }}
</h1>
<p>{{ props.cancelMsg }}</p>
<VBtn
color="success"
@click="cancelled = false"
>
Ok
</VBtn>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,463 @@
<script setup>
import laptopGirl from '@images/illustrations/laptop-girl.png'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'updatedData',
])
const currentStep = ref(0)
const createApp = [
{
icon: 'tabler-file-text',
title: 'DETAILS',
subtitle: 'Enter Details',
},
{
icon: 'tabler-id',
title: 'FRAMEWORKS',
subtitle: 'Select Framework',
},
{
icon: 'tabler-database',
title: 'DATABASE',
subtitle: 'Select Database',
},
{
icon: 'tabler-credit-card',
title: 'BILLING',
subtitle: 'Payment Details',
},
{
icon: 'tabler-check',
title: 'SUBMIT',
subtitle: 'Submit',
},
]
const categories = [
{
icon: 'tabler-file-text',
color: 'info',
title: 'CRM Application',
subtitle: 'Scales with any business',
slug: 'crm-application',
},
{
icon: 'tabler-shopping-cart',
color: 'success',
title: 'Ecommerce Platforms',
subtitle: 'Grow Your Business With App',
slug: 'ecommerce-application',
},
{
icon: 'tabler-device-laptop',
color: 'error',
title: 'Online Learning platform',
subtitle: 'Start learning today',
slug: 'online-learning-application',
},
]
const frameworks = [
{
icon: 'tabler-brand-react-native',
color: 'info',
title: 'React Native',
subtitle: 'Create truly native apps',
slug: 'react-framework',
},
{
icon: 'tabler-brand-angular',
color: 'error',
title: 'Angular',
subtitle: 'Most suited for your application',
slug: 'angular-framework',
},
{
icon: 'tabler-brand-vue',
color: 'success',
title: 'Vue',
subtitle: 'JS web frameworks',
slug: 'js-framework',
},
{
icon: 'tabler-brand-html5',
color: 'warning',
title: 'HTML',
subtitle: 'Progressive Framework',
slug: 'html-framework',
},
]
const databases = [
{
icon: 'tabler-brand-firebase',
color: 'error',
title: 'Firebase',
subtitle: 'Cloud Firestore',
slug: 'firebase-database',
},
{
icon: 'tabler-brand-amazon',
color: 'warning',
title: 'AWS',
subtitle: 'Amazon Fast NoSQL Database',
slug: 'aws-database',
},
{
icon: 'tabler-database',
color: 'info',
title: 'MySQL',
subtitle: 'Basic MySQL database',
slug: 'mysql-database',
},
]
const createAppData = ref({
category: 'crm-application',
framework: 'js-framework',
database: 'firebase-database',
cardNumber: null,
cardName: '',
cardExpiry: '',
cardCvv: '',
isSave: true,
})
const dialogVisibleUpdate = val => {
emit('update:isDialogVisible', val)
currentStep.value = 0
}
watch(() => props, () => {
if (!props.isDialogVisible)
currentStep.value = 0
})
const onSubmit = () => {
// eslint-disable-next-line no-alert
alert('submitted...!!')
emit('updatedData', createAppData.value)
}
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
max-width="900"
min-height="590"
@update:model-value="dialogVisibleUpdate"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn
size="small"
@click="emit('update:isDialogVisible', false)"
/>
<VCard
class="create-app-dialog"
min-height="590"
>
<VCardText class="pa-5 pa-sm-16">
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
Create App
</h4>
<p class="text-body-1 text-center mb-6">
Provide data with this form to create your app.
</p>
<VRow>
<VCol
cols="12"
sm="5"
md="4"
lg="3"
>
<AppStepper
v-model:current-step="currentStep"
direction="vertical"
:items="createApp"
icon-size="22"
class="stepper-icon-step-bg"
/>
</VCol>
<VCol
cols="12"
sm="7"
md="8"
lg="9"
>
<VWindow
v-model="currentStep"
class="disable-tab-transition stepper-content"
>
<!-- 👉 category -->
<VWindowItem>
<AppTextField
label="Application Name"
placeholder="Application Name"
/>
<h5 class="text-h5 mt-6 mb-4">
Category
</h5>
<VRadioGroup v-model="createAppData.category">
<VList class="card-list">
<VListItem
v-for="category in categories"
:key="category.title"
@click="createAppData.category = category.slug"
>
<template #prepend>
<VAvatar
size="46"
rounded
variant="tonal"
:color="category.color"
>
<VIcon
:icon="category.icon"
size="30"
/>
</VAvatar>
</template>
<VListItemTitle>
<h6 class="text-h6 mb-1">
{{ category.title }}
</h6>
</VListItemTitle>
<VListItemSubtitle>
{{ category.subtitle }}
</VListItemSubtitle>
<template #append>
<VRadio :value="category.slug" />
</template>
</VListItem>
</VList>
</VRadioGroup>
</VWindowItem>
<!-- 👉 Frameworks -->
<VWindowItem>
<h5 class="text-h5 mb-4">
Select Framework
</h5>
<VRadioGroup v-model="createAppData.framework">
<VList class="card-list">
<VListItem
v-for="framework in frameworks"
:key="framework.title"
@click="createAppData.framework = framework.slug"
>
<template #prepend>
<VAvatar
size="46"
rounded
variant="tonal"
:color="framework.color"
>
<VIcon
:icon="framework.icon"
size="30"
/>
</VAvatar>
</template>
<VListItemTitle>
<h6 class="text-h6 mb-1">
{{ framework.title }}
</h6>
</VListItemTitle>
<VListItemSubtitle>
{{ framework.subtitle }}
</VListItemSubtitle>
<template #append>
<VRadio :value="framework.slug" />
</template>
</VListItem>
</VList>
</VRadioGroup>
</VWindowItem>
<!-- 👉 Database Engine -->
<VWindowItem>
<AppTextField
label="Database Name"
placeholder="UserDB"
/>
<h5 class="text-h5 mt-6 mb-4">
Select Database Engine
</h5>
<VRadioGroup v-model="createAppData.database">
<VList class="card-list">
<VListItem
v-for="database in databases"
:key="database.title"
@click="createAppData.database = database.slug"
>
<template #prepend>
<VAvatar
size="46"
rounded
variant="tonal"
:color="database.color"
>
<VIcon
:icon="database.icon"
size="30"
/>
</VAvatar>
</template>
<VListItemTitle>
<h6 class="text-h6 mb-1">
{{ database.title }}
</h6>
</VListItemTitle>
<VListItemSubtitle>
{{ database.subtitle }}
</VListItemSubtitle>
<template #append>
<VRadio :value="database.slug" />
</template>
</VListItem>
</VList>
</VRadioGroup>
</VWindowItem>
<!-- 👉 Billing form -->
<VWindowItem>
<h6 class="text-h6 mb-6">
Payment Details
</h6>
<VForm>
<VRow>
<VCol cols="12">
<AppTextField
v-model="createAppData.cardNumber"
label="Card Number"
placeholder="1234 1234 1234 1234"
type="number"
/>
</VCol>
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="createAppData.cardName"
label="Name on Card"
placeholder="John Doe"
/>
</VCol>
<VCol
cols="6"
md="3"
>
<AppTextField
v-model="createAppData.cardExpiry"
label="Expiry"
placeholder="MM/YY"
/>
</VCol>
<VCol
cols="6"
md="3"
>
<AppTextField
v-model="createAppData.cardCvv"
label="CVV"
placeholder="123"
/>
</VCol>
<VCol cols="12">
<VSwitch
v-model="createAppData.isSave"
label="Save Card for future billing?"
/>
</VCol>
</VRow>
</VForm>
</VWindowItem>
<VWindowItem class="text-center">
<h5 class="text-h5 mb-1">
Submit
</h5>
<p class="text-sm mb-4">
Submit to kickstart your project.
</p>
<VImg
:src="laptopGirl"
width="176"
class="mx-auto"
/>
</VWindowItem>
</VWindow>
<div class="d-flex justify-space-between mt-6">
<VBtn
variant="tonal"
color="secondary"
:disabled="currentStep === 0"
@click="currentStep--"
>
<VIcon
icon="tabler-arrow-left"
start
class="flip-in-rtl"
/>
Previous
</VBtn>
<VBtn
v-if="createApp.length - 1 === currentStep"
color="success"
@click="onSubmit"
>
submit
</VBtn>
<VBtn
v-else
@click="currentStep++"
>
Next
<VIcon
icon="tabler-arrow-right"
end
class="flip-in-rtl"
/>
</VBtn>
</div>
</VCol>
</VRow>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.stepper-content .card-list {
--v-card-list-gap: 16px;
}
</style>

View File

@@ -0,0 +1,90 @@
<script setup>
const props = defineProps({
mobileNumber: {
type: String,
required: false,
},
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'update:isDialogVisible',
'submit',
])
const phoneNumber = ref(structuredClone(toRaw(props.mobileNumber)))
const formSubmit = () => {
if (phoneNumber.value) {
emit('submit', phoneNumber.value)
emit('update:isDialogVisible', false)
}
}
const resetPhoneNumber = () => {
phoneNumber.value = structuredClone(toRaw(props.mobileNumber))
emit('update:isDialogVisible', false)
}
const dialogModelValueUpdate = val => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 900"
:model-value="props.isDialogVisible"
@update:model-value="dialogModelValueUpdate"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<h5 class="text-h5 mb-2">
Verify Your Mobile Number for SMS
</h5>
<p class="text-body-1 mb-6">
Enter your mobile phone number with country code and we will send you a verification code.
</p>
<VForm @submit.prevent="() => {}">
<AppTextField
v-model="phoneNumber"
name="mobile"
label="Phone Number"
placeholder="+1 123 456 7890"
type="number"
class="mb-6"
/>
<div class="d-flex flex-wrap justify-end gap-4">
<VBtn
color="secondary"
variant="tonal"
@click="resetPhoneNumber"
>
Cancel
</VBtn>
<VBtn
type="submit"
@click="formSubmit"
>
continue
<VIcon
end
icon="tabler-arrow-right"
class="flip-in-rtl"
/>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,159 @@
<script setup>
import americanExDark from '@images/icons/payments/img/ae-dark.png'
import americanExLight from '@images/icons/payments/img/american-express.png'
import dcDark from '@images/icons/payments/img/dc-dark.png'
import dcLight from '@images/icons/payments/img/dc-light.png'
import jcbDark from '@images/icons/payments/img/jcb-dark.png'
import jcbLight from '@images/icons/payments/img/jcb-light.png'
import masterCardDark from '@images/icons/payments/img/master-dark.png'
import masterCardLight from '@images/icons/payments/img/mastercard.png'
import visaDark from '@images/icons/payments/img/visa-dark.png'
import visaLight from '@images/icons/payments/img/visa-light.png'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDialogVisible'])
const visa = useGenerateImageVariant(visaLight, visaDark)
const masterCard = useGenerateImageVariant(masterCardLight, masterCardDark)
const americanEx = useGenerateImageVariant(americanExLight, americanExDark)
const jcb = useGenerateImageVariant(jcbLight, jcbDark)
const dc = useGenerateImageVariant(dcLight, dcDark)
const dialogVisibleUpdate = val => {
emit('update:isDialogVisible', val)
}
const paymentProvidersData = [
{
title: 'Adyen',
providers: [
visa,
masterCard,
americanEx,
jcb,
dc,
],
},
{
title: '2Checkout',
providers: [
visa,
americanEx,
jcb,
dc,
],
},
{
title: 'Airpay',
providers: [
visa,
americanEx,
masterCard,
jcb,
],
},
{
title: 'Authorize.net',
providers: [
americanEx,
jcb,
dc,
],
},
{
title: 'Bambora',
providers: [
masterCard,
americanEx,
jcb,
],
},
{
title: 'Bambora',
providers: [
visa,
masterCard,
americanEx,
jcb,
dc,
],
},
{
title: 'Chase Paymentech (Orbital)',
providers: [
visa,
americanEx,
jcb,
dc,
],
},
{
title: 'Checkout.com',
providers: [
visa,
masterCard,
],
},
]
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
:width="$vuetify.display.smAndDown ? 'auto' : 900"
@update:model-value="dialogVisibleUpdate"
>
<DialogCloseBtn @click="emit('update:isDialogVisible', false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
Select Payment Providers
</h4>
<p class="text-body-1 text-center mb-6">
Third-party payment providers
</p>
<div
v-for="(item, index) in paymentProvidersData"
:key="index"
>
<div class="d-flex flex-column flex-sm-row justify-space-between gap-4 flex-wrap py-4">
<h6 class="text-h6">
{{ item.title }}
</h6>
<div class="d-flex gap-4 flex-wrap">
<img
v-for="(img, iterator) in item.providers"
:key="iterator"
:src="img.value"
height="30"
width="50"
>
</div>
</div>
<VDivider v-if="index !== paymentProvidersData.length - 1" />
</div>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.refer-link-input {
.v-field--appended {
padding-inline-end: 0;
}
.v-field__append-inner {
padding-block-start: 0.125rem;
}
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup>
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDialogVisible'])
const dialogVisibleUpdate = val => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
:width="$vuetify.display.smAndDown ? 'auto' : 1200"
@update:model-value="dialogVisibleUpdate"
>
<!-- 👉 Dialog close btn -->
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
<VCard class="pricing-dialog pa-2 pa-sm-10">
<VCardText>
<AppPricing md="4" />
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,178 @@
<script setup>
import keyboard from '@images/svg/keyboard.svg'
import paper from '@images/svg/paper-send.svg'
import rocket from '@images/svg/rocket.svg'
import { themeConfig } from '@themeConfig'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDialogVisible'])
const dialogVisibleUpdate = val => {
emit('update:isDialogVisible', val)
}
const referAndEarnSteps = [
{
icon: paper,
title: 'Send Invitation 👍🏻',
subtitle: 'Send your referral link to your friend',
},
{
icon: keyboard,
title: 'Registration 😎',
subtitle: 'Let them register to our services',
},
{
icon: rocket,
title: 'Free Trial 🎉',
subtitle: 'Your friend will get 30 days free trial',
},
]
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
:width="$vuetify.display.smAndDown ? 'auto' : 800"
@update:model-value="dialogVisibleUpdate"
>
<!-- 👉 Dialog close btn -->
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<h4 class="text-h4 text-center mb-2">
Refer & Earn
</h4>
<p class="text-body-1 mb-6 text-center">
Invite your friend to <span class="text-capitalize">{{ themeConfig.app.title }}</span>, if they sign up, you and your friend will get 30 days free trial
</p>
<VRow class="text-center mt-8">
<VCol
v-for="step in referAndEarnSteps"
:key="step.title"
cols="12"
sm="4"
>
<VAvatar
variant="tonal"
size="88"
color="primary"
rounded
>
<VIcon
size="40"
:icon="step.icon"
/>
</VAvatar>
<h5 class="text-h5 mt-4 mb-2">
{{ step.title }}
</h5>
<div>{{ step.subtitle }}</div>
</VCol>
</VRow>
<VDivider class="mt-12 mb-6" />
<h5 class="text-h5 mb-6">
Invite your friends
</h5>
<VForm
class="d-flex align-center flex-wrap gap-4"
@submit.prevent="() => {}"
>
<AppTextField
placeholder="johnDoe@gmail.com"
label="Enter your friend's email address and invite them to join Vuexy 😍"
/>
<VBtn
class="align-self-end"
type="submit"
>
Send
</VBtn>
</VForm>
<h5 class="text-h5 my-6">
Share the referral link
</h5>
<VForm
class="d-flex align-center flex-wrap gap-4"
@submit.prevent="() => {}"
>
<AppTextField
placeholder="http://pixinvent.link"
label="You can also copy and send it or share it on your social media. 🚀"
class="refer-link-input"
>
<template #append-inner>
<VBtn variant="text">
Copy link
</VBtn>
</template>
</AppTextField>
<div class="d-flex align-self-end gap-1">
<VBtn
icon
class="rounded"
color="#3B5998"
size="38"
>
<VIcon
color="white"
icon="tabler-brand-facebook"
size="22"
/>
</VBtn>
<VBtn
icon
class="rounded"
color="#55ACEE"
size="38"
>
<VIcon
color="white"
icon="tabler-brand-twitter"
size="22"
/>
</VBtn>
<VBtn
icon
class="rounded"
color="#007BB6"
size="38"
>
<VIcon
color="white"
icon="tabler-brand-linkedin"
size="22"
/>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.refer-link-input {
.v-field--appended {
padding-inline-end: 0;
}
}
</style>

View File

@@ -0,0 +1,186 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar3 from '@images/avatars/avatar-3.png'
import avatar4 from '@images/avatars/avatar-4.png'
import avatar5 from '@images/avatars/avatar-5.png'
import avatar6 from '@images/avatars/avatar-6.png'
import avatar7 from '@images/avatars/avatar-7.png'
import avatar8 from '@images/avatars/avatar-8.png'
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDialogVisible'])
const dialogVisibleUpdate = val => {
emit('update:isDialogVisible', val)
}
const membersList = [
{
avatar: avatar1,
name: 'Lester Palmer',
email: 'jerrod98@gmail.com',
permission: 'Can Edit',
},
{
avatar: avatar2,
name: 'Mattie Blair',
email: 'prudence.boehm@yahoo.com',
permission: 'Owner',
},
{
avatar: avatar3,
name: 'Marvin Wheeler',
email: 'rumet@jujpejah.net',
permission: 'Can Comment',
},
{
avatar: avatar4,
name: 'Nannie Ford',
email: 'negza@nuv.io',
permission: 'Can View',
},
{
avatar: avatar5,
name: 'Julian Murphy',
email: 'lunebame@umdomgu.net',
permission: 'Can Edit',
},
{
avatar: avatar6,
name: 'Sophie Gilbert',
email: 'ha@sugit.gov',
permission: 'Can View',
},
{
avatar: avatar7,
name: 'Chris Watkins',
email: 'zokap@mak.org',
permission: 'Can Comment',
},
{
avatar: avatar8,
name: 'Adelaide Nichols',
email: 'ujinomu@jigo.com',
permission: 'Can Edit',
},
]
</script>
<template>
<VDialog
:model-value="props.isDialogVisible"
:width="$vuetify.display.smAndDown ? 'auto' : 900"
@update:model-value="dialogVisibleUpdate"
>
<!-- 👉 Dialog close btn -->
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
<VCard class="share-project-dialog pa-2 pa-sm-10">
<VCardText>
<h4 class="text-h4 text-center mb-2">
Share Project
</h4>
<p class="text-body-1 text-center mb-6">
Share project with a team members
</p>
<AppAutocomplete
label="Add Members"
:items="membersList"
item-title="name"
item-value="name"
placeholder="Add project members..."
>
<template #item="{ props: listItemProp, item }">
<VListItem v-bind="listItemProp">
<template #prepend>
<VAvatar
:image="item.raw.avatar"
size="30"
/>
</template>
</VListItem>
</template>
</AppAutocomplete>
<h5 class="text-h5 mb-4 mt-6">
8 Members
</h5>
<VList class="card-list">
<VListItem
v-for="member in membersList"
:key="member.name"
>
<template #prepend>
<VAvatar :image="member.avatar" />
</template>
<VListItemTitle>
{{ member.name }}
</VListItemTitle>
<VListItemSubtitle>
{{ member.email }}
</VListItemSubtitle>
<template #append>
<VBtn
variant="text"
color="secondary"
:icon="$vuetify.display.xs"
>
<span class="d-none d-sm-block me-1">{{ member.permission }}</span>
<VIcon icon="tabler-chevron-down" />
<VMenu activator="parent">
<VList :selected="[member.permission]">
<VListItem
v-for="(item, index) in ['Owner', 'Can Edit', 'Can Comment', 'Can View']"
:key="index"
:value="item"
>
<VListItemTitle>{{ item }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</template>
</VListItem>
</VList>
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-3 mt-6">
<h6 class="text-h6 font-weight-medium d-flex align-start">
<VIcon
icon="tabler-users"
class="me-2"
size="20"
/>
<div>Public to Vuexy - Pixinvent</div>
</h6>
<VBtn
class="text-capitalize"
prepend-icon="tabler-link"
>
Copy Project Link
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
</template>
<style lang="scss">
.share-project-dialog {
.card-list {
--v-card-list-gap: 1rem;
}
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup>
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
default: false,
},
smsCode: {
type: String,
required: false,
default: '',
},
authAppCode: {
type: String,
required: false,
default: '',
},
})
const emit = defineEmits(['update:isDialogVisible'])
const authMethods = [
{
icon: 'tabler-settings',
title: 'Authenticator Apps',
desc: 'Get code from an app like Google Authenticator or Microsoft Authenticator.',
value: 'authApp',
},
{
icon: 'tabler-message',
title: 'SMS',
desc: 'We will send a code via SMS if you need to use your backup login method.',
value: 'sms',
},
]
const selectedMethod = ref('authApp')
const isAuthAppDialogVisible = ref(false)
const isSmsDialogVisible = ref(false)
const openSelectedMethodDialog = () => {
if (selectedMethod.value === 'authApp') {
isAuthAppDialogVisible.value = true
isSmsDialogVisible.value = false
emit('update:isDialogVisible', false)
}
if (selectedMethod.value === 'sms') {
isAuthAppDialogVisible.value = false
isSmsDialogVisible.value = true
emit('update:isDialogVisible', false)
}
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 800"
:model-value="props.isDialogVisible"
@update:model-value="(val) => $emit('update:isDialogVisible', val)"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="$emit('update:isDialogVisible', false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<div class="mb-6">
<h4 class="text-h4 text-center mb-2">
Select Authentication Method
</h4>
<p class="text-body-1 text-center mb-6">
You also need to select a method by which the proxy authenticates to the directory serve.
</p>
<CustomRadios
v-model:selected-radio="selectedMethod"
:radio-content="authMethods"
:grid-column="{ cols: '12' }"
>
<template #default="items">
<div class="d-flex flex-column">
<div class="d-flex gap-1 mb-2">
<VIcon
:icon="items.item.icon"
size="20"
class="text-high-emphasis"
/>
<h6 class="text-h6">
{{ items.item.title }}
</h6>
</div>
<p class="text-body-2 mb-0">
{{ items.item.desc }}
</p>
</div>
</template>
</CustomRadios>
</div>
<div class="d-flex gap-4 justify-center">
<VBtn @click="openSelectedMethodDialog">
submit
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="$emit('update:isDialogVisible', false)"
>
Cancel
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
<AddAuthenticatorAppDialog
v-model:is-dialog-visible="isAuthAppDialogVisible"
:auth-code="props.authAppCode"
/>
<EnableOneTimePasswordDialog
v-model:is-dialog-visible="isSmsDialogVisible"
:mobile-number="props.smsCode"
/>
</template>

View File

@@ -0,0 +1,224 @@
<script setup>
const props = defineProps({
userData: {
type: Object,
required: false,
default: () => ({
id: 0,
fullName: '',
company: '',
role: '',
username: '',
country: '',
contact: '',
email: '',
currentPlan: '',
status: '',
avatar: '',
taskDone: null,
projectDone: null,
taxId: '',
language: '',
}),
},
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'submit',
'update:isDialogVisible',
])
const userData = ref(structuredClone(toRaw(props.userData)))
const isUseAsBillingAddress = ref(false)
watch(() => props, () => {
userData.value = structuredClone(toRaw(props.userData))
})
const onFormSubmit = () => {
emit('update:isDialogVisible', false)
emit('submit', userData.value)
}
const onFormReset = () => {
userData.value = structuredClone(toRaw(props.userData))
emit('update:isDialogVisible', false)
}
const dialogModelValueUpdate = val => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 900"
:model-value="props.isDialogVisible"
@update:model-value="dialogModelValueUpdate"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
<VCard class="pa-sm-10 pa-2">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
Edit User Information
</h4>
<p class="text-body-1 text-center mb-6">
Updating user details will receive a privacy audit.
</p>
<!-- 👉 Form -->
<VForm
class="mt-6"
@submit.prevent="onFormSubmit"
>
<VRow>
<!-- 👉 First Name -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="userData.fullName.split(' ')[0]"
label="First Name"
placeholder="John"
/>
</VCol>
<!-- 👉 Last Name -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="userData.fullName.split(' ')[1]"
label="Last Name"
placeholder="Doe"
/>
</VCol>
<!-- 👉 Username -->
<VCol cols="12">
<AppTextField
v-model="userData.username"
label="Username"
placeholder="john.doe.007"
/>
</VCol>
<!-- 👉 Billing Email -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="userData.email"
label="Email"
placeholder="johndoe@email.com"
/>
</VCol>
<!-- 👉 Status -->
<VCol
cols="12"
md="6"
>
<AppSelect
v-model="userData.status"
label="Status"
placeholder="Active"
:items="['Active', 'Inactive', 'Pending']"
/>
</VCol>
<!-- 👉 Tax Id -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="userData.taxId"
label="Tax ID"
placeholder="123456789"
/>
</VCol>
<!-- 👉 Contact -->
<VCol
cols="12"
md="6"
>
<AppTextField
v-model="userData.contact"
label="Phone Number"
placeholder="+1 9876543210"
/>
</VCol>
<!-- 👉 Language -->
<VCol
cols="12"
md="6"
>
<AppSelect
v-model="userData.language"
closable-chips
chips
multiple
label="Language"
placeholder="English"
:items="['English', 'Spanish', 'French']"
/>
</VCol>
<!-- 👉 Country -->
<VCol
cols="12"
md="6"
>
<AppSelect
v-model="userData.country"
label="Country"
placeholder="United States"
:items="['United States', 'United Kingdom', 'France']"
/>
</VCol>
<!-- 👉 Switch -->
<VCol cols="12">
<VSwitch
v-model="isUseAsBillingAddress"
density="compact"
label="Use as a billing address?"
/>
</VCol>
<!-- 👉 Submit and Cancel -->
<VCol
cols="12"
class="d-flex flex-wrap justify-center gap-4"
>
<VBtn type="submit">
Submit
</VBtn>
<VBtn
color="secondary"
variant="tonal"
@click="onFormReset"
>
Cancel
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,114 @@
<script setup>
const props = defineProps({
isDialogVisible: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:isDialogVisible'])
const selectedPlan = ref('standard')
const plansList = [
{
desc: 'Standard - $99/month',
title: 'Standard',
value: 'standard',
},
{
desc: 'Basic - $0/month',
title: 'Basic',
value: 'basic',
},
{
desc: 'Enterprise - $499/month',
title: 'Enterprise',
value: 'enterprice',
},
{
desc: 'Company - $999/month',
title: 'Company',
value: 'company',
},
]
const isConfirmDialogVisible = ref(false)
const dialogModelValueUpdate = val => {
emit('update:isDialogVisible', val)
}
</script>
<template>
<!-- 👉 upgrade plan -->
<VDialog
:width="$vuetify.display.smAndDown ? 'auto' : 650"
:model-value="props.isDialogVisible"
@update:model-value="dialogModelValueUpdate"
>
<!-- Dialog close btn -->
<DialogCloseBtn @click="dialogModelValueUpdate(false)" />
<VCard class="pa-2 pa-sm-10">
<VCardText>
<!-- 👉 Title -->
<h4 class="text-h4 text-center mb-2">
Upgrade Plan
</h4>
<p class="text-body-1 text-center mb-6">
Choose the best plan for user.
</p>
<div class="d-flex justify-space-between flex-column flex-sm-row gap-4">
<AppSelect
v-model="selectedPlan"
:items="plansList"
label="Choose a plan"
placeholder="Basic"
/>
<VBtn
class="align-self-end"
:block="$vuetify.display.xs"
>
Upgrade
</VBtn>
</div>
<VDivider class="my-6" />
<p class="text-body-1 mb-1">
User current plan is standard plan
</p>
<div class="d-flex justify-space-between align-center flex-wrap">
<div class="d-flex align-center gap-1 me-3">
<sup class="text-body-1 text-primary">$</sup>
<h1 class="text-h1 text-primary">
99
</h1>
<sub class="text-body-2 mt-5">
/ month
</sub>
</div>
<VBtn
color="error"
variant="tonal"
@click="isConfirmDialogVisible = true"
>
Cancel Subscription
</VBtn>
</div>
</VCardText>
<!-- 👉 Confirm Dialog -->
<ConfirmDialog
v-model:is-dialog-visible="isConfirmDialogVisible"
cancel-title="Cancelled"
confirm-title="Unsubscribed!"
confirm-msg="Your subscription cancelled successfully."
confirmation-question="Are you sure to cancel your subscription?"
cancel-msg="Unsubscription Cancelled!!"
/>
</VCard>
</VDialog>
</template>