This commit is contained in:
2025-05-24 01:49:48 +09:00
commit 62abbcf4eb
2376 changed files with 325522 additions and 0 deletions

View 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>

View 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>&lt; 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>&lt; 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>&lt; 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>