mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 21:03:35 +09:00
first
This commit is contained in:
122
src/pages/wizard-v1/index.vue
Normal file
122
src/pages/wizard-v1/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import type { WizardRelatedTo } from '/@src/types/wizard'
|
||||
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 1,
|
||||
})
|
||||
|
||||
const validateStep = (relatedTo: WizardRelatedTo) => {
|
||||
wizard.data.relatedTo = relatedTo
|
||||
|
||||
router.push('/wizard-v1/project-info')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
Select a project type
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="wizard-types">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="wizard-card">
|
||||
<img
|
||||
src="/images/illustrations/wizard/type-1.svg"
|
||||
alt=""
|
||||
>
|
||||
<h3 class="dark-inverted">
|
||||
UI/UX Design
|
||||
</h3>
|
||||
<p>Some short explanation about the type goes here.</p>
|
||||
<div class="button-wrap">
|
||||
<VButton
|
||||
color="primary"
|
||||
class="type-select-button"
|
||||
rounded
|
||||
elevated
|
||||
bold
|
||||
@click="validateStep('UI/UX Design')"
|
||||
>
|
||||
Continue
|
||||
</VButton>
|
||||
</div>
|
||||
<div class="learn-more-link">
|
||||
<a
|
||||
href="#"
|
||||
class="dark-inverted-hover"
|
||||
>Or Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="wizard-card">
|
||||
<img
|
||||
src="/images/illustrations/wizard/type-2.svg"
|
||||
alt=""
|
||||
>
|
||||
<h3 class="dark-inverted">
|
||||
Web Development
|
||||
</h3>
|
||||
<p>Some short explanation about the type goes here.</p>
|
||||
<div class="button-wrap">
|
||||
<VButton
|
||||
color="primary"
|
||||
class="type-select-button"
|
||||
rounded
|
||||
elevated
|
||||
bold
|
||||
@click="validateStep('Web Development')"
|
||||
>
|
||||
Continue
|
||||
</VButton>
|
||||
</div>
|
||||
<div class="learn-more-link">
|
||||
<a
|
||||
href="#"
|
||||
class="dark-inverted-hover"
|
||||
>Or Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="wizard-card">
|
||||
<img
|
||||
src="/images/illustrations/wizard/type-3.svg"
|
||||
alt=""
|
||||
>
|
||||
<h3 class="dark-inverted">
|
||||
Marketing
|
||||
</h3>
|
||||
<p>Some short explanation about the type goes here.</p>
|
||||
<div class="button-wrap">
|
||||
<VButton
|
||||
color="primary"
|
||||
class="type-select-button"
|
||||
rounded
|
||||
elevated
|
||||
bold
|
||||
@click="validateStep('Marketing')"
|
||||
>
|
||||
Continue
|
||||
</VButton>
|
||||
</div>
|
||||
<div class="learn-more-link">
|
||||
<a
|
||||
href="#"
|
||||
class="dark-inverted-hover"
|
||||
>Or Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
212
src/pages/wizard-v1/project-details.vue
Normal file
212
src/pages/wizard-v1/project-details.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import type { WizardCustomer } from '/@src/types/wizard'
|
||||
import { customers } from '/@src/data/wizard'
|
||||
|
||||
const search = ref('')
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 3,
|
||||
canNavigate: true,
|
||||
previousStepFn: async () => {
|
||||
router.push('/wizard-v1/project-info')
|
||||
},
|
||||
validateStepFn: async () => {
|
||||
router.push('/wizard-v1/project-files')
|
||||
},
|
||||
})
|
||||
|
||||
const filteredCustomers = computed<WizardCustomer[]>(() => {
|
||||
if (!search.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return customers
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.name.match(new RegExp(search.value, 'i'))
|
||||
|| item.location.match(new RegExp(search.value, 'i'))
|
||||
)
|
||||
})
|
||||
.splice(0, 4)
|
||||
})
|
||||
|
||||
const selectCustomer = (customer: WizardCustomer | null) => {
|
||||
wizard.data.customer = customer
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
Add more details
|
||||
</h2>
|
||||
<p>Add useful details to your project. You can edit this later.</p>
|
||||
</div>
|
||||
|
||||
<div class="project-customer">
|
||||
<h4>Customer</h4>
|
||||
|
||||
<VField v-if="!wizard.data.customer">
|
||||
<VControl icon="lucide:search">
|
||||
<VInput
|
||||
v-model="search"
|
||||
placeholder="search..."
|
||||
/>
|
||||
</VControl>
|
||||
</VField>
|
||||
|
||||
<VBlock
|
||||
v-if="wizard.data.customer"
|
||||
:title="wizard.data.customer.name"
|
||||
:subtitle="wizard.data.customer.location"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
size="medium"
|
||||
:picture="wizard.data.customer.logo"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<VIconButton
|
||||
size="small"
|
||||
icon="lucide:x"
|
||||
circle
|
||||
@click="selectCustomer(null)"
|
||||
/>
|
||||
</template>
|
||||
</VBlock>
|
||||
|
||||
<template v-else-if="filteredCustomers.length > 0">
|
||||
<TransitionGroup
|
||||
name="list"
|
||||
tag="div"
|
||||
>
|
||||
<VBlock
|
||||
v-for="customer in filteredCustomers"
|
||||
:key="customer.name"
|
||||
:title="customer.name"
|
||||
:subtitle="customer.location"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
size="medium"
|
||||
:picture="customer.logo"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<VIconButton
|
||||
size="small"
|
||||
icon="lucide:plus"
|
||||
circle
|
||||
@click="selectCustomer(customer)"
|
||||
/>
|
||||
</template>
|
||||
</VBlock>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="project-dates">
|
||||
<h4>Project Time Frame</h4>
|
||||
<ClientOnly>
|
||||
<VDatePicker
|
||||
v-model.range="wizard.data.timeFrame"
|
||||
color="green"
|
||||
trim-weeks
|
||||
>
|
||||
<template #default="{ inputValue, inputEvents }">
|
||||
<div class="project-dates-inner">
|
||||
<div class="project-date">
|
||||
<div class="date-icon">
|
||||
<VIcon
|
||||
icon="lucide:map-pin"
|
||||
/>
|
||||
</div>
|
||||
<VControl>
|
||||
<input
|
||||
:value="inputValue.start"
|
||||
class="input form-datepicker"
|
||||
placeholder="Start Date"
|
||||
v-on="inputEvents.start"
|
||||
>
|
||||
</VControl>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<div class="project-date">
|
||||
<div class="date-icon">
|
||||
<VIcon
|
||||
icon="lucide:flag"
|
||||
/>
|
||||
</div>
|
||||
<VControl>
|
||||
<input
|
||||
:value="inputValue.end"
|
||||
class="input form-datepicker"
|
||||
placeholder="End Date"
|
||||
v-on="inputEvents.end"
|
||||
>
|
||||
</VControl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDatePicker>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
<div class="project-budget">
|
||||
<h4>Project Budget</h4>
|
||||
<div class="project-budget-inner">
|
||||
<div class="budget-item">
|
||||
<a
|
||||
class="budget-item-inner"
|
||||
:class="[wizard.data.budget === '< 5K' && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="wizard.data.budget = '< 5K'"
|
||||
@click="wizard.data.budget = '< 5K'"
|
||||
>
|
||||
<span>< 5K</span>
|
||||
</a>
|
||||
<a
|
||||
class="budget-item-inner"
|
||||
:class="[wizard.data.budget === '< 30K' && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="wizard.data.budget = '< 30K'"
|
||||
@click="wizard.data.budget = '< 30K'"
|
||||
>
|
||||
<span>< 30K</span>
|
||||
</a>
|
||||
<a
|
||||
class="budget-item-inner"
|
||||
:class="[wizard.data.budget === '< 100K' && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="wizard.data.budget = '< 100K'"
|
||||
@click="wizard.data.budget = '< 100K'"
|
||||
>
|
||||
<span>< 100K</span>
|
||||
</a>
|
||||
<a
|
||||
class="budget-item-inner"
|
||||
:class="[wizard.data.budget === '100K+' && 'is-active']"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@click="wizard.data.budget = '100K+'"
|
||||
@keydown.enter.prevent="wizard.data.budget = '100K+'"
|
||||
>
|
||||
<span>100K+</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
396
src/pages/wizard-v1/project-files.vue
Normal file
396
src/pages/wizard-v1/project-files.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<script setup lang="ts">
|
||||
import Dropzone from 'dropzone'
|
||||
import 'dropzone/dist/dropzone.css'
|
||||
|
||||
Dropzone.autoDiscover = false
|
||||
|
||||
let isInit = false
|
||||
const isUploading = ref(false)
|
||||
const previewTemplateElement = ref<HTMLElement>()
|
||||
const previewContainerElement = ref<HTMLElement>()
|
||||
const totalProgressElement = ref<HTMLElement>()
|
||||
const addUploadElement = ref<HTMLElement>()
|
||||
const startUploadElement = ref<HTMLElement>()
|
||||
const cancelUploadElement = ref<HTMLElement>()
|
||||
const dropzone = ref<typeof Dropzone>()
|
||||
const previewTemplate = ref('')
|
||||
|
||||
const { onceError } = useImageError()
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 4,
|
||||
canNavigate: true,
|
||||
previousStepFn: async () => {
|
||||
router.push('/wizard-v1/project-details')
|
||||
},
|
||||
validateStepFn: async () => {
|
||||
router.push('/wizard-v1/project-team')
|
||||
},
|
||||
})
|
||||
|
||||
const initDropzone = () => {
|
||||
if (isInit) {
|
||||
return
|
||||
}
|
||||
isInit = true
|
||||
|
||||
// We use dropzone library to handle the file upload
|
||||
// https://docs.dropzone.dev/
|
||||
dropzone.value = new Dropzone(document.body, {
|
||||
// Make the whole body a dropzone
|
||||
url: 'https://www.cssninja.io/upload.php', // Set the url
|
||||
thumbnailWidth: 800,
|
||||
thumbnailHeight: 600,
|
||||
parallelUploads: 2,
|
||||
previewTemplate: previewTemplate.value,
|
||||
autoQueue: false, // Make sure the files aren't queued until manually added
|
||||
previewsContainer: previewContainerElement.value, // Define the container to display the previews
|
||||
clickable: '.fileinput-button', // Define the element that should be used as click trigger to select files.
|
||||
})
|
||||
|
||||
dropzone.value.on('complete', (file: any) => {
|
||||
const attachment = {
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
dataURL: file.dataURL,
|
||||
type: file.type,
|
||||
upload: {
|
||||
uuid: file.upload.uuid,
|
||||
url: file.upload.url,
|
||||
},
|
||||
}
|
||||
wizard.data.attachments.push(attachment)
|
||||
})
|
||||
|
||||
dropzone.value.on('removedfile', (file: any) => {
|
||||
const fileIndex = wizard.data.attachments.findIndex((item) => {
|
||||
return item.upload.uuid === file.upload.uuid
|
||||
})
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
wizard.data.attachments.splice(fileIndex, 1)
|
||||
}
|
||||
})
|
||||
|
||||
dropzone.value.on('addedfile', (file: any) => {
|
||||
const startElement = file.previewElement.querySelector('.start')
|
||||
if (startElement) {
|
||||
startElement.onclick = () => {
|
||||
dropzone.value.enqueueFile(file)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dropzone.value.on('totaluploadprogress', (progress: number) => {
|
||||
if (totalProgressElement.value) {
|
||||
totalProgressElement.value.style.width = `${progress}%`
|
||||
}
|
||||
})
|
||||
|
||||
dropzone.value.on('sending', (file: any) => {
|
||||
const startElement = file.previewElement.querySelector('.start')
|
||||
|
||||
if (totalProgressElement.value) {
|
||||
totalProgressElement.value.style.opacity = '1'
|
||||
}
|
||||
if (startElement) {
|
||||
startElement.disabled = true
|
||||
}
|
||||
})
|
||||
|
||||
dropzone.value.on('queuecomplete', () => {
|
||||
if (totalProgressElement.value) {
|
||||
totalProgressElement.value.style.opacity = '0'
|
||||
}
|
||||
})
|
||||
|
||||
if (startUploadElement.value) {
|
||||
startUploadElement.value.onclick = () => {
|
||||
if (dropzone.value) {
|
||||
const files = dropzone.value.getAddedFiles()
|
||||
dropzone.value.enqueueFiles(files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelUploadElement.value) {
|
||||
cancelUploadElement.value.onclick = () => {
|
||||
if (dropzone.value) {
|
||||
dropzone.value.removeAllFiles(true)
|
||||
}
|
||||
wizard.data.attachments.splice(0, wizard.data.attachments.length)
|
||||
}
|
||||
}
|
||||
|
||||
const minSteps = 6
|
||||
const maxSteps = 60
|
||||
const timeBetweenSteps = 100
|
||||
const bytesPerStep = 1024 * 1024 // 1024 kilooctets upload rate simulation
|
||||
|
||||
dropzone.value.uploadFiles = async (files: any) => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i]
|
||||
const totalSteps = Math.round(
|
||||
Math.min(maxSteps, Math.max(minSteps, file.size / bytesPerStep)),
|
||||
)
|
||||
|
||||
for (let step = 0; step < totalSteps; step++) {
|
||||
const duration = timeBetweenSteps * (step + 1)
|
||||
await sleep(duration)
|
||||
|
||||
file.upload = {
|
||||
...file.upload,
|
||||
progress: (100 * (step + 1)) / totalSteps,
|
||||
bytesSent: ((step + 1) * file.size) / totalSteps,
|
||||
}
|
||||
|
||||
dropzone.value.emit(
|
||||
'uploadprogress',
|
||||
file,
|
||||
file.upload.progress,
|
||||
file.upload.bytesSent,
|
||||
)
|
||||
if (file.upload.progress >= 100) {
|
||||
file.status = Dropzone.SUCCESS
|
||||
file.upload = {
|
||||
url: `https://fake-uploads.cssninja.io/${file.name}`,
|
||||
}
|
||||
|
||||
dropzone.value.emit('success', file, 'success', null)
|
||||
dropzone.value.emit('complete', file)
|
||||
dropzone.value.processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (dropzone.value) {
|
||||
dropzone.value.destroy()
|
||||
isInit = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(isUploading, () => {
|
||||
if (isUploading.value) {
|
||||
nextTick(() => {
|
||||
if (previewTemplateElement.value) {
|
||||
previewTemplate.value = previewTemplateElement.value.outerHTML
|
||||
previewTemplateElement.value.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(previewTemplate, () => {
|
||||
if (previewTemplate.value) {
|
||||
initDropzone()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
Add files to this project
|
||||
</h2>
|
||||
<p>Or you can skip this step. You can always add more files later.</p>
|
||||
</div>
|
||||
|
||||
<!--List Empty Search Placeholder -->
|
||||
<VPlaceholderPage
|
||||
v-if="!isUploading"
|
||||
class="is-files"
|
||||
title="Upload project files"
|
||||
subtitle="You can already start adding files to your project if you have them handy. But don't worry, you'll be able to add and manage files later."
|
||||
larger
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
class="light-image is-rounded"
|
||||
src="/images/illustrations/wizard/upload-placeholder.svg"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
class="dark-image is-rounded"
|
||||
src="/images/illustrations/wizard/upload-placeholder.svg"
|
||||
alt=""
|
||||
>
|
||||
</template>
|
||||
<template #action>
|
||||
<a
|
||||
class="action-link toggle-uploader-link"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown.enter.prevent="isUploading = true"
|
||||
@click="isUploading = true"
|
||||
>
|
||||
Add Files
|
||||
</a>
|
||||
</template>
|
||||
</VPlaceholderPage>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="uploader"
|
||||
>
|
||||
<div class="uploader-toolbar">
|
||||
<div class="left">
|
||||
<div class="uploader-actions">
|
||||
<div class="uploader-action">
|
||||
<span
|
||||
ref="addUploadElement"
|
||||
class="inner-action fileinput-button hint--bubble hint--primary hint--top"
|
||||
data-hint="Add Files"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:plus"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="uploader-action">
|
||||
<button
|
||||
ref="startUploadElement"
|
||||
type="button"
|
||||
class="inner-action start hint--bubble hint--primary hint--top"
|
||||
data-hint="Upload All"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:upload"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="uploader-action">
|
||||
<button
|
||||
ref="cancelUploadElement"
|
||||
type="button"
|
||||
class="inner-action cancel hint--bubble hint--primary hint--top"
|
||||
data-hint="Remove All"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:x"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<!-- The global file processing state -->
|
||||
<div class="fileupload-process">
|
||||
<div
|
||||
ref="totalProgressElement"
|
||||
class="progress progress-striped active"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="0"
|
||||
>
|
||||
<div
|
||||
class="progress-bar progress-bar-success"
|
||||
data-dz-uploadprogress
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uploader-container">
|
||||
<div class="upload-wrapper">
|
||||
<div class="upload-box fileinput-button">
|
||||
<div class="uploader-label">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-cloud-upload"
|
||||
/>
|
||||
<h3>Upload photos/videos</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="previewContainerElement"
|
||||
class="template-list"
|
||||
>
|
||||
<div
|
||||
ref="previewTemplateElement"
|
||||
class="template-list-item"
|
||||
>
|
||||
<div class="preview-box">
|
||||
<!-- This is used as the file preview template -->
|
||||
<div class="preview">
|
||||
<img
|
||||
data-dz-thumbnail
|
||||
alt=""
|
||||
@error.once="onceError($event, 150)"
|
||||
>
|
||||
</div>
|
||||
<div class="list-item-meta">
|
||||
<p
|
||||
class="name"
|
||||
data-dz-name
|
||||
/>
|
||||
<p
|
||||
class="error text-danger"
|
||||
data-dz-errormessage
|
||||
/>
|
||||
</div>
|
||||
<div class="list-item-progress">
|
||||
<p
|
||||
class="size"
|
||||
data-dz-size
|
||||
/>
|
||||
<div
|
||||
class="progress active"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="0"
|
||||
>
|
||||
<div
|
||||
class="progress-bar progress-bar-success"
|
||||
data-dz-uploadprogress
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item-actions">
|
||||
<button
|
||||
class="list-item-action start hint--bubble hint--primary hint--top"
|
||||
data-hint="Upload File"
|
||||
type="button"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:play"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
data-dz-remove
|
||||
class="list-item-action cancel hint--bubble hint--primary hint--top"
|
||||
data-hint="Cancel"
|
||||
type="button"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:arrow-left"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
data-dz-remove
|
||||
type="button"
|
||||
class="list-item-action delete"
|
||||
>
|
||||
<VIcon
|
||||
icon="lucide:trash-2"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
148
src/pages/wizard-v1/project-info.vue
Normal file
148
src/pages/wizard-v1/project-info.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
const notyf = useNotyf()
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 2,
|
||||
canNavigate: true,
|
||||
validateStepFn: async () => {
|
||||
router.push('/wizard-v1/project-details')
|
||||
},
|
||||
})
|
||||
|
||||
const onAddFile = (error: any, fileInfo: any) => {
|
||||
if (error) {
|
||||
notyf.error(`${error.main}: ${error.sub}`)
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
|
||||
const _file = fileInfo.file as File
|
||||
if (_file) {
|
||||
wizard.data.logo = _file
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveFile = (error: any, fileInfo: any) => {
|
||||
if (error) {
|
||||
notyf.error(error)
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(fileInfo)
|
||||
|
||||
wizard.data.logo = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
What is this project about?
|
||||
</h2>
|
||||
<p>Manage better by adding all relevant project information</p>
|
||||
</div>
|
||||
|
||||
<div class="project-info">
|
||||
<div class="project-info-head">
|
||||
<div class="project-avatar-upload">
|
||||
<VField>
|
||||
<VControl>
|
||||
<VFilePond
|
||||
size="small"
|
||||
class="profile-filepond"
|
||||
name="profile_filepond"
|
||||
:chunk-retry-delays="[500, 1000, 3000]"
|
||||
label-idle="<i class='lnil lnil-cloud-upload'></i>"
|
||||
:accepted-file-types="['image/png', 'image/jpeg', 'image/gif']"
|
||||
:image-preview-height="140"
|
||||
:image-resize-target-width="140"
|
||||
:image-resize-target-height="140"
|
||||
image-crop-aspect-ratio="1:1"
|
||||
style-panel-layout="compact circle"
|
||||
style-load-indicator-position="center bottom"
|
||||
style-progress-indicator-position="right bottom"
|
||||
style-button-remove-item-position="left bottom"
|
||||
style-button-process-item-position="right bottom"
|
||||
@addfile="onAddFile"
|
||||
@removefile="onRemoveFile"
|
||||
/>
|
||||
</VControl>
|
||||
<p>
|
||||
<span>Upload a project logo</span>
|
||||
<span>File size cannot exceed 2MB</span>
|
||||
</p>
|
||||
</VField>
|
||||
</div>
|
||||
<div class="project-info">
|
||||
<div class="project-name">
|
||||
<VField>
|
||||
<VControl>
|
||||
<VInput
|
||||
v-model="wizard.data.name"
|
||||
placeholder="Project Name"
|
||||
/>
|
||||
</VControl>
|
||||
</VField>
|
||||
</div>
|
||||
<div class="project-description p-t-10">
|
||||
<VField>
|
||||
<VControl>
|
||||
<VTextarea
|
||||
v-model="wizard.data.description"
|
||||
class="textarea"
|
||||
rows="4"
|
||||
placeholder="Describe your project..."
|
||||
/>
|
||||
<p
|
||||
v-if="wizard.data.description.length === 0"
|
||||
class="help"
|
||||
>
|
||||
Minimum of 50 characters
|
||||
</p>
|
||||
<p
|
||||
v-else-if="wizard.data.description.length === 49"
|
||||
class="help"
|
||||
>
|
||||
{{ 50 - wizard.data.description.length }} character remaining
|
||||
</p>
|
||||
<p
|
||||
v-else-if="wizard.data.description.length < 50"
|
||||
class="help"
|
||||
>
|
||||
{{ 50 - wizard.data.description.length }} characters remaining
|
||||
</p>
|
||||
</VControl>
|
||||
</VField>
|
||||
<VField v-slot="{ id }">
|
||||
<label>Related Industries</label>
|
||||
<VControl>
|
||||
<Multiselect
|
||||
v-model="wizard.data.relatedTo"
|
||||
:attrs="{ id }"
|
||||
label="value"
|
||||
placeholder="Enter something"
|
||||
:options="[
|
||||
{
|
||||
value: 'UI/UX Design',
|
||||
},
|
||||
{
|
||||
value: 'Web Development',
|
||||
},
|
||||
{
|
||||
value: 'Marketing',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</VControl>
|
||||
</VField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
343
src/pages/wizard-v1/project-review.vue
Normal file
343
src/pages/wizard-v1/project-review.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 7,
|
||||
canNavigate: true,
|
||||
previousStepFn: async () => {
|
||||
router.push('/wizard-v1/project-tools')
|
||||
},
|
||||
validateStepFn: async () => {
|
||||
router.push('/wizard-v1/success')
|
||||
},
|
||||
})
|
||||
|
||||
const capitalize = (string: string) => {
|
||||
return string.slice(0, 1).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
const projectInitial = computed(() => {
|
||||
return wizard.data.name.slice(0, 1).toUpperCase() || 'P'
|
||||
})
|
||||
|
||||
const formatedDueDate = computed(() => {
|
||||
return dayjs(wizard.data.timeFrame.end).format('MMM D, YYYY')
|
||||
})
|
||||
|
||||
const projectPicture = ref('')
|
||||
|
||||
watchEffect(async () => {
|
||||
try {
|
||||
projectPicture.value = await new Promise((resolve, reject) => {
|
||||
if (wizard.data.logo) {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(wizard.data.logo)
|
||||
reader.onload = () => resolve(reader.result?.toString() || '')
|
||||
reader.onerror = error => reject(error)
|
||||
}
|
||||
else {
|
||||
projectPicture.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
projectPicture.value = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active" data-step-title="Preview">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
Make sure everything is good
|
||||
</h2>
|
||||
<p>You can go back to previous steps if you need to edit anything.</p>
|
||||
</div>
|
||||
|
||||
<VLoader
|
||||
size="xl"
|
||||
class="project-preview-wrapper"
|
||||
:active="wizard.loading"
|
||||
grey
|
||||
>
|
||||
<div class="project-preview-header">
|
||||
<VAvatar
|
||||
color="h-green"
|
||||
size="big"
|
||||
:initials="projectInitial"
|
||||
:picture="projectPicture"
|
||||
/>
|
||||
|
||||
<h3 class="title is-4 is-narrow is-thin">
|
||||
<span v-if="wizard.data.name">{{ wizard.data.name }}</span>
|
||||
<span v-else>Project Title Goes Here</span>
|
||||
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-info"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="project-preview-body">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 is-tablet-100">
|
||||
<div class="edit-box">
|
||||
<h4>Description</h4>
|
||||
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-info"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
|
||||
<p v-if="wizard.data.description">
|
||||
{{ wizard.data.description }}
|
||||
</p>
|
||||
<p v-else>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis negat?
|
||||
Tamen a proposito, inquam, aberramus. Deinde dolorem quem maximum? Quae
|
||||
duo sunt, unum facit. Quod vestri non item.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6 is-tablet-50">
|
||||
<div class="edit-box">
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
<VBlock
|
||||
:title="wizard.data.relatedTo"
|
||||
subtitle="Project Type"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VIconBox
|
||||
size="medium"
|
||||
color="warning"
|
||||
rounded
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-vector-pen"
|
||||
/>
|
||||
</VIconBox>
|
||||
</template>
|
||||
</VBlock>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6 is-tablet-50">
|
||||
<div class="edit-box">
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-details"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
<VBlock
|
||||
v-if="wizard.data.customer"
|
||||
:title="wizard.data.customer.name"
|
||||
subtitle="Project Customer"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
size="medium"
|
||||
:picture="wizard.data.customer.logo"
|
||||
/>
|
||||
</template>
|
||||
</VBlock>
|
||||
<div
|
||||
v-else
|
||||
class="edit-box-placeholder is-media"
|
||||
>
|
||||
<span>No selected customer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 is-tablet-33">
|
||||
<div class="edit-box">
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-details"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
<div class="estimated-budget">
|
||||
<div class="inner-block">
|
||||
<div class="budget">
|
||||
<span>{{ wizard.data.budget }}</span>
|
||||
</div>
|
||||
<p>Estimated Budget</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 is-tablet-33">
|
||||
<div class="edit-box">
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-details"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
<div class="estimated-due-date">
|
||||
<div class="inner-block">
|
||||
<div class="date">
|
||||
<span>{{ formatedDueDate }}</span>
|
||||
</div>
|
||||
<p>Estimated Due Date</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4 is-tablet-33">
|
||||
<div class="edit-box">
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-files"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
<div class="attachments-count">
|
||||
<div class="inner-block">
|
||||
<div class="attachments">
|
||||
<span v-if="wizard.data.attachments.length">{{
|
||||
wizard.data.attachments.length
|
||||
}}</span>
|
||||
<span v-else>No</span>
|
||||
</div>
|
||||
<p>Attachments</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6 is-tablet-50">
|
||||
<div class="edit-box">
|
||||
<h4>Team</h4>
|
||||
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-team"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
|
||||
<div
|
||||
v-if="wizard.data.teammates.length === 0"
|
||||
class="edit-box-placeholder is-media"
|
||||
>
|
||||
<span>No selected teammate</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="media-list"
|
||||
>
|
||||
<div
|
||||
v-for="teammate in wizard.data.teammates"
|
||||
:key="teammate.name"
|
||||
class="media-list-item"
|
||||
>
|
||||
<VBlock
|
||||
:title="teammate.name"
|
||||
:subtitle="capitalize(teammate.role)"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar :picture="teammate.picture" />
|
||||
</template>
|
||||
</VBlock>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6 is-tablet-50">
|
||||
<div class="edit-box">
|
||||
<h4>Tools</h4>
|
||||
|
||||
<RouterLink
|
||||
class="edit-icon"
|
||||
to="/wizard-v1/project-tools"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="lnil lnil-pencil"
|
||||
/>
|
||||
</RouterLink>
|
||||
|
||||
<div
|
||||
v-if="wizard.data.tools.length === 0"
|
||||
class="edit-box-placeholder is-list"
|
||||
>
|
||||
<span>No selected tools</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="media-list"
|
||||
>
|
||||
<div
|
||||
v-for="tool in wizard.data.tools"
|
||||
:key="tool.name"
|
||||
class="media-list-item"
|
||||
>
|
||||
<VBlock
|
||||
:title="tool.name"
|
||||
:subtitle="tool.description"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar :picture="tool.logo" />
|
||||
</template>
|
||||
</VBlock>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VLoader>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
298
src/pages/wizard-v1/project-team.vue
Normal file
298
src/pages/wizard-v1/project-team.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import type { WizardTeammate, WizardTeammateRole } from '/@src/types/wizard'
|
||||
import { users } from '/@src/data/wizard'
|
||||
|
||||
const search = ref('')
|
||||
const isAddingMembers = ref(false)
|
||||
const filteredUsers = ref<Omit<WizardTeammate, 'role'>[]>([])
|
||||
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 5,
|
||||
canNavigate: true,
|
||||
previousStepFn: async () => {
|
||||
router.push('/wizard-v1/project-files')
|
||||
},
|
||||
validateStepFn: async () => {
|
||||
if (search.value) return
|
||||
|
||||
router.push('/wizard-v1/project-tools')
|
||||
},
|
||||
})
|
||||
|
||||
const addTeammate = (teammate: Omit<WizardTeammate, 'role'>) => {
|
||||
wizard.data.teammates.push({
|
||||
...teammate,
|
||||
role: 'reader',
|
||||
})
|
||||
search.value = ''
|
||||
}
|
||||
|
||||
const setTeammateRole = (
|
||||
teammate: Omit<WizardTeammate, 'role'>,
|
||||
role: WizardTeammateRole,
|
||||
) => {
|
||||
const index = wizard.data.teammates.findIndex((item) => {
|
||||
return item.name === teammate.name
|
||||
})
|
||||
|
||||
if (index > -1) {
|
||||
wizard.data.teammates[index].role = role
|
||||
}
|
||||
}
|
||||
|
||||
const removeTeammate = (teammate: Omit<WizardTeammate, 'role'>) => {
|
||||
const index = wizard.data.teammates.findIndex((item) => {
|
||||
return item.name === teammate.name
|
||||
})
|
||||
if (index > -1) {
|
||||
wizard.data.teammates.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleLevel = (teammate: WizardTeammate) => {
|
||||
switch (teammate.role) {
|
||||
case 'collaborator':
|
||||
return 1
|
||||
case 'manager':
|
||||
return 2
|
||||
case 'owner':
|
||||
return 3
|
||||
case 'reader':
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (!search.value) {
|
||||
filteredUsers.value = []
|
||||
return
|
||||
}
|
||||
|
||||
filteredUsers.value = users
|
||||
.filter((item) => {
|
||||
return !wizard.data.teammates.find((_item) => {
|
||||
return item.name === _item.name
|
||||
})
|
||||
})
|
||||
.filter(item => item.name.match(new RegExp(search.value, 'i')))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
Who will be working on this project?
|
||||
</h2>
|
||||
<p>Start by adding members to your team</p>
|
||||
</div>
|
||||
|
||||
<!--List Empty Search Placeholder -->
|
||||
<VPlaceholderPage
|
||||
v-if="!isAddingMembers"
|
||||
class="is-people"
|
||||
title="Invite People"
|
||||
subtitle="You can already start adding files to your project if you have them handy. But
|
||||
don't worry, you'll be able to add and manage files later."
|
||||
larger
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
class="light-image is-rounded"
|
||||
src="/images/illustrations/wizard/team-placeholder.svg"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
class="dark-image is-rounded"
|
||||
src="/images/illustrations/wizard/team-placeholder.svg"
|
||||
alt=""
|
||||
>
|
||||
</template>
|
||||
<template #action>
|
||||
<a
|
||||
role="button"
|
||||
class="action-link toggle-members-link"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="isAddingMembers = true"
|
||||
@click="isAddingMembers = true"
|
||||
>
|
||||
Add Members
|
||||
</a>
|
||||
</template>
|
||||
</VPlaceholderPage>
|
||||
|
||||
<div
|
||||
v-if="isAddingMembers"
|
||||
class="project-team-wrapper"
|
||||
>
|
||||
<div class="project-team-header">
|
||||
<VAvatar
|
||||
size="big"
|
||||
picture="/images/avatars/svg/vuero-1.svg"
|
||||
badge="/images/icons/flags/united-states-of-america.svg"
|
||||
/>
|
||||
<h3 class="title is-4 is-narrow is-thin">
|
||||
Erik Kovalsky
|
||||
</h3>
|
||||
<p class="light-text">
|
||||
You are the project owner
|
||||
</p>
|
||||
|
||||
<VField class="mt-4">
|
||||
<VControl icon="lucide:search">
|
||||
<VInput
|
||||
v-model="search"
|
||||
type="search"
|
||||
placeholder="Search teammates..."
|
||||
/>
|
||||
</VControl>
|
||||
</VField>
|
||||
</div>
|
||||
|
||||
<div class="project-team-body">
|
||||
<div class="members-list">
|
||||
<template v-if="filteredUsers.length > 0">
|
||||
<TransitionGroup
|
||||
name="list"
|
||||
tag="div"
|
||||
>
|
||||
<VBlock
|
||||
v-for="teammate in filteredUsers"
|
||||
:key="teammate.name"
|
||||
class="invited-member"
|
||||
title="Invite"
|
||||
:subtitle="teammate.name"
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
size="medium"
|
||||
:picture="teammate.picture"
|
||||
/>
|
||||
</template>
|
||||
<template #action>
|
||||
<div class="actions">
|
||||
<VIconButton
|
||||
icon="fas fa-plus"
|
||||
class="cancel-button hint--top hint--bubble hint--primary"
|
||||
:aria-label="`Invite ${teammate.name}`"
|
||||
circle
|
||||
@click="addTeammate(teammate)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VBlock>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
<template v-if="wizard.data.teammates.length > 0">
|
||||
<TransitionGroup
|
||||
name="list-complete"
|
||||
tag="div"
|
||||
>
|
||||
<VBlock
|
||||
v-for="teammate in wizard.data.teammates"
|
||||
:key="teammate.name"
|
||||
class="invited-member"
|
||||
title="Invited"
|
||||
:subtitle="teammate.name"
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar
|
||||
size="medium"
|
||||
:picture="teammate.picture"
|
||||
/>
|
||||
</template>
|
||||
<template #action>
|
||||
<div class="actions">
|
||||
<div class="permissions">
|
||||
<div class="permission-levels">
|
||||
<div
|
||||
class="permission-level hint--bubble hint--primary hint--top"
|
||||
aria-label="Reader"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="setTeammateRole(teammate, 'reader')"
|
||||
@click="setTeammateRole(teammate, 'reader')"
|
||||
>
|
||||
<div
|
||||
class="permission-level-inner"
|
||||
:class="[getRoleLevel(teammate) >= 0 && 'is-active']"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="permission-level hint--bubble hint--primary hint--top"
|
||||
aria-label="Collaborator"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="
|
||||
setTeammateRole(teammate, 'collaborator')
|
||||
"
|
||||
@click="setTeammateRole(teammate, 'collaborator')"
|
||||
>
|
||||
<div
|
||||
class="permission-level-inner"
|
||||
:class="[getRoleLevel(teammate) >= 1 && 'is-active']"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="permission-level hint--bubble hint--primary hint--top"
|
||||
aria-label="Manager"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="setTeammateRole(teammate, 'manager')"
|
||||
@click="setTeammateRole(teammate, 'manager')"
|
||||
>
|
||||
<div
|
||||
class="permission-level-inner"
|
||||
:class="[getRoleLevel(teammate) >= 2 && 'is-active']"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="permission-level hint--bubble hint--primary hint--top"
|
||||
aria-label="Owner"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent="setTeammateRole(teammate, 'owner')"
|
||||
@click="setTeammateRole(teammate, 'owner')"
|
||||
>
|
||||
<div
|
||||
class="permission-level-inner"
|
||||
:class="[getRoleLevel(teammate) >= 3 && 'is-active']"
|
||||
/>
|
||||
</div>
|
||||
<progress
|
||||
class="progress permissions-progress is-primary is-tiny"
|
||||
:value="getRoleLevel(teammate)"
|
||||
:max="3"
|
||||
>
|
||||
0%
|
||||
</progress>
|
||||
</div>
|
||||
</div>
|
||||
<VIconButton
|
||||
icon="fas fa-times"
|
||||
class="cancel-button hint--top hint--bubble hint--primary"
|
||||
aria-label="Cancel Invite"
|
||||
circle
|
||||
@click="removeTeammate(teammate)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VBlock>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="empty-wrap has-text-centered"
|
||||
>
|
||||
<span>No team members yet</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
74
src/pages/wizard-v1/project-tools.vue
Normal file
74
src/pages/wizard-v1/project-tools.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { tools } from '/@src/data/wizard'
|
||||
|
||||
const wizard = useWizard()
|
||||
const router = useRouter()
|
||||
wizard.setStep({
|
||||
number: 6,
|
||||
canNavigate: true,
|
||||
previousStepFn: async () => {
|
||||
router.push('/wizard-v1/project-team')
|
||||
},
|
||||
validateStepFn: async () => {
|
||||
router.push('/wizard-v1/project-review')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
What tools will you be using?
|
||||
</h2>
|
||||
<p>Choose a set of tools that you'll be using in this project.</p>
|
||||
</div>
|
||||
|
||||
<div class="tools-wrapper">
|
||||
<div class="columns is-multiline">
|
||||
<!--Tool-->
|
||||
<VField
|
||||
v-for="tool in tools"
|
||||
:key="tool.name"
|
||||
v-slot="{ id }"
|
||||
raw
|
||||
class="column is-4"
|
||||
>
|
||||
<VLabel
|
||||
tabindex="0"
|
||||
class="tool-card"
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
v-model="wizard.data.tools"
|
||||
tabindex="-1"
|
||||
type="checkbox"
|
||||
:value="tool"
|
||||
>
|
||||
|
||||
<div class="tool-card-inner">
|
||||
<VBlock
|
||||
:title="tool.name"
|
||||
:subtitle="tool.description"
|
||||
center
|
||||
>
|
||||
<template #icon>
|
||||
<VAvatar :picture="tool.logo" />
|
||||
</template>
|
||||
<template #action>
|
||||
<div class="checkmark">
|
||||
<VIcon
|
||||
icon="lucide:check"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VBlock>
|
||||
</div>
|
||||
</VLabel>
|
||||
</VField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
51
src/pages/wizard-v1/success.vue
Normal file
51
src/pages/wizard-v1/success.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
const wizard = useWizard()
|
||||
wizard.setStep({
|
||||
number: 8,
|
||||
canNavigate: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inner-wrapper is-active" data-step-title="Finish">
|
||||
<div class="step-content">
|
||||
<div class="step-title">
|
||||
<h2 class="dark-inverted">
|
||||
Congrats! You're all set.
|
||||
</h2>
|
||||
<p>Awesome, you just finished creating this project.</p>
|
||||
</div>
|
||||
|
||||
<VPlaceholderPage
|
||||
class="end-placeholder"
|
||||
title="Get ready for next steps."
|
||||
subtitle="You, and the team members you've added can already start working and creating tasks."
|
||||
larger
|
||||
>
|
||||
<template #image>
|
||||
<img
|
||||
class="light-image"
|
||||
src="/images/illustrations/wizard/finish.svg"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
class="dark-image"
|
||||
src="/images/illustrations/wizard/finish-dark.svg"
|
||||
alt=""
|
||||
>
|
||||
</template>
|
||||
<template #action>
|
||||
<VButton
|
||||
color="primary"
|
||||
rounded
|
||||
bold
|
||||
elevated
|
||||
to="/sidebar/layouts/projects-details"
|
||||
>
|
||||
View Project
|
||||
</VButton>
|
||||
</template>
|
||||
</VPlaceholderPage>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user