LIL-190 - Взаимодействие с API при редактировании курса/уроков

remotes/origin/hasaccess
Vitaly Baev 8 years ago
parent f897296374
commit 8d69cfa7de
  1. 4
      project/templates/lilcity/index.html
  2. 5
      web/package-lock.json
  3. 1
      web/package.json
  4. 578
      web/src/components/CourseRedactor.vue
  5. 122
      web/src/components/LessonRedactor.vue
  6. 121
      web/src/components/blocks/BlockAdd.vue
  7. 16
      web/src/components/blocks/BlockImage.vue
  8. 16
      web/src/components/blocks/BlockImageText.vue
  9. 18
      web/src/components/blocks/BlockImages.vue
  10. 30
      web/src/components/blocks/BlockText.vue
  11. 16
      web/src/components/blocks/BlockVideo.vue
  12. 5
      web/src/components/objects/objects.js
  13. 10
      web/src/components/redactor/VueRedactor.vue
  14. 266
      web/src/js/modules/api.js
  15. 1
      web/src/js/modules/comments.js
  16. 29
      web/src/sass/_common.sass
  17. 1
      web/src/sass/app.sass

@ -32,7 +32,7 @@
<meta property="og:site_name" content="Site Name, i.e. Moz">
<meta property="fb:admins" content="Facebook numeric ID">
<meta name="csrf-token" content="{{ csrf_token }}">
<link rel="stylesheet" media="all" href={% static "css/app.css" %}>
<link rel="stylesheet" media="all" href={% static "app.css" %}>
<link rel="shortcut icon" type="image/png" href="{% static 'img/favicon.ico' %}"/>
<script>
var viewportmeta = document.querySelector('meta[name="viewport"]');
@ -480,7 +480,7 @@
</div>
</div>
</div>
<script type="text/javascript" src={% static "js/app.js" %}></script>
<script type="text/javascript" src={% static "app.js" %}></script>
{% block foot %}{% endblock foot %}
</body>

@ -10127,6 +10127,11 @@
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
},
"slugify": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.2.9.tgz",
"integrity": "sha512-n0cdJ+kN3slJu8SbZXt/EHjljBqF6MxvMGSg/NPpBzoY7yyXoH38wp/ox20a1JaG1KgmdTN5Lf3aS9+xB2Y2aQ=="
},
"smooth-scroll": {
"version": "12.1.5",
"resolved": "https://registry.npmjs.org/smooth-scroll/-/smooth-scroll-12.1.5.tgz",

@ -59,6 +59,7 @@
"jquery": "^3.3.1",
"moment": "^2.20.1",
"owl.carousel": "^2.2.0",
"slugify": "^1.2.9",
"smooth-scroll": "^12.1.5",
"validator": "^9.2.0",
"vue": "^2.5.13",

@ -1,222 +1,225 @@
<template>
<form @submit.prevent="onSubmit">
<div class="info">
<div class="info__section" :style="backgroundStyle">
<div class="info__main">
<div class="info__head">
<div class="info__user">
<div class="info__ava ava">
<img :src="authorPicture" alt="Аватар" class="ava__pic">
</div>
<div class="info__group">
<div class="info__label">АВТОР</div>
<div class="info__value">{{ authorName }}</div>
<div>
<div v-if="!courseLoading">
<form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit">
<div class="info">
<div class="info__section" :style="coverBackgroundStyle">
<div class="info__main">
<div class="info__head">
<div class="info__user">
<div class="info__ava ava">
<img :src="authorPicture" alt="Аватар" class="ava__pic">
</div>
<div class="info__group">
<div class="info__label">АВТОР</div>
<div class="info__value">{{ authorName }}</div>
</div>
</div>
<div class="info__upload upload">
Загрузить фон
<input type="file" class="upload__file" @change="onCoverImageSelected">
</div>
</div>
</div>
<div class="info__upload upload">
Загрузить фон
<input type="file" class="upload__file" @change="onBackgroundImageSelected">
</div>
</div>
<div class="info__title">
<div class="info__field field field_info">
<div class="field__label">НАЗВАНИЕ КУРСА</div>
<div class="field__wrap">
<div class="info__title">
<div class="info__field field field_info">
<div class="field__label">НАЗВАНИЕ КУРСА</div>
<div class="field__wrap">
<textarea class="field__textarea field__textarea_lg" title="Название курса"
v-model="course.title"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="info__foot">
<div class="info__field field field_info">
<div class="field__label field__label_gray">КАТЕГОРИЯ</div>
<div class="field__wrap">
<lil-select :value.sync="categorySelect" :options="categoryOptions" placeholder="Выберите категорию"/>
</div>
</div>
<div class="info__field field field_info">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model.number="course.duration">
</div>
</div>
<div class="info__field field field_info">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="displayPrice" :disabled="!course.is_paid">
</div>
</div>
</div>
</div>
</div>
<div class="info__sidebar">
<div class="info__wrap">
<div class="info__fieldset">
<div class="info__field field">
<div class="field__label field__label_gray">ССЫЛКА</div>
<div class="field__wrap">
<link-input prefix="https://lil.city/course/" :url.sync="course.url"/>
</div>
</div>
<div 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">
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Платный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Бесплатный</span>
</label>
<div class="info__foot">
<div class="info__field field field_info">
<div class="field__label field__label_gray">КАТЕГОРИЯ</div>
<div class="field__wrap">
<lil-select :value.sync="categorySelect" :options="categoryOptions"
placeholder="Выберите категорию"/>
</div>
</div>
<div class="info__field field field_info">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model.number="course.duration">
</div>
</div>
<div class="info__field field field_info">
<div class="field__label field__label_gray">СТОИМОСТЬ</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="displayPrice" :disabled="!course.is_paid">
</div>
</div>
</div>
</div>
<label 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 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">
<input type="radio" :value="false" class="switch__input" v-model="course.is_deferred">
<span class="switch__content">Мгновенный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="true" class="switch__input" v-model="course.is_deferred">
<span class="switch__content">Отложенный</span>
<div class="info__sidebar">
<div class="info__wrap">
<div class="info__fieldset">
<!--<div class="info__field field">
<div class="field__label field__label_gray">ССЫЛКА</div>
<div class="field__wrap">
<input type="text" class="field__input" v-model="course.url">
</div>
<div class="field__wrap field__wrap&#45;&#45;additional">{{ courseFullUrl }}</div>
</div>-->
<div 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">
<input type="radio" :value="true" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Платный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="false" class="switch__input" v-model="course.is_paid">
<span class="switch__content">Бесплатный</span>
</label>
</div>
</div>
<label 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>
<div class="info__field field">
<div class="field__label">ДАТА</div>
<div class="field__wrap">
<vue-datepicker input-class="field__input" v-model="course.date" language="ru" format="dd/MM/yyyy"/>
</div>
</div>
<div class="info__field field">
<div class="field__label">ВРЕМЯ</div>
<div class="field__wrap">
<lil-select :value.sync="course.time" :options="timeOptions" placeholder="Выберите время"/>
<div class="info__fieldset">
<div 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">
<input type="radio" :value="false" class="switch__input" v-model="course.is_deferred">
<span class="switch__content">Мгновенный</span>
</label>
<label class="field__switch switch switch_lg switch_circle">
<input type="radio" :value="true" class="switch__input" v-model="course.is_deferred">
<span class="switch__content">Отложенный</span>
</label>
</div>
</div>
<div class="info__field field" v-show="course.is_deferred">
<div class="field__label">ДАТА</div>
<div class="field__wrap">
<vue-datepicker input-class="field__input" v-model="course.date" language="ru" format="dd/MM/yyyy"/>
</div>
</div>
<div class="info__field field" v-show="course.is_deferred">
<div class="field__label">ВРЕМЯ</div>
<div class="field__wrap">
<lil-select :value.sync="course.time" :options="timeOptions" placeholder="Выберите время"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section__center center">
<div class="kit">
<div class="kit__section">
<div class="kit__field field">
<div class="field__wrap">
<input type="text" class="field__input" placeholder="Кратко о курсе" v-model="course.short_description">
<div class="section">
<div class="section__center center">
<div class="kit">
<div class="kit__section">
<div class="kit__field field">
<div class="field__wrap">
<input type="text" class="field__input" placeholder="Кратко о курсе"
v-model="course.short_description">
</div>
</div>
</div>
</div>
</div>
<div class="kit__nav">
<button class="kit__btn btn btn_lg btn_stroke" type="button" @click="viewSection = 'course'">Описание курса</button>
<button class="kit__btn btn btn_lg btn_gray" type="button" @click="viewSection = 'lessons'" :disabled="!course.id">Уроки</button>
</div>
<div v-if="viewSection === 'course'" class="kit__body">
<div v-for="block in course.blocks">
<block-text v-if="block.type === 'text'"
:title.sync="block.data.title"
:text.sync="block.data.text"
:access-token="accessToken"/>
<block-image-text v-if="block.type === 'image-text'"
<div 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'">Описание
курса
</button>
<button class="kit__btn btn btn_lg"
v-bind:class="{ 'btn_stroke': viewSection === 'lessons', 'btn_gray': viewSection !== 'lessons' }"
type="button"
@click="showLessons"
:disabled="!course.id">
Уроки
</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'"
: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"
:images.sync="block.data.images"
v-on:remove="onBlockRemoved"
:access-token="accessToken"/>
<block-image v-if="block.type === 'image'"
:title.sync="block.data.title"
:image-id.sync="block.data.image_id"
:image-url.sync="block.data.image_url"
:access-token="accessToken"/>
<block-images v-if="block.type === 'images'"
:title.sync="block.data.title"
:text.sync="block.data.text"
:images.sync="block.data.images"
:access-token="accessToken"/>
<block-video v-if="block.type === 'video'"
:title.sync="block.data.title"
:video-url.sync="block.data.video_url"/>
</div>
<div class="kit__section">
<div v-if="!is_adding_block" class="kit__add add">
<button type="button" class="add__toggle" @click="is_adding_block = true">
<span class="add__circle">
<svg class="icon icon-add-plus">
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use>
</svg>
</span>
<span class="add__title">Добавить блок</span>
</button>
</div>
<div v-if="is_adding_block" class="kit__add add open">
<button type="button" class="add__toggle" @click="is_adding_block = false">
<span class="add__circle">
<svg class="icon icon-add-plus">
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use>
</svg>
</span>
<span class="add__title">Добавить блок</span>
</button>
<div class="add__list">
<button class="add__btn" type="button" @click="addBlockText">
<svg class="icon icon-text">
<use xlink:href="/static/img/sprite.svg#icon-text"></use>
</svg>
</button>
<button class="add__btn" type="button" @click="addBlockImage">
<svg class="icon icon-image">
<use xlink:href="/static/img/sprite.svg#icon-image"></use>
</svg>
</button>
<button type="button" class="add__btn" @click="addBlockImageText">
<svg class="icon icon-image-text">
<use xlink:href="/static/img/sprite.svg#icon-image-text"></use>
</svg>
</button>
<button type="button" class="add__btn" @click="addBlockImages">
<svg class="icon icon-images">
<use xlink:href="/static/img/sprite.svg#icon-images"></use>
</svg>
</button>
<button type="button" class="add__btn" @click="addBlockVideo">
<svg class="icon icon-video-stroke">
<use xlink:href="/static/img/sprite.svg#icon-video-stroke"></use>
</svg>
<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-add v-on:added="onBlockAdded"/>
<div class="kit__foot">
<button type="submit" class="kit__submit btn btn_md" v-bind:class="{ loading: courseSaving }">
Сохранить
</button>
</div>
</div>
</div>
<div class="kit__section">
<div class="kit__theme">О чем курс?</div>
<vue-redactor :value.sync="course.about" />
</div>
<div class="kit__foot">
<button type="submit" class="kit__submit btn btn_md">Сохранить</button>
</div>
</div>
<div v-if="viewSection === 'lessons'" class="kit__body">
<div class="lessons__title title">Содержание курса</div>
<div class="lessons__list">
</div>
<div class="lessons__foot">
<button class="lessons__btn btn btn_md">СОЗДАТЬ УРОК</button>
<div v-if="viewSection === 'lessons'" class="kit__body">
<div class="lessons__title title">Содержание курса</div>
<div v-if="!lessonsLoading" class="lessons__list">
<div class="lessons__item" v-for="(lesson, index) in lessons">
<div class="lessons__actions">
<button type="button" class="lessons__action" @click="removeLesson(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
<button type="button" class="lessons__action" @click="editLesson(index)">
<svg class="icon icon-edit">
<use xlink:href="/static/img/sprite.svg#icon-edit"></use>
</svg>
</button>
</div>
<div class="lessons__subtitle subtitle">{{ lesson.title }}</div>
<div class="lessons__row">
<div class="lessons__content">{{ lesson.short_description }}</div>
</div>
</div>
</div>
<div v-if="lessonsLoading">Загрузка...</div>
<div class="lessons__foot">
<button type="button" class="lessons__btn btn btn_md" @click="addLesson">СОЗДАТЬ УРОК</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<form v-if="viewSection === 'lessons-edit'" @submit.prevent="onLessonSubmit">
<lesson-redactor :lesson.sync="currentLesson" :saving="lessonSaving" :access-token="accessToken"
v-on:back="goToLessons"/>
</form>
</div>
<div v-else>
<div>Loading...</div>
</div>
</form>
</div>
</template>
<script>
@ -227,13 +230,14 @@
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import VueRedactor from './redactor/VueRedactor'
import LilSelect from "./inputs/LilSelect";
import LessonRedactor from "./LessonRedactor";
import {api} from "../js/modules/api";
import BlockAdd from "./blocks/BlockAdd";
export default {
name: "course-redactor",
props: ["authorName", "authorPicture", "accessToken"],
props: ["authorName", "authorPicture", "accessToken", "courseId"],
data() {
return {
viewSection: 'course',
@ -243,20 +247,26 @@
categorySelect: null,
duration: 1,
price: 1000,
url: 'my-awesome-url',
backgroundImage: '',
url: '',
coverImage: '',
coverImageId: null,
is_paid: true,
is_featured: true,
is_deferred: false,
date: '2018-03-08',
time: null,
short_description: 'Этот курс поможет детям освоить базовые навыки рисования простых персонажей',
blocks: [],
about: '<b>Главная цель курса</b> - рассказать...',
},
lessons: {
content: [],
gallery: {
images: [],
}
},
courseLoading: false,
courseSaving: false,
lessons: [],
lessonsLoading: false,
lessonSaving: false,
currentLesson: null,
is_adding_block: false,
timeOptions: [
{
@ -300,69 +310,27 @@
}
},
methods: {
onBackgroundImageSelected(event) {
onCoverImageSelected(event) {
let file = event.target.files[0];
let reader = new FileReader();
reader.onload = () => {
this.course.backgroundImage = reader.result;
this.$set(this.course, 'coverImage', reader.result);
api.uploadImage(reader.result, this.accessToken)
.then((response) => {
this.course.coverImageId = response.data.id;
})
.catch((error) => {
console.log('error', error);
});
};
if (file) {
reader.readAsDataURL(file);
}
},
onCoursePriceChange(event) {
console.log('onCoursePriceChange');
this.course.price = event.target.value;
},
addBlockText() {
this.course.blocks.push({
type: 'text',
data: {
title: 'тест заголовок',
text: 'текст',
}
})
},
addBlockImage() {
this.course.blocks.push({
type: 'image',
data: {
title: 'тест картинка',
image_id: null,
image_url: null,
}
})
},
addBlockImageText() {
this.course.blocks.push({
type: 'image-text',
data: {
title: 'тест картинка-текст',
text: 'текст какой-то',
image_id: null,
image_url: null,
}
})
},
addBlockImages() {
this.course.blocks.push({
type: 'images',
data: {
title: 'тест заголовок картинок',
text: 'описание блока галереи',
images: [],
}
})
},
addBlockVideo() {
this.course.blocks.push({
type: 'video',
data: {
title: 'тест видео',
video_url: 'http://vimeo.com/safmklsamfk',
}
})
},
updateCategory() {
if (this.categoryOptions && Array.isArray(this.categoryOptions) && this.course.category) {
this.categoryOptions.forEach((category) => {
@ -372,10 +340,92 @@
});
}
},
onBlockRemoved(blockIndex) {
const blockToRemove = this.course.content[blockIndex];
// Удаляем блок из Vue
this.course.content.splice(blockIndex, 1);
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken);
}
},
onBlockAdded(blockData) {
this.course.content.push(blockData);
},
removeLesson(lessonIndex) {
if (!confirm('Вы действительно хотите удалить этот урок?')) {
return
}
const lesson = this.lessons[lessonIndex];
if (lesson.hasOwnProperty('id') && lesson.id) {
api.removeCourseLesson(lesson.id, this.accessToken);
}
this.lessons.splice(lessonIndex, 1);
},
editLesson(lessonIndex) {
this.currentLesson = this.lessons[lessonIndex];
this.viewSection = 'lessons-edit';
},
showLessons() {
this.viewSection = 'lessons';
},
addLesson() {
this.currentLesson = {
title: '',
short_description: '',
course_id: this.course.id,
content: [],
};
this.viewSection = 'lessons-edit';
},
onSubmit() {
this.courseSaving = true;
api.saveCourse(this.course, this.accessToken)
.then((response) => {
this.course = api.convertCourseJson(response.course);
this.courseSaving = false;
this.course = api.convertCourseJson(response.data);
})
.catch((err) => {
this.courseSaving = false;
});
},
onLessonSubmit() {
this.lessonSaving = true;
const currentLessonId = this.currentLesson.id;
api.saveLesson(this.currentLesson, this.accessToken)
.then((response) => {
this.lessonSaving = false;
const newLesson = api.convertLessonJson(response.data);
newLesson.course_id = this.course.id;
this.currentLesson = newLesson;
if (!currentLessonId) {
this.lessons.push(newLesson);
}
if (this.lessons && Array.isArray(this.lessons)) {
this.lessons.forEach((lesson, index) => {
if (newLesson.id === lesson.id) {
this.$set('lessons', index, newLesson);
}
});
}
})
.catch((err) => {
this.lessonSaving = false;
});
},
goToLessons() {
this.viewSection = 'lessons';
},
loadCourse(courseId) {
this.courseLoading = true;
api.loadCourse(courseId, this.accessToken)
.then((response) => {
this.courseLoading = false;
this.course = api.convertCourseJson(response.data);
})
.catch((err) => {
this.courseLoading = false;
console.log('error course loading', err);
});
}
},
@ -392,10 +442,14 @@
}
this.updateCategory();
});
if (this.courseId) {
this.loadCourse(this.courseId);
}
},
computed: {
backgroundStyle() {
return this.course.backgroundImage ? `background-image: url(${this.course.backgroundImage});` : '';
coverBackgroundStyle() {
return this.course.coverImage ? `background-image: url(${this.course.coverImage});` : '';
},
displayPrice: {
get: function () {
@ -421,9 +475,15 @@
set(value) {
this.course.category = value.value;
}
},
courseFullUrl() {
let suffix = this.course.url ? this.course.url : 'ваша_ссылка';
return `https://lil.city/course/${suffix}`;
}
},
components: {
BlockAdd,
LessonRedactor,
LilSelect,
BlockText,
'link-input': LinkInput,
@ -433,7 +493,7 @@
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
'vue-redactor': VueRedactor,
'lesson-redactor': LessonRedactor,
}
}
</script>

@ -0,0 +1,122 @@
<template>
<div class="section">
<div class="section__center center">
<div class="kit">
<div class="kit__go go">
<a href="#" class="go__item" @click.prevent="goBack">
<div class="go__arrow">
<svg class="icon icon-arrow-left">
<use xlink:href="/static/img/sprite.svg#icon-arrow-left"></use>
</svg>
</div>
<div class="go__title">К списку уроков</div>
</a>
</div>
<div class="kit__title title">{{ title }}</div>
<div class="kit__section">
<div class="kit__field field">
<div class="field__wrap">
<input type="text" class="field__input" placeholder="Название урока" v-model="lesson.title">
</div>
</div>
<div class="kit__field field">
<div class="field__wrap">
<textarea class="field__input" placeholder="Описание урока" v-model="lesson.short_description"></textarea>
</div>
</div>
</div>
<div v-for="(block, index) in lesson.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'"
: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-add v-on:added="onBlockAdded" />
<div class="kit__foot">
<button class="kit__submit btn btn_md" v-bind:class="{ loading: saving }">Сохранить</button>
</div>
</div>
</div>
</div>
</template>
<script>
import BlockAdd from "./blocks/BlockAdd";
import BlockText from './blocks/BlockText'
import BlockImage from './blocks/BlockImage'
import BlockImages from './blocks/BlockImages'
import BlockImageText from './blocks/BlockImageText'
import BlockVideo from './blocks/BlockVideo'
import {api} from "../js/modules/api";
export default {
name: "lesson-redactor",
props: ["lesson", "saving", "accessToken"],
methods: {
goBack() {
this.$emit('back');
},
onBlockAdded(blockData) {
this.lesson.content.push(blockData);
this.$emit('update:lesson', this.lesson);
},
onBlockRemoved(blockIndex) {
const blockToRemove = this.lesson.content[blockIndex];
// Удаляем блок из Vue
this.lesson.content.splice(blockIndex, 1);
this.$emit('update:lesson', this.lesson);
// Если блок уже был записан в БД, отправляем запрос на сервер на удаление блока из БД
if (blockToRemove.data.id) {
api.removeContentBlock(blockToRemove, this.accessToken);
}
}
},
computed: {
title() {
return this.lesson && this.lesson.id ? 'Редактирование урока' : 'Создать урок';
}
},
components: {
BlockAdd,
'block-text': BlockText,
'block-image': BlockImage,
'block-image-text': BlockImageText,
'block-images': BlockImages,
'block-video': BlockVideo,
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,121 @@
<template>
<div class="kit__section">
<div v-if="!isOpen" class="kit__add add">
<button type="button" class="add__toggle" @click="isOpen = true">
<span class="add__circle">
<svg class="icon icon-add-plus">
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use>
</svg>
</span>
<span class="add__title">Добавить блок</span>
</button>
</div>
<div v-if="isOpen" class="kit__add add open">
<button type="button" class="add__toggle" @click="isOpen = false">
<span class="add__circle">
<svg class="icon icon-add-plus">
<use xlink:href="/static/img/sprite.svg#icon-add-plus"></use>
</svg>
</span>
<span class="add__title">Добавить блок</span>
</button>
<div class="add__list">
<button class="add__btn" type="button" @click="addBlockText">
<svg class="icon icon-text">
<use xlink:href="/static/img/sprite.svg#icon-text"></use>
</svg>
</button>
<button class="add__btn" type="button" @click="addBlockImage">
<svg class="icon icon-image">
<use xlink:href="/static/img/sprite.svg#icon-image"></use>
</svg>
</button>
<button type="button" class="add__btn" @click="addBlockImageText">
<svg class="icon icon-image-text">
<use xlink:href="/static/img/sprite.svg#icon-image-text"></use>
</svg>
</button>
<button type="button" class="add__btn" @click="addBlockImages">
<svg class="icon icon-images">
<use xlink:href="/static/img/sprite.svg#icon-images"></use>
</svg>
</button>
<button type="button" class="add__btn" @click="addBlockVideo">
<svg class="icon icon-video-stroke">
<use xlink:href="/static/img/sprite.svg#icon-video-stroke"></use>
</svg>
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: "block-add",
data() {
return {
isOpen: false
}
},
methods: {
add(blockData) {
this.isOpen = false;
this.$emit('added', blockData)
},
addBlockText() {
this.add({
type: 'text',
data: {
title: 'тест заголовок',
text: 'текст',
}
})
},
addBlockImage() {
this.add({
type: 'image',
data: {
title: 'тест картинка',
image_id: null,
image_url: null,
}
})
},
addBlockImageText() {
this.add({
type: 'image-text',
data: {
title: 'тест картинка-текст',
text: 'текст какой-то',
image_id: null,
image_url: null,
}
})
},
addBlockImages() {
this.add({
type: 'images',
data: {
title: 'тест заголовок картинок',
text: 'описание блока галереи',
images: [],
}
})
},
addBlockVideo() {
this.add({
type: 'video',
data: {
title: 'тест видео',
video_url: 'http://vimeo.com/safmklsamfk',
}
})
},
}
}
</script>
<style scoped>
</style>

@ -1,7 +1,14 @@
<template>
<div class="kit__section">
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div class="kit__field field">
<div class="field__wrap">
<div class="field__wrap field__wrap--title">
<input type="text"
:value="title"
class="field__input"
@ -21,7 +28,7 @@
export default {
name: "block-image",
props: ["title", "imageUrl", "imageId", "accessToken"],
props: ["index", "title", "imageUrl", "imageId", "accessToken"],
methods: {
onTitleChange(event) {
this.$emit('update:title', event.target.value);
@ -31,6 +38,9 @@
},
onUpdateImageId(newValue) {
this.$emit('update:imageId', newValue);
},
onRemove() {
this.$emit('remove', this.index);
}
},
components: {

@ -1,11 +1,18 @@
<template>
<div class="kit__section">
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div class="kit__row">
<lil-image :image-id="imageId" :image-url="imageUrl" v-on:update:imageUrl="onUpdateImageUrl"
v-on:update:imageId="onUpdateImageId" :access-token="accessToken"/>
<div class="kit__fieldset">
<div class="kit__field field">
<div class="field__wrap">
<div class="field__wrap field__wrap--title">
<input type="text"
:value="title"
class="field__input"
@ -31,7 +38,7 @@
export default {
name: "block-image-text",
props: ["title", "text", "imageUrl", "imageId", "accessToken"],
props: ["index", "title", "text", "imageUrl", "imageId", "accessToken"],
methods: {
onTitleChange(event) {
this.$emit('update:title', event.target.value);
@ -44,6 +51,9 @@
},
onUpdateImageId(newValue) {
this.$emit('update:imageId', newValue);
},
onRemove() {
this.$emit('remove', this.index);
}
},
components: {

@ -1,7 +1,14 @@
<template>
<div class="kit__section">
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div class="kit__field field">
<div class="field__wrap">
<div class="field__wrap field__wrap--title">
<input type="text"
:value="title"
class="field__input"
@ -36,7 +43,7 @@
export default {
name: "block-images",
props: ["title", "text", "images", "accessToken"],
props: ["index", "title", "text", "images", "accessToken"],
methods: {
onTitleChange(event) {
this.$emit('update:title', event.target.value);
@ -57,8 +64,6 @@
api.uploadImage(reader.result, this.accessToken)
.then((response) => {
console.log('completed', response);
let images = this.images;
images.forEach((image, index) => {
if (image.src === reader.result) {
@ -77,6 +82,9 @@
reader.readAsDataURL(file);
}
});
},
onRemove() {
this.$emit('remove', this.index);
}
}
}

@ -1,7 +1,14 @@
<template>
<div class="kit__section">
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div class="kit__field field">
<div class="field__wrap">
<div class="field__wrap field__wrap--title">
<input type="text"
:value="title"
class="field__input"
@ -11,26 +18,31 @@
</div>
<div class="kit__field field">
<div class="field__wrap">
<textarea class="field__textarea field__textarea_sm"
:value="text"
placeholder="Описание"
@change="onTextChange"></textarea>
<vue-redactor :value="text" v-on:update:value="onTextChange" placeholder="Описание"/>
</div>
</div>
</div>
</template>
<script>
import VueRedactor from '../redactor/VueRedactor';
export default {
name: "block-text",
props: ["title", "text"],
props: ["index", "title", "text"],
methods: {
onTitleChange(event) {
this.$emit('update:title', event.target.value);
},
onTextChange(event) {
this.$emit('update:text', event.target.value);
onTextChange(newValue) {
this.$emit('update:text', newValue);
},
onRemove() {
this.$emit('remove', this.index);
}
},
components: {
'vue-redactor': VueRedactor,
}
}
</script>

@ -1,7 +1,14 @@
<template>
<div class="kit__section">
<div class="kit__section kit__section--block">
<div class="kit__section-remove">
<button type="button" @click="onRemove">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<div class="kit__field field">
<div class="field__wrap">
<div class="field__wrap field__wrap--title">
<input type="text"
:value="title"
class="field__input"
@ -26,13 +33,16 @@
<script>
export default {
name: "block-video",
props: ["title", "videoUrl"],
props: ["index", "title", "videoUrl"],
methods: {
onTitleChange(event) {
this.$emit('update:title', event.target.value);
},
onVideoUrlChange(event) {
this.$emit('update:videoUrl', event.target.value);
},
onRemove() {
this.$emit('remove', this.index);
}
}
}

@ -0,0 +1,5 @@
export var lessonObject = {
title: '',
short_description: '',
course_id: 0
};

@ -7,7 +7,7 @@
import './redactor-full.js';
export default {
props: ['value'],
props: ['value', 'placeholder'],
name: "vue-redactor",
data() {
return {
@ -18,9 +18,9 @@
const me = this;
$(me.$refs.input).redactor({
air: true,
formatting: ['p', 'blockquote', 'h2', 'h3', 'h4'],
buttonsHide: ['image', 'link', 'format'],
lang: 'ru',
placeholder: 'Напишите, о чем ваш курс',
placeholder: this.placeholder,
callbacks: {
visual: function () {
me.$emit('update:value', this.code.get());
@ -30,6 +30,10 @@
}
},
});
},
beforeDestroy() {
$(this.$refs.input).redactor('core.destroy');
$(this.$refs.input).remove();
}
}
</script>

@ -21,6 +21,13 @@ export const api = {
}
});
},
loadCourse: (courseId, accessToken) => {
return axios.get(`/api/v1/courses/${courseId}/`, {
headers: {
'Authorization': `Token ${accessToken}`,
}
});
},
saveCourse: (courseObject, accessToken) => {
const isAdding = (!courseObject.hasOwnProperty('id') || !courseObject.hasOwnProperty('id'));
@ -40,13 +47,17 @@ export const api = {
duration: courseObject.duration,
is_featured: courseObject.is_featured,
url: courseObject.url,
cover: courseObject.cover_id ? courseObject.cover_id : null,
content: courseObject.blocks.map((block) => {
cover: courseObject.coverImageId ? courseObject.coverImageId : null,
gallery: {
gallery_images: courseObject.gallery && courseObject.gallery.images ? courseObject.gallery.images : []
},
content: courseObject.content.map((block, index) => {
if (block.type === 'text') {
return {
'type': 'text',
'data': {
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'txt': block.data.text,
}
@ -56,6 +67,7 @@ export const api = {
'type': 'image',
'data': {
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
}
@ -65,6 +77,7 @@ export const api = {
'type': 'image-text',
'data': {
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'txt': block.data.text,
@ -75,6 +88,7 @@ export const api = {
'type': 'images',
'data': {
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
@ -89,6 +103,7 @@ export const api = {
'type': 'video',
'data': {
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'url': block.data.video_url,
}
@ -97,99 +112,189 @@ export const api = {
}),
};
console.log(courseJson);
let request;
if (isAdding) {
return api.addCourse(courseJson, accessToken);
} else {
return api.updateCourse(courseObject.id, courseJson, accessToken);
}
},
convertCourseJson: (courseJSON) => {
let isDeferred = false;
let deferredDate = '';
let deferredTime = '';
if (courseJSON.deferred_start_at) {
let deferredDateTime = moment(courseJSON.deferred_start_at);
isDeferred = true;
deferredDate = deferredDateTime.format('MM-DD-YYYY');
deferredTime = deferredDateTime.format('HH:mm');
}
return {
id: courseJSON.id,
title: courseJSON.title,
short_description: courseJSON.short_description,
category: courseJSON.category,
price: courseJSON.price,
is_paid: courseJSON.price === 0,
is_deferred: isDeferred,
date: deferredDate,
time: deferredTime ? {title: deferredTime, value: deferredTime} : null,
duration: courseJSON.duration,
is_featured: courseJSON.is_featured,
url: courseJSON.url,
cover_id: courseJSON.cover.id,
cover_url: courseJSON.cover.image,
content: courseJSON.content.map((contentItem) => {
if (contentItem.type === 'text') {
saveLesson: (lessonObject, accessToken) => {
const isAdding = (!lessonObject.hasOwnProperty('id') || !lessonObject.hasOwnProperty('id'));
const lessonJson = {
title: lessonObject.title,
short_description: lessonObject.short_description,
course: lessonObject.course_id,
content: lessonObject.content.map((block, index) => {
if (block.type === 'text') {
return {
'type': 'text',
'data': {
'id': contentItem.data.id ? contentItem.data.id : null,
'title': contentItem.data.title,
'text': contentItem.data.txt,
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'txt': block.data.text,
}
}
} else if (contentItem.type === 'image') {
} else if (block.type === 'image') {
return {
'type': 'image',
'data': {
'id': contentItem.data.id ? contentItem.data.id : null,
'title': contentItem.data.title,
'image_id': contentItem.data.image.id,
'image_url': contentItem.data.image.image,
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
}
}
} else if (contentItem.type === 'image-text') {
} else if (block.type === 'image-text') {
return {
'type': 'image-text',
'data': {
'id': contentItem.data.id ? contentItem.data.id : null,
'title': contentItem.data.title,
'image_id': contentItem.data.image.id,
'image_url': contentItem.data.image.image,
'text': contentItem.data.txt,
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'img': block.data.image_id,
'txt': block.data.text,
}
}
} else if (contentItem.type === 'images') {
} else if (block.type === 'images') {
return {
'type': 'images',
'data': {
'id': contentItem.data.id ? contentItem.data.id : null,
'title': contentItem.data.title,
'images': contentItem.data.images.map((galleryImage) => {
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'images': block.data.images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'image_id': galleryImage.image.id,
'image_url': galleryImage.image.url,
'img': galleryImage.img,
}
}),
}
}
} else if (contentItem.type === 'video') {
} else if (block.type === 'video') {
return {
'type': 'video',
'data': {
'id': contentItem.data.id ? contentItem.data.id : null,
'title': contentItem.data.title,
'video_url': contentItem.data.url,
'id': block.data.id ? block.data.id : null,
'position': ++index,
'title': block.data.title,
'url': block.data.video_url,
}
}
}
}),
};
if (isAdding) {
return api.addLesson(lessonJson, accessToken);
} else {
return api.updateLesson(lessonObject.id, lessonJson, accessToken);
}
},
convertLessonJson: (lessonJSON) => {
return {
id: lessonJSON.id,
title: lessonJSON.title,
short_description: lessonJSON.short_description,
content: api.convertContentResponse(lessonJSON.content)
}
},
convertCourseJson: (courseJSON) => {
let isDeferred = false;
let deferredDate = '';
let deferredTime = '';
if (courseJSON.deferred_start_at) {
let deferredDateTime = moment(courseJSON.deferred_start_at);
isDeferred = true;
deferredDate = deferredDateTime.format('MM-DD-YYYY');
deferredTime = deferredDateTime.format('HH:mm');
}
return {
id: courseJSON.id,
title: courseJSON.title,
short_description: courseJSON.short_description,
category: courseJSON.category.id ? courseJSON.category.id : courseJSON.category,
price: parseFloat(courseJSON.price),
is_paid: parseFloat(courseJSON.price) === 0,
is_deferred: isDeferred,
date: deferredDate,
time: deferredTime ? {title: deferredTime, value: deferredTime} : null,
duration: courseJSON.duration,
is_featured: courseJSON.is_featured,
url: courseJSON.url,
coverImageId: courseJSON.cover && courseJSON.cover.id ? courseJSON.cover.id : null,
coverImage: courseJSON.cover && courseJSON.cover.image ? courseJSON.cover.image : null,
content: api.convertContentResponse(courseJSON.content),
}
},
convertContentResponse: (contentJson) => {
return contentJson.sort((a, b) => {
if (a.position < b.position) {
return -1;
}
if (a.position > b.position) {
return 1;
}
return 0;
}).map((contentItem) => {
if (contentItem.type === 'text') {
return {
'type': 'text',
'data': {
'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title,
'text': contentItem.txt,
}
}
} else if (contentItem.type === 'image') {
return {
'type': 'image',
'data': {
'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title,
'image_id': contentItem.img.id,
'image_url': contentItem.img.image,
}
}
} else if (contentItem.type === 'image-text') {
return {
'type': 'image-text',
'data': {
'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title,
'image_id': contentItem.img.id,
'image_url': contentItem.img.image,
'text': contentItem.txt,
}
}
} else if (contentItem.type === 'images') {
return {
'type': 'images',
'data': {
'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title,
'images': contentItem.gallery_images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.img.id,
'src': galleryImage.img.image,
}
}),
}
}
} else if (contentItem.type === 'video') {
return {
'type': 'video',
'data': {
'id': contentItem.id ? contentItem.id : null,
'title': contentItem.title,
'video_url': contentItem.url,
}
}
}
});
},
addCourse: (courseJson, accessToken) => {
return axios.post('/api/v1/courses/', courseJson, {
headers: {
@ -211,4 +316,53 @@ export const api = {
}
});
},
removeCourseLesson: (lessonId, accessToken) => {
return axios.delete(`/api/v1/lessons/${lessonId}/`, {
headers: {
'Authorization': `Token ${accessToken}`,
}
});
},
addLesson: (lessonJson, accessToken) => {
return axios.post('/api/v1/lessons/', lessonJson, {
headers: {
'Authorization': `Token ${accessToken}`,
}
});
},
updateLesson: (lessonId, lessonJson, accessToken) => {
return axios.put(`/api/v1/lessons/${lessonId}/`, lessonJson, {
headers: {
'Authorization': `Token ${accessToken}`,
}
});
},
removeContentBlock: (blockData, accessToken) => {
let removeUrl;
switch(blockData.type) {
case 'text':
removeUrl = `/api/v1/texts/${blockData.data.id}/`;
break;
case 'image':
removeUrl = `/api/v1/images/${blockData.data.id}/`;
break;
case 'image-text':
removeUrl = `/api/v1/image-texts/${blockData.data.id}/`;
break;
case 'images':
removeUrl = `/api/v1/galleries/${blockData.data.id}/`;
break;
case 'video':
removeUrl = `/api/v1/videos/${blockData.data.id}/`;
break;
}
if (!removeUrl) {
return;
}
return axios.delete(removeUrl, {
headers: {
'Authorization': `Token ${accessToken}`,
}
});
},
};

@ -20,7 +20,6 @@ $(document).ready(function () {
}
})
.done(function (data) {
console.log(data);
if (data.success === true) {
if (replyToValue > 0) {
$(`#question__${replyToValue}`).after(data.comment);

@ -1,14 +1,14 @@
=d
@media only screen and (max-width: "1120px")
@media only screen and (max-width: 1120px)
@content
=t
@media only screen and (max-width: "1023px")
@media only screen and (max-width: 1023px)
@content
=m
@media only screen and (max-width: "599px")
@media only screen and (max-width: 599px)
@content
=s
@media only screen and (max-width: "474px")
@media only screen and (max-width: 474px)
@content
+font(ProximaNova-Regular, ProximaNova-Regular)
@ -227,7 +227,7 @@ b
animation: loading .6s infinite linear
@keyframes loading
0
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
@ -1945,6 +1945,10 @@ a.grey-link
background: none
margin-top: 11px
.field__wrap
&--title
margin-right: 25px
&--additional
margin-top: 10px
svg.icon-password-eye
display: block
svg.icon-password-hidden-eye
@ -3054,6 +3058,21 @@ a.grey-link
max-width: 620px
&__section
margin-bottom: 60px
&--block
box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.06)
border-radius: 10px
padding: 15px
position: relative
&-remove
position: absolute
right: 15px
top: 20px
.icon-delete
width: 1.3em
height: 1.3em
transition: fill 0.3s ease
&:hover
fill: #000
&__nav
display: flex
margin: 0 -10px 60px

@ -1,5 +1,4 @@
// done by arturmoroz.com
@import helpers/all
@import generated/sprite-svg
// @import lib/owl.carousel
@import common

Loading…
Cancel
Save