|
|
|
|
@ -35,8 +35,20 @@ |
|
|
|
|
v-model="course.title"></textarea> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="info__field field field_info" |
|
|
|
|
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }"> |
|
|
|
|
<div class="field__label">КРАТКО О КУРСЕ</div> |
|
|
|
|
<div class="field__wrap"> |
|
|
|
|
<textarea class="field__input" |
|
|
|
|
rows="1" |
|
|
|
|
placeholder="Кратко о курсе" |
|
|
|
|
v-autosize="course.short_description" |
|
|
|
|
@input="$v.course.short_description.$touch()" |
|
|
|
|
v-model="course.short_description"></textarea> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="info__foot"> |
|
|
|
|
<div class="info__foot" v-if="!live"> |
|
|
|
|
<div class="info__field field field_info info__field--light" |
|
|
|
|
v-bind:class="{ error: ($v.course.category.$dirty || showErrors) && $v.course.category.$invalid }"> |
|
|
|
|
<div class="field__label field__label_gray">КАТЕГОРИЯ</div> |
|
|
|
|
@ -72,7 +84,22 @@ |
|
|
|
|
</div> |
|
|
|
|
<div class="field__wrap field__wrap--additional">{{ courseFullUrl }}</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="info__field field"> |
|
|
|
|
|
|
|
|
|
<div v-if="live" class="info__field field"> |
|
|
|
|
<div class="field__label field__label_gray">ССЫЛКА НА VIMEO</div> |
|
|
|
|
<div class="field__wrap"> |
|
|
|
|
<input type="text" class="field__input" v-model="course.stream_url"> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div v-if="live" class="info__field field"> |
|
|
|
|
<div class="field__label">ДАТА</div> |
|
|
|
|
<div class="field__wrap"> |
|
|
|
|
<lil-select :value.sync="course.time" :options="dateOptions" placeholder="Выберите дату"/> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div v-if="!live" class="info__field field"> |
|
|
|
|
<div class="field__label field__label_gray">ДОСТУП</div> |
|
|
|
|
<div class="field__wrap"> |
|
|
|
|
<label class="field__switch switch switch_lg switch_circle"> |
|
|
|
|
@ -85,12 +112,12 @@ |
|
|
|
|
</label> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<label class="info__switch switch switch_lg"> |
|
|
|
|
<label v-if="!live" class="info__switch switch switch_lg"> |
|
|
|
|
<input type="checkbox" class="switch__input" v-model="course.is_featured"> |
|
|
|
|
<span class="switch__content">Выделить</span> |
|
|
|
|
</label> |
|
|
|
|
</div> |
|
|
|
|
<div class="info__fieldset"> |
|
|
|
|
<div v-if="!live" class="info__fieldset"> |
|
|
|
|
<div class="info__field field"> |
|
|
|
|
<div class="field__label field__label_gray">ЗАПУСК</div> |
|
|
|
|
<div class="field__wrap"> |
|
|
|
|
@ -123,23 +150,10 @@ |
|
|
|
|
<div class="section"> |
|
|
|
|
<div class="section__center center"> |
|
|
|
|
<div class="kit"> |
|
|
|
|
<div class="kit__section"> |
|
|
|
|
<div class="kit__field field" |
|
|
|
|
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }"> |
|
|
|
|
<div class="field__wrap"> |
|
|
|
|
<textarea class="field__input" |
|
|
|
|
rows="1" |
|
|
|
|
placeholder="Кратко о курсе" |
|
|
|
|
v-autosize="course.short_description" |
|
|
|
|
@input="$v.course.short_description.$touch()" |
|
|
|
|
v-model="course.short_description"></textarea> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div id="course-redactor__nav" class="kit__nav"> |
|
|
|
|
<div v-if="!live" id="course-redactor__nav" class="kit__nav"> |
|
|
|
|
<button class="kit__btn btn btn_lg" |
|
|
|
|
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }" |
|
|
|
|
type="button" @click="viewSection = 'course'">Описание |
|
|
|
|
type="button" @click="showCourse">Описание |
|
|
|
|
курса |
|
|
|
|
</button> |
|
|
|
|
<button class="kit__btn btn btn_lg" |
|
|
|
|
@ -151,41 +165,43 @@ |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
<div v-if="viewSection === 'course'" class="kit__body"> |
|
|
|
|
<div v-for="(block, index) in course.content"> |
|
|
|
|
<block-text v-if="block.type === 'text'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:text.sync="block.data.text" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-image-text v-if="block.type === 'image-text'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:text.sync="block.data.text" |
|
|
|
|
:image-id.sync="block.data.image_id" |
|
|
|
|
:image-url.sync="block.data.image_url" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-image v-if="block.type === 'image'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:image-id.sync="block.data.image_id" |
|
|
|
|
:image-url.sync="block.data.image_url" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-images v-if="block.type === 'images'" |
|
|
|
|
<vue-draggable v-model="course.content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> |
|
|
|
|
<div v-for="(block, index) in course.content" :key="block.data.id ? block.data.id : block.data.guid"> |
|
|
|
|
<block-text v-if="block.type === 'text'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:text.sync="block.data.text" |
|
|
|
|
:images.sync="block.data.images" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-video v-if="block.type === 'video'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:video-url.sync="block.data.video_url"/> |
|
|
|
|
</div> |
|
|
|
|
<block-image-text v-if="block.type === 'image-text'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:text.sync="block.data.text" |
|
|
|
|
:image-id.sync="block.data.image_id" |
|
|
|
|
:image-url.sync="block.data.image_url" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-image v-if="block.type === 'image'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:image-id.sync="block.data.image_id" |
|
|
|
|
:image-url.sync="block.data.image_url" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-images v-if="block.type === 'images'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
:text.sync="block.data.text" |
|
|
|
|
:images.sync="block.data.images" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:access-token="accessToken"/> |
|
|
|
|
<block-video v-if="block.type === 'video'" |
|
|
|
|
:index="index" |
|
|
|
|
:title.sync="block.data.title" |
|
|
|
|
v-on:remove="onBlockRemoved" |
|
|
|
|
:video-url.sync="block.data.video_url"/> |
|
|
|
|
</div> |
|
|
|
|
</vue-draggable> |
|
|
|
|
|
|
|
|
|
<block-add v-on:added="onBlockAdded"/> |
|
|
|
|
|
|
|
|
|
@ -257,10 +273,15 @@ |
|
|
|
|
import $ from 'jquery'; |
|
|
|
|
import {required, minValue, numeric } from 'vuelidate/lib/validators' |
|
|
|
|
import slugify from 'slugify'; |
|
|
|
|
import Draggable from 'vuedraggable'; |
|
|
|
|
import {showNotification} from "../js/modules/notification"; |
|
|
|
|
import createHistory from "history/createBrowserHistory"; |
|
|
|
|
|
|
|
|
|
const history = createHistory(); |
|
|
|
|
|
|
|
|
|
export default { |
|
|
|
|
name: "course-redactor", |
|
|
|
|
props: ["authorName", "authorPicture", "accessToken", "courseId"], |
|
|
|
|
props: ["authorName", "authorPicture", "accessToken", "courseId", "live"], |
|
|
|
|
data() { |
|
|
|
|
return { |
|
|
|
|
viewSection: 'course', |
|
|
|
|
@ -295,7 +316,11 @@ |
|
|
|
|
lessons: [], |
|
|
|
|
lessonsLoading: false, |
|
|
|
|
lessonSaving: false, |
|
|
|
|
currentLesson: null, |
|
|
|
|
currentLesson: { |
|
|
|
|
title: '', |
|
|
|
|
short_description: '', |
|
|
|
|
content: [], |
|
|
|
|
}, |
|
|
|
|
is_adding_block: false, |
|
|
|
|
timeOptions: [ |
|
|
|
|
{ |
|
|
|
|
@ -335,6 +360,25 @@ |
|
|
|
|
'value': '18:00', |
|
|
|
|
} |
|
|
|
|
], |
|
|
|
|
|
|
|
|
|
dateOptions: [ |
|
|
|
|
{ |
|
|
|
|
'title': 'Акварельс (Понедельник, 2 апр)', |
|
|
|
|
'value': 'Акварельс (Понедельник, 2 апр)', |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
'title': 'Рельсотрон (Вторник, 3 апр)', |
|
|
|
|
'value': 'Рельсотрон (Вторник, 3 апр)', |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
'title': 'Коломёт (Среда, 4 апр)', |
|
|
|
|
'value': 'Коломёт (Среда, 4 апр)', |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
'title': 'Зиккурат (Четверг, 5 апр)', |
|
|
|
|
'value': 'Зиккурат (Четверг, 5 апр)', |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
showErrors: false, |
|
|
|
|
savingTimeout: null, |
|
|
|
|
savingDebounceTimeout: null, |
|
|
|
|
@ -422,18 +466,30 @@ |
|
|
|
|
}, |
|
|
|
|
editLesson(lessonIndex) { |
|
|
|
|
this.currentLesson = this.lessons[lessonIndex]; |
|
|
|
|
history.push("/course/create/lessons/edit/"+this.currentLesson.id); |
|
|
|
|
this.viewSection = 'lessons-edit'; |
|
|
|
|
}, |
|
|
|
|
showCourse() { |
|
|
|
|
if (this.viewSection !== 'course') { |
|
|
|
|
history.push("/course/create"); |
|
|
|
|
} |
|
|
|
|
this.viewSection = 'course' |
|
|
|
|
}, |
|
|
|
|
showLessons() { |
|
|
|
|
if (this.viewSection !== 'lessons') { |
|
|
|
|
history.push("/course/create/lessons"); |
|
|
|
|
} |
|
|
|
|
this.viewSection = 'lessons'; |
|
|
|
|
}, |
|
|
|
|
addLesson() { |
|
|
|
|
this.currentLesson = { |
|
|
|
|
title: '', |
|
|
|
|
short_description: '', |
|
|
|
|
course_id: this.course.id, |
|
|
|
|
content: [], |
|
|
|
|
}; |
|
|
|
|
if (this.viewSection !== 'lessons-edit') { |
|
|
|
|
history.push("/course/create/lessons/new"); |
|
|
|
|
} |
|
|
|
|
this.viewSection = 'lessons-edit'; |
|
|
|
|
window.scrollTo(0, 0); |
|
|
|
|
}, |
|
|
|
|
@ -451,6 +507,7 @@ |
|
|
|
|
onLessonSubmit() { |
|
|
|
|
this.lessonSaving = true; |
|
|
|
|
const currentLessonId = this.currentLesson.id; |
|
|
|
|
this.currentLesson.course_id = this.course.id; |
|
|
|
|
api.saveLesson(this.currentLesson, this.accessToken) |
|
|
|
|
.then((response) => { |
|
|
|
|
this.lessonSaving = false; |
|
|
|
|
@ -473,6 +530,7 @@ |
|
|
|
|
}); |
|
|
|
|
}, |
|
|
|
|
goToLessons() { |
|
|
|
|
history.push("/course/create/lessons"); |
|
|
|
|
this.viewSection = 'lessons'; |
|
|
|
|
this.$nextTick(() => { |
|
|
|
|
const elementTop = $('#course-redactor__nav').position().top - 130; |
|
|
|
|
@ -481,7 +539,9 @@ |
|
|
|
|
}, |
|
|
|
|
loadCourseDraft() { |
|
|
|
|
this.courseLoading = true; |
|
|
|
|
api.getCourseDraft(this.accessToken) |
|
|
|
|
let response = api.getCourseDraft(this.accessToken); |
|
|
|
|
|
|
|
|
|
response |
|
|
|
|
.then((response) => { |
|
|
|
|
this.courseLoading = false; |
|
|
|
|
this.course = api.convertCourseJson(response.data); |
|
|
|
|
@ -493,10 +553,13 @@ |
|
|
|
|
this.courseLoading = false; |
|
|
|
|
console.log('error course loading', err); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return response; |
|
|
|
|
}, |
|
|
|
|
loadCourse() { |
|
|
|
|
this.courseLoading = true; |
|
|
|
|
api.loadCourse(this.courseId, this.accessToken) |
|
|
|
|
let response = api.loadCourse(this.courseId, this.accessToken) |
|
|
|
|
response |
|
|
|
|
.then((response) => { |
|
|
|
|
this.courseLoading = false; |
|
|
|
|
this.course = api.convertCourseJson(response.data); |
|
|
|
|
@ -508,6 +571,8 @@ |
|
|
|
|
this.courseLoading = false; |
|
|
|
|
console.log('error course loading', err); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return response; |
|
|
|
|
}, |
|
|
|
|
loadLessons(courseId) { |
|
|
|
|
|
|
|
|
|
@ -515,6 +580,7 @@ |
|
|
|
|
onCoursePublish() { |
|
|
|
|
this.showErrors = true; |
|
|
|
|
if (this.$v.$invalid) { |
|
|
|
|
showNotification("error", "Заполните все необходимые поля"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const publishButton = $('#course-redactor__publish-button'); |
|
|
|
|
@ -549,7 +615,7 @@ |
|
|
|
|
this.savingDebounceTimeout = setTimeout(() => { |
|
|
|
|
this.courseSaving = true; |
|
|
|
|
clearTimeout(this.savingTimeout); |
|
|
|
|
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ'; |
|
|
|
|
document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ...'; |
|
|
|
|
const courseObject = this.course; |
|
|
|
|
courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url; |
|
|
|
|
api.saveCourse(courseObject, this.accessToken) |
|
|
|
|
@ -567,7 +633,17 @@ |
|
|
|
|
if (this.course.is_deferred) { |
|
|
|
|
courseData.is_deferred = true; |
|
|
|
|
} |
|
|
|
|
this.course = courseData; |
|
|
|
|
let remoteUUIDMapper = {} |
|
|
|
|
if (courseData.content) { |
|
|
|
|
courseData.content.forEach((contentElement) => { |
|
|
|
|
remoteUUIDMapper[contentElement.uuid] = contentElement.data.id |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
this.course.content.forEach((contentElement, index) => { |
|
|
|
|
if (!contentElement.data.id) { |
|
|
|
|
this.$set(this.course.content[index].data, 'id', remoteUUIDMapper[contentElement.uuid]) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
if (courseData.url) { |
|
|
|
|
this.slugChanged = true; |
|
|
|
|
} |
|
|
|
|
@ -578,11 +654,37 @@ |
|
|
|
|
.catch((err) => { |
|
|
|
|
this.courseSyncHook = false; |
|
|
|
|
this.courseSaving = false; |
|
|
|
|
console.error(err); |
|
|
|
|
document.getElementById('course-redactor__saving-status').innerText = 'ОШИБКА'; |
|
|
|
|
this.savingTimeout = setTimeout(() => { |
|
|
|
|
document.getElementById('course-redactor__saving-status').innerText = ''; |
|
|
|
|
}, 2000); |
|
|
|
|
alert('Произошло что-то страшное: '+err.toString()); |
|
|
|
|
}); |
|
|
|
|
}, 2000); |
|
|
|
|
}, 500); |
|
|
|
|
}, |
|
|
|
|
updateViewSection(location, action) { |
|
|
|
|
console.log('updateViewSection[action]', action); |
|
|
|
|
if (location.pathname === '/course/create/lessons') { |
|
|
|
|
this.viewSection = 'lessons'; |
|
|
|
|
} else if (location.pathname === '/course/create') { |
|
|
|
|
this.viewSection = 'course'; |
|
|
|
|
} else if (location.pathname === '/course/create/lessons/new') { |
|
|
|
|
this.viewSection = 'lessons-edit'; |
|
|
|
|
} else if (location.pathname.indexOf('/course/create/lessons/edit') !== -1) { |
|
|
|
|
let lessonId = parseInt(location.pathname.split('/').pop()); |
|
|
|
|
console.log('lessonId', lessonId, this.lessons.toString()); |
|
|
|
|
console.log('lessod edit', this.lessons.find((i)=>{return i.id === lessonId})); |
|
|
|
|
this.currentLesson = this.lessons.find((i)=>{return i.id === lessonId}); |
|
|
|
|
this.viewSection = 'lessons-edit'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
mounted() { |
|
|
|
|
console.log('mounted'); |
|
|
|
|
// Listen for changes to the current location. |
|
|
|
|
this.unlisten = history.listen(this.updateViewSection); |
|
|
|
|
|
|
|
|
|
api.getCategories(this.accessToken) |
|
|
|
|
.then((response) => { |
|
|
|
|
if (response.data) { |
|
|
|
|
@ -618,10 +720,13 @@ |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (this.courseId) { |
|
|
|
|
this.loadCourse() |
|
|
|
|
this.loadCourse().then(()=>{this.updateViewSection(window.location, 'load')}) |
|
|
|
|
} else { |
|
|
|
|
this.loadCourseDraft(); |
|
|
|
|
this.loadCourseDraft().then(()=>{this.updateViewSection(window.location, 'load draft')}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
console.log('mounted end'); |
|
|
|
|
// this.updateViewSection(window.location); |
|
|
|
|
}, |
|
|
|
|
computed: { |
|
|
|
|
coverBackgroundStyle() { |
|
|
|
|
@ -629,10 +734,10 @@ |
|
|
|
|
}, |
|
|
|
|
displayPrice: { |
|
|
|
|
get: function () { |
|
|
|
|
return this.course.is_paid ? this.course.price : 0; |
|
|
|
|
return this.course.is_paid ? (this.course.price || 0) : 0; |
|
|
|
|
}, |
|
|
|
|
set: function (value) { |
|
|
|
|
this.course.price = value; |
|
|
|
|
this.course.price = value || 0; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
categorySelect: { |
|
|
|
|
@ -677,6 +782,9 @@ |
|
|
|
|
return `https://lil.city/course/${suffix}`; |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
beforeDestroy() { |
|
|
|
|
this.unlisten(); |
|
|
|
|
}, |
|
|
|
|
watch: { |
|
|
|
|
'course': { |
|
|
|
|
handler: function (newValue, oldValue) { |
|
|
|
|
@ -707,6 +815,7 @@ |
|
|
|
|
'block-images': BlockImages, |
|
|
|
|
'block-video': BlockVideo, |
|
|
|
|
'lesson-redactor': LessonRedactor, |
|
|
|
|
'vue-draggable': Draggable, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
</script> |
|
|
|
|
@ -776,4 +885,23 @@ |
|
|
|
|
width: 140px; |
|
|
|
|
height: 140px; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|
|
|
|
|
.kit__section-remove { |
|
|
|
|
button.sortable__handle { |
|
|
|
|
margin-right: 10px; |
|
|
|
|
cursor: -webkit-grab; |
|
|
|
|
cursor: grab; |
|
|
|
|
|
|
|
|
|
svg.icon-hamburger { |
|
|
|
|
width: 1em; |
|
|
|
|
height: 1em; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.sortable-ghost, .sortable-chosen { |
|
|
|
|
background: white; |
|
|
|
|
border-radius: 10px; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|
|
|
|
|
|