* 'main' of https://freeleaps.com:3443/products/freeleaps:
  adjust the size of images on blogs and about pages
  implement link content viewer
  catch up on enabling blog APIs in client
  trying to rendering pdf(not succeeded yet)
  add blog retrieving
  display workspace according to correct role in project
  added support for self intro template
  added 'update' and 'cancel' function for self-intra
This commit is contained in:
min.jiang 2024-06-06 20:48:04 +08:00
commit 75298bc3e1
17 changed files with 431 additions and 41 deletions

View File

@ -26,6 +26,7 @@
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"axios": "^1.4.0", "axios": "^1.4.0",
"bootstrap": "^5.3.1", "bootstrap": "^5.3.1",
"buffer": "^6.0.3",
"pdfjs-dist": "^4.2.67", "pdfjs-dist": "^4.2.67",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"quill": "^1.3.7", "quill": "^1.3.7",

View File

@ -14,6 +14,12 @@ export default {
mnx_navToContact() { mnx_navToContact() {
this.$router.push('/contact') this.$router.push('/contact')
}, },
mnx_navToPdfContentViewer(content_id) {
this.$router.push('/pdf-content-viewer/' + content_id)
},
mnx_navToLinkContentViewer(content_link_based64) {
this.$router.push('/link-content-viewer/' + content_link_based64)
},
//common //common
mnx_navToFrontDoor() { mnx_navToFrontDoor() {

View File

@ -1,11 +1,58 @@
<template> <template>
<div class="">About</div> <div class="directories_containter">
<div
class="directory_container"
v-for="(directory, index) in directories"
:key="index"
@click="view_link(directory)"
>
<p>{{ directory.title_text }}</p>
<img class="directory_cover_image" :src="directory.cover_picture" />
<p>{{ directory.summary_text }}</p>
</div>
</div>
</template> </template>
<script> <script>
import { ContentApi } from '../../utils/index'
export default { export default {
name: 'About', name: 'About',
components: {}, components: {},
computed: {}, computed: {},
methods: {} mounted() {
this.retrieve_directories()
},
methods: {
retrieve_directories() {
ContentApi.retrieve_about_directories()
.then((response) => {
this.directories = response.data
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
view_link(directory) {
this.mnx_navToLinkContentViewer(btoa(directory.content_link))
}
},
data() {
return {
directories: null
}
}
} }
</script> </script>
<style scoped lang="scss">
.directories_containter {
@extend .container;
}
.directory_container {
@extend .container;
cursor: pointer;
}
.directory_cover_image{
height: 20vh;
}
</style>

View File

@ -1,11 +1,56 @@
<template> <template>
<div class="">Blogs</div> <div v-if="blogs" class="blogs_containter">
<div class="blog_containter" v-for="(blog, index) in blogs" :key="index" @click="view_blog(blog)">
<h2>{{ blog.blog_name }}</h2>
<img class="blog_cover_image" :src="blog.cover_picture" />
<p v-text="retrieve_summary(blog)"></p>
</div>
</div>
</template> </template>
<script> <script>
import { ContentApi } from '../../utils/index'
export default { export default {
name: 'Blogs', name: 'Blogs',
components: {}, components: {},
computed: {}, computed: {},
methods: {} mounted() {
this.retrieve_blogs()
},
methods: {
retrieve_blogs() {
ContentApi.retrieve_blogs()
.then((response) => {
this.blogs = response.data
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
view_blog(blog) {
this.mnx_navToPdfContentViewer(blog.content_id)
},
retrieve_summary(blog) {
return blog.summary_text
}
},
data() {
return {
blogs: null
}
}
} }
</script> </script>
<style scoped lang="scss">
.blogs_containter {
@extend .container;
}
.blog_containter {
@extend .container;
cursor: pointer;
}
.blog_cover_image {
height: 20vh;
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<div class="content_containter">
<iframe
class="link-iframe"
:src="get_link()"
frameborder="0"
marginheight="0"
marginwidth="0"
max-width="100%"
sandbox="allow-forms allow-modals allow-orientation-lock allow-popups allow-same-origin allow-scripts"
scrolling="no"
style="border: none; max-width: 100%; max-height: 100vh"
allowfullscreen
mozallowfullscreen
msallowfullscreen
webkitallowfullscreen
></iframe>
</div>
</template>
<script>
export default {
name: 'LinkContentViewer',
props: {
content_link_based64: {
required: true,
type: String
}
},
components: {},
computed: {},
mounted() {},
methods: {
get_link() {
return atob(this.content_link_based64)
}
},
data() {
return {}
}
}
</script>
<style scoped lang="scss">
.content_containter {
@extend .container;
}
.link-iframe {
@extend .container;
height: 70vh;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<div class="">ContentViewer</div>
</template>
<script>
import { pdfjsLib } from '../../plugins/index'
import { ContentApi } from '../../utils/index'
import { Buffer } from 'buffer'
export default {
name: 'PdfContentViewer',
props: {
content_id: {
required: true,
type: String
}
},
components: {},
computed: {},
mounted() {
this.retrieve_blog_content()
},
methods: {
retrieve_blog_content() {
ContentApi.retrieve_blog_content(this.content_id)
.then((response) => {
this.content_media_data = response.data
var loadingTask = pdfjsLib.getDocument({
data: Buffer.from(this.content_media_data, 'base64')
})
loadingTask.promise.then(function (pdf) {
//
// Fetch the first page
//
pdf.getPage(1).then(function (page) {
//rendering
})
})
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
}
},
data() {
return {
content_media_data: null
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="userProfile" class="user-profile-container"> <div v-if="userProfile" class="user-profile-container offcanvas-parent">
<div class="account-panel-container" id="account-panel"> <div class="account-panel-container" id="account-panel">
<div class="account-summary-bar-container"> <div class="account-summary-bar-container">
<div class="account-summary-header-container"> <div class="account-summary-header-container">
@ -429,18 +429,41 @@
<label class="personal-self-intro-content-label" for="personal-self-intro-editor" <label class="personal-self-intro-content-label" for="personal-self-intro-editor"
>Self-intro</label >Self-intro</label
> >
<div
v-if="personalOperation.self_intro.is_editing"
class="self-intro-in-header-editing-action-container"
>
<button
class="personal-self-intro-template-button"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvas-template"
aria-controls="offcanvas-template"
>
templates
</button>
</div>
</div>
<div
v-if="personalOperation.self_intro.is_editing"
class="self-intro-in-content-editing-action-container"
>
<button @click="updateSelfIntro()" class="in-context-foward-button">Update</button>
<button @click="stopEdittingSelfintro($event)" class="in-context-back-button">
Cancel
</button>
</div> </div>
<div <div
class="personal-self-intro-content-editor" class="personal-self-intro-content-editor"
id="personal-self-intro-editor" id="personal-self-intro-editor"
ref="personal_self_intro_editor_div"
v-html="personalOperation.self_intro.content_html" v-html="personalOperation.self_intro.content_html"
contenteditable="false" contenteditable="false"
v-tooltip v-tooltip
title="Click to edit" title="Click to edit"
delay='{"show":"500", "hide":"100"}' delay='{"show":"500", "hide":"100"}'
@click="edittingSelfintro($event)" @click="edittingSelfintro($event)"
@blur="stopEdittingSelfintro($event)" @keyup="keyUpOnSelfIntroEditor($event)"
@keyup.enter="blurInput($event)"
></div> ></div>
</div> </div>
</div> </div>
@ -681,19 +704,72 @@
</div> </div>
</div> </div>
</div> </div>
<div
class="offcanvas offcanvas-end offcanvas-container"
tabindex="-1"
id="offcanvas-template"
aria-labelledby="offcanvas-template"
>
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvas-template">Apply self-intro template</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
aria-label="Close"
></button>
</div>
<div class="offcanvas-body">
<div class="accordion" id="template-item-container">
<div
v-for="(template, index) in availableTemplates"
:key="index"
:id="'template' + index"
class="accordion-item"
>
<h2 class="accordion-header" :id="'heading' + index">
<button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#' + 'collapse' + index"
aria-expanded="false"
:aria-controls="'collapse' + index"
>
{{ template.title }}
</button>
</h2>
<div
:id="'collapse' + index"
class="accordion-collapse collapse"
:aria-labelledby="'heading' + index"
data-bs-parent="#template-item-container"
>
<div class="accordion-body">
<button class="select-template-button" @click="selectTemplate(template)">
Apply
</button>
<div class="template-content-textarea" v-html="template.content"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { moneyCollectionTypeEnum } from '../../../types/index' import { moneyCollectionTypeEnum } from '../../../types/index'
import { UserProfileApi, elementHandler } from '../../../utils/index' import { UserProfileApi, elementHandler, textAreaAujuster } from '../../../utils/index'
export default { export default {
name: 'UserProfile', name: 'UserProfile',
props: {}, props: {},
mounted() { mounted() {
this.fetchProfile() this.fetchProfile()
this.fetchTemplates()
}, },
watch: { watch: {
'userProfile.account.basic.first_name': { 'userProfile.account.basic.first_name': {
@ -726,6 +802,7 @@ export default {
userProfile: null, userProfile: null,
message: null, message: null,
accountNeedAttention: false, accountNeedAttention: false,
availableTemplates: null,
identityOperation: { identityOperation: {
first_name: null, first_name: null,
last_name: null, last_name: null,
@ -768,7 +845,8 @@ export default {
personalOperation: { personalOperation: {
self_intro: { self_intro: {
summary: null, summary: null,
content_html: null content_html: '',
is_editing: false
}, },
expected_salary: { expected_salary: {
hourly: 140 hourly: 140
@ -1067,31 +1145,47 @@ export default {
//self-intro //self-intro
edittingSelfintro($event) { edittingSelfintro($event) {
elementHandler.edittingContent($event.target) elementHandler.edittingContent($event.target)
this.personalOperation.self_intro.is_editing = true
$event.target.focus() $event.target.focus()
}, },
stopEdittingSelfintro($event) { stopEdittingSelfintro($event) {
elementHandler.stopEdittingContent($event.target) elementHandler.stopEdittingContent($event.target)
if ( this.personalOperation.self_intro.is_editing = false
!( this.personalOperation.self_intro.content_html =
this.personalOperation.self_intro.content_html ===
this.userProfile.account.basic.self_intro.content_html this.userProfile.account.basic.self_intro.content_html
)
) {
if (!this.personalOperation.self_intro.content_html) {
this.updateSelfIntro(this.personalOperation.self_intro.content_html)
}
}
}, },
updateSelfIntro(content) { keyUpOnSelfIntroEditor($event) {
UserProfileApi.updateSelfIntro(content) let element = $event.target
textAreaAujuster.adjustHight(element)
},
updateSelfIntro() {
this.personalOperation.self_intro.content_html =
this.$refs.personal_self_intro_editor_div.innerHTML
if (!this.personalOperation.self_intro.content_html) {
return
}
UserProfileApi.updateSelfIntro(this.personalOperation.self_intro.content_html)
.then((response) => { .then((response) => {
this.userProfile.account.basic.self_intro = response.data.self_intro this.userProfile.account.basic.self_intro = response.data.self_intro
this.personalOperation.self_intro = response.data.self_intro this.personalOperation.self_intro = response.data.self_intro // makes a reference instead of a copy
}) })
.catch((error) => { .catch((error) => {
this.mnx_backendErrorHandler(error) this.mnx_backendErrorHandler(error)
}) })
}, },
fetchTemplates() {
UserProfileApi.fetchSelfIntroTemplates('')
.then((response) => {
this.availableTemplates = response.data
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
selectTemplate(template) {
this.personalOperation.self_intro.content_html = template.content
},
//payment //payment
updateLocalPaymentData() { updateLocalPaymentData() {
this.paymentOperation.ready_to_receive_money = this.paymentOperation.ready_to_receive_money =
@ -1555,11 +1649,34 @@ export default {
width: fit-content; width: fit-content;
} }
.self-intro-in-header-editing-action-container {
@extend .flex-row-container;
@extend .justify-content-around;
width: fit-content;
}
.self-intro-in-content-editing-action-container {
@extend .float-end;
@extend .flex-row-container;
@extend .justify-content-end;
@extend .w-20;
}
.personal-self-intro-template-button { .personal-self-intro-template-button {
@extend .initiate-button; @extend .initiate-button;
width: fit-content; width: fit-content;
} }
.in-context-foward-button {
@extend .proceed-button;
width: fit-content;
}
.in-context-back-button {
@extend .back-button;
width: fit-content;
}
.personal-self-intro-content-editor { .personal-self-intro-content-editor {
@extend .container; @extend .container;
@extend .border; @extend .border;
@ -1818,4 +1935,8 @@ export default {
@extend .m-auto; @extend .m-auto;
width: fit-content; width: fit-content;
} }
.offcanvas-parent {
position: relative;
}
</style> </style>

View File

@ -284,14 +284,13 @@ export default {
back() { back() {
this.mnx_goBack() this.mnx_goBack()
}, },
coverLetterDone($event) { coverLetterDone(event) {
this.proposal.content = event.target.innerHTML this.proposal.content = event.target.innerHTML
}, },
submit() { submit() {
if (this.uploadFile) { if (this.uploadFile) {
DocumentApi.upload(this.uploadFile) DocumentApi.upload(this.uploadFile)
.then((response) => { .then((response) => {
console.log(response.data.document_id)
return RequestHubApi.makeProposalForRequest( return RequestHubApi.makeProposalForRequest(
this.requestId, this.requestId,
this.proposal.content, this.proposal.content,

View File

@ -693,15 +693,15 @@ export default {
return true return true
} else { } else {
if (milestone.status === milestoneStatusEnum.IMPLEMENTING) { if (milestone.status === milestoneStatusEnum.IMPLEMENTING) {
if (project.current_user_id === project.proposer_id) { if (project.providers.includes(project.current_user_id)) {
return false return false
} }
} else if (milestone.status === milestoneStatusEnum.OUTSTANDING) { } else if (milestone.status === milestoneStatusEnum.OUTSTANDING) {
if (project.current_user_id === project.requester_id) { if (project.issuers.includes(project.current_user_id)) {
return false return false
} }
} else if (milestone.status === milestoneStatusEnum.PAID) { } else if (milestone.status === milestoneStatusEnum.PAID) {
if (project.current_user_id === project.proposer_id) { if (project.providers.includes(project.current_user_id)) {
return false return false
} }
} }
@ -711,7 +711,7 @@ export default {
handleMilestoneAction(project, milestone) { handleMilestoneAction(project, milestone) {
if (milestone.index === project.current_milestone) { if (milestone.index === project.current_milestone) {
if (milestone.status === milestoneStatusEnum.IMPLEMENTING) { if (milestone.status === milestoneStatusEnum.IMPLEMENTING) {
if (project.current_user_id === project.proposer_id) { if (project.providers.includes(project.current_user_id)) {
WorksapceApi.setMillestoneStatus( WorksapceApi.setMillestoneStatus(
project.id, project.id,
milestone.index, milestone.index,
@ -725,7 +725,7 @@ export default {
}) })
} }
} else if (milestone.status === milestoneStatusEnum.OUTSTANDING) { } else if (milestone.status === milestoneStatusEnum.OUTSTANDING) {
if (project.current_user_id === project.requester_id) { if (project.issuers.includes(project.current_user_id)) {
WorksapceApi.createMilestoneCheckoutSession(project.id, milestone.index).then( WorksapceApi.createMilestoneCheckoutSession(project.id, milestone.index).then(
(response) => { (response) => {
if (response.data.result) { if (response.data.result) {
@ -740,7 +740,7 @@ export default {
) )
} }
} else if (milestone.status === milestoneStatusEnum.PAID) { } else if (milestone.status === milestoneStatusEnum.PAID) {
if (project.current_user_id === project.proposer_id) { if (project.providers.includes(project.current_user_id)) {
WorksapceApi.setMillestoneStatus(project.id, milestone.index, milestoneStatusEnum.DONE) WorksapceApi.setMillestoneStatus(project.id, milestone.index, milestoneStatusEnum.DONE)
.then((response) => { .then((response) => {
this.fetchView() this.fetchView()
@ -766,19 +766,19 @@ export default {
return 'Operation Complete' return 'Operation Complete'
} else if (milestone.index === project.current_milestone) { } else if (milestone.index === project.current_milestone) {
if (milestone.status === milestoneStatusEnum.IMPLEMENTING) { if (milestone.status === milestoneStatusEnum.IMPLEMENTING) {
if (project.current_user_id === project.requester_id) { if (project.issuers.includes(project.current_user_id)) {
return 'Wait for completion' return 'Wait for completion'
} else { } else {
return 'Mission complete' return 'Mission complete'
} }
} else if (milestone.status === milestoneStatusEnum.OUTSTANDING) { } else if (milestone.status === milestoneStatusEnum.OUTSTANDING) {
if (project.current_user_id === project.requester_id) { if (project.issuers.includes(project.current_user_id)) {
return 'Payment' return 'Payment'
} else { } else {
return 'Wait for payment' return 'Wait for payment'
} }
} else if (milestone.status === milestoneStatusEnum.PAID) { } else if (milestone.status === milestoneStatusEnum.PAID) {
if (project.current_user_id === project.requester_id) { if (project.issuers.includes(project.current_user_id)) {
return 'Wait for confirmation' return 'Wait for confirmation'
} else { } else {
return 'Confirm receipt' return 'Confirm receipt'
@ -889,17 +889,17 @@ export default {
switch (issue.status) { switch (issue.status) {
case projectIssueStatusEnum.OPEN: case projectIssueStatusEnum.OPEN:
if (button_text === 'Resolve') { if (button_text === 'Resolve') {
return project.current_user_id === project.proposer_id return project.providers.includes(project.current_user_id)
} }
break break
case 1: case 1:
if (button_text === 'Reopen' || button_text === 'Confirm') { if (button_text === 'Reopen' || button_text === 'Confirm') {
return project.current_user_id === project.requester_id return project.issuers.includes(project.current_user_id)
} }
break break
case projectIssueStatusEnum.CLOSED: case projectIssueStatusEnum.CLOSED:
if (button_text === 'Reopen') { if (button_text === 'Reopen') {
return project.current_user_id === project.requester_id return project.issuers.includes(project.current_user_id)
} }
break break
default: default:

View File

@ -1,6 +1,7 @@
import * as pdfjsLib from 'pdfjs-dist' import * as pdfjsLib from 'pdfjs-dist'
//need to set worker before the pdfjs can be used.
//pdfjsLib.GlobalWorkerOptions.workerPort = new Worker() // pdfjsLib.GlobalWorkerOptions.workerPort = new Worker(
//new URL('pdfjs-dist/build/pdf.worker.js', import.meta.url) // new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url)
// )
export { pdfjsLib } export { pdfjsLib }

View File

@ -1,10 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { store, userRoleEnum } from '../store/index' import { store, userRoleEnum } from '../store/index'
//public //public
import About from '../../pages/public/About.vue' import About from '../../pages/public/About.vue'
import Blogs from '../../pages/public/Blogs.vue' import Blogs from '../../pages/public/Blogs.vue'
import Career from '../../pages/public/Career.vue' import Career from '../../pages/public/Career.vue'
import Contact from '../../pages/public/Contact.vue' import Contact from '../../pages/public/Contact.vue'
import PdfContentViewer from '../../pages/public/PdfContentViewer.vue'
import LinkContentViewer from '../../pages/public/LinkContentViewer.vue'
//guest //guest
import FrontDoor from '../../pages/guest/FrontDoor.vue' import FrontDoor from '../../pages/guest/FrontDoor.vue'
@ -107,6 +110,21 @@ const router = createRouter({
components: { default: Contact, footer: FooterGuest, header: HeaderGuest } components: { default: Contact, footer: FooterGuest, header: HeaderGuest }
}, },
{
name: 'pdf-content-viewer',
path: '/pdf-content-viewer/:content_id',
meta: { requiredRoles: [userRoleEnum.NONE] },
components: { default: PdfContentViewer, footer: FooterGuest, header: HeaderGuest },
props: true
},
{
name: 'link-content-viewer',
path: '/link-content-viewer/:content_link_based64',
meta: { requiredRoles: [userRoleEnum.NONE] },
components: { default: LinkContentViewer, footer: FooterGuest, header: HeaderGuest },
props: true
},
// guest // guest
{ {
name: 'front-door', name: 'front-door',

View File

@ -0,0 +1,23 @@
import { backendAxios } from './axios'
class ContentApi {
static retrieve_blogs() {
const request = backendAxios.post('/api/content/retrieve-blogs', {}, {})
return request
}
static retrieve_blog_content(document_id) {
const request = backendAxios.post(
'/api/content/retrieve-blog-content',
{
document_id: document_id
},
{}
)
return request
}
static retrieve_about_directories() {
const request = backendAxios.post('/api/content/retrieve-about-directories', {}, {})
return request
}
}
export { ContentApi }

View File

@ -6,3 +6,4 @@ export { ProviderHubApi } from './providerHub'
export { MessageHubApi } from './messageHub' export { MessageHubApi } from './messageHub'
export { DocumentApi } from './document' export { DocumentApi } from './document'
export { HistoryApi } from './history' export { HistoryApi } from './history'
export { ContentApi } from './content'

View File

@ -208,5 +208,18 @@ class UserProfileApi {
) )
return request return request
} }
static fetchSelfIntroTemplates(tags) {
let jwt = userUtils.getJwtToken()
const request = backendAxios.post(
'/api/user/profile/fetch-templates-for-self-intro',
{
tags: tags
},
{
headers: { Authorization: `Bearer ${jwt}` }
}
)
return request
}
} }
export { UserProfileApi } export { UserProfileApi }

View File

@ -1,3 +1,4 @@
export { textAreaAujuster } from './textArea' export { textAreaAujuster } from './textArea'
export { tooltip } from './tooltip' export { tooltip } from './tooltip'
export { elementHandler } from './element' export { elementHandler } from './element'
export { mediaDataHandler } from './mediaData'

View File

@ -0,0 +1,12 @@
class MediaDataHandler {
retrieveText(mediaData) {
let prefix = 'data:text/plain;base64,'
let pos = mediaData.search(prefix)
if (pos < 0) return ''
return atob(mediaData.substring(pos + prefix.length))
}
}
const mediaDataHandler = new MediaDataHandler()
export { mediaDataHandler }

View File

@ -6,7 +6,8 @@ export {
ProviderHubApi, ProviderHubApi,
MessageHubApi, MessageHubApi,
DocumentApi, DocumentApi,
HistoryApi HistoryApi,
ContentApi
} from './backend/index' } from './backend/index'
export { userUtils, userProfileUtils, requestIssueUtils, requestHubUtils } from './store/index' export { userUtils, userProfileUtils, requestIssueUtils, requestHubUtils } from './store/index'
@ -15,4 +16,4 @@ export { ValueChecker, DateUtils, OIDUtils } from './common/index'
export { userProfileValidator } from './validator/index' export { userProfileValidator } from './validator/index'
export { textAreaAujuster, tooltip, elementHandler } from './html/index' export { textAreaAujuster, tooltip, elementHandler, mediaDataHandler } from './html/index'