* '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",
"axios": "^1.4.0",
"bootstrap": "^5.3.1",
"buffer": "^6.0.3",
"pdfjs-dist": "^4.2.67",
"pinia": "^2.1.6",
"quill": "^1.3.7",

View File

@ -14,6 +14,12 @@ export default {
mnx_navToContact() {
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
mnx_navToFrontDoor() {

View File

@ -1,11 +1,58 @@
<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>
<script>
import { ContentApi } from '../../utils/index'
export default {
name: 'About',
components: {},
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>
<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>
<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>
<script>
import { ContentApi } from '../../utils/index'
export default {
name: 'Blogs',
components: {},
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>
<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>
<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-summary-bar-container">
<div class="account-summary-header-container">
@ -429,18 +429,41 @@
<label class="personal-self-intro-content-label" for="personal-self-intro-editor"
>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
class="personal-self-intro-content-editor"
id="personal-self-intro-editor"
ref="personal_self_intro_editor_div"
v-html="personalOperation.self_intro.content_html"
contenteditable="false"
v-tooltip
title="Click to edit"
delay='{"show":"500", "hide":"100"}'
@click="edittingSelfintro($event)"
@blur="stopEdittingSelfintro($event)"
@keyup.enter="blurInput($event)"
@keyup="keyUpOnSelfIntroEditor($event)"
></div>
</div>
</div>
@ -681,19 +704,72 @@
</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>
</template>
<script>
import { moneyCollectionTypeEnum } from '../../../types/index'
import { UserProfileApi, elementHandler } from '../../../utils/index'
import { UserProfileApi, elementHandler, textAreaAujuster } from '../../../utils/index'
export default {
name: 'UserProfile',
props: {},
mounted() {
this.fetchProfile()
this.fetchTemplates()
},
watch: {
'userProfile.account.basic.first_name': {
@ -726,6 +802,7 @@ export default {
userProfile: null,
message: null,
accountNeedAttention: false,
availableTemplates: null,
identityOperation: {
first_name: null,
last_name: null,
@ -768,7 +845,8 @@ export default {
personalOperation: {
self_intro: {
summary: null,
content_html: null
content_html: '',
is_editing: false
},
expected_salary: {
hourly: 140
@ -1067,31 +1145,47 @@ export default {
//self-intro
edittingSelfintro($event) {
elementHandler.edittingContent($event.target)
this.personalOperation.self_intro.is_editing = true
$event.target.focus()
},
stopEdittingSelfintro($event) {
elementHandler.stopEdittingContent($event.target)
if (
!(
this.personalOperation.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)
}
}
this.personalOperation.self_intro.is_editing = false
this.personalOperation.self_intro.content_html =
this.userProfile.account.basic.self_intro.content_html
},
updateSelfIntro(content) {
UserProfileApi.updateSelfIntro(content)
keyUpOnSelfIntroEditor($event) {
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) => {
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) => {
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
updateLocalPaymentData() {
this.paymentOperation.ready_to_receive_money =
@ -1555,11 +1649,34 @@ export default {
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 {
@extend .initiate-button;
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 {
@extend .container;
@extend .border;
@ -1818,4 +1935,8 @@ export default {
@extend .m-auto;
width: fit-content;
}
.offcanvas-parent {
position: relative;
}
</style>

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router'
import { store, userRoleEnum } from '../store/index'
//public
import About from '../../pages/public/About.vue'
import Blogs from '../../pages/public/Blogs.vue'
import Career from '../../pages/public/Career.vue'
import Contact from '../../pages/public/Contact.vue'
import PdfContentViewer from '../../pages/public/PdfContentViewer.vue'
import LinkContentViewer from '../../pages/public/LinkContentViewer.vue'
//guest
import FrontDoor from '../../pages/guest/FrontDoor.vue'
@ -107,6 +110,21 @@ const router = createRouter({
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
{
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 { DocumentApi } from './document'
export { HistoryApi } from './history'
export { ContentApi } from './content'

View File

@ -208,5 +208,18 @@ class UserProfileApi {
)
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 }

View File

@ -1,3 +1,4 @@
export { textAreaAujuster } from './textArea'
export { tooltip } from './tooltip'
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,
MessageHubApi,
DocumentApi,
HistoryApi
HistoryApi,
ContentApi
} from './backend/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 { textAreaAujuster, tooltip, elementHandler } from './html/index'
export { textAreaAujuster, tooltip, elementHandler, mediaDataHandler } from './html/index'