Merge branch 'hotfix/LIL-706' into 'master'

Hotfix/lil 706

See merge request lilcity/backend!220
remotes/origin/editis_13-01-19
cfwme 7 years ago
commit 7ed17d4e5a
  1. 14
      api/v1/serializers/mixins.py
  2. 31
      api/v1/serializers/user.py
  3. 3
      api/v1/urls.py
  4. 11
      api/v1/views.py
  5. 4
      apps/school/templates/blocks/schedule.html
  6. 2
      apps/school/templates/school/livelessons_list.html
  7. 41
      apps/school/templates/summer/schedule_purchased.html
  8. 48
      apps/school/views.py
  9. 2
      apps/user/forms.py
  10. 18
      apps/user/migrations/0026_user_site.py
  11. 20
      apps/user/migrations/0027_user_gallery.py
  12. 5
      apps/user/models.py
  13. 35
      apps/user/templates/user/edit-gallery.html
  14. 11
      apps/user/templates/user/profile-settings.html
  15. 25
      apps/user/templates/user/profile.html
  16. 5
      apps/user/views.py
  17. 1
      project/templates/blocks/lil_store_js.html
  18. 2
      project/templates/lilcity/index.html
  19. 3
      project/urls.py
  20. 164
      web/src/components/CourseRedactor.vue
  21. 40
      web/src/components/blocks/BlockContent.vue
  22. 19
      web/src/components/blocks/BlockImage.vue
  23. 17
      web/src/components/blocks/BlockImageText.vue
  24. 40
      web/src/components/blocks/BlockImages.vue
  25. 17
      web/src/components/blocks/BlockText.vue
  26. 19
      web/src/components/blocks/BlockVideo.vue
  27. 4
      web/src/js/app.js
  28. 26
      web/src/js/modules/api.js
  29. 14
      web/src/js/user-gallery-edit.js
  30. 48
      web/src/sass/_common.sass
  31. 5
      web/src/sass/generated/_sprite-svg.scss
  32. 1
      web/webpack.config.js

@ -131,7 +131,7 @@ class DispatchMaterialMixin(object):
class DispatchGalleryMixin(object): class DispatchGalleryMixin(object):
def dispatch_gallery(self, course, gallery): def dispatch_gallery(self, obj, gallery):
if gallery: if gallery:
if 'id' in gallery and gallery['id']: if 'id' in gallery and gallery['id']:
g = Gallery.objects.get(id=gallery['id']) g = Gallery.objects.get(id=gallery['id'])
@ -145,15 +145,19 @@ class DispatchGalleryMixin(object):
) )
if 'images' in gallery: if 'images' in gallery:
for image in gallery['images']: for image in gallery['images']:
if isinstance(image['img'], ImageObject):
img = image['img']
else:
img = ImageObject.objects.get(id=image['img'])
if 'id' in image and image['id']: if 'id' in image and image['id']:
gi = GalleryImage.objects.get(id=image['id']) gi = GalleryImage.objects.get(id=image['id'])
gi.gallery = g gi.gallery = g
gi.img = image['img'] gi.img = img
gi.save() gi.save()
else: else:
gi = GalleryImage.objects.create( gi = GalleryImage.objects.create(
gallery=g, gallery=g,
img=image['img'], img=img,
) )
course.gallery = g obj.gallery = g
course.save() obj.save()

@ -3,8 +3,10 @@ from rest_framework import serializers
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from api.v1.serializers.content import GallerySerializer, GalleryImageSerializer, GalleryImageCreateSerializer
from . import Base64ImageField from . import Base64ImageField
from apps.user.models import AuthorRequest from apps.user.models import AuthorRequest
from .mixins import DispatchGalleryMixin
User = get_user_model() User = get_user_model()
@ -104,3 +106,32 @@ class AuthorRequestSerializer(serializers.ModelSerializer):
'created_at', 'created_at',
'update_at', 'update_at',
) )
class UserGallerySerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'gallery',
)
class UserGalleryUpdateSerializer(DispatchGalleryMixin, serializers.ModelSerializer):
images = serializers.ListField()
class Meta:
model = User
fields = (
'images',
)
def update(self, instance, validated_data):
images = validated_data.pop('images', {})
self.dispatch_gallery(instance, {
'id': instance.gallery_id,
'images': images,
})
return instance
def to_representation(self, instance):
return UserGallerySerializer(instance=instance, context=self.context).to_representation(instance)

@ -19,7 +19,7 @@ from .views import (
SchoolScheduleViewSet, LiveLessonViewSet, SchoolScheduleViewSet, LiveLessonViewSet,
PaymentViewSet, ObjectCommentsViewSet, PaymentViewSet, ObjectCommentsViewSet,
ContestViewSet, ContestWorkViewSet, ContestViewSet, ContestWorkViewSet,
AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet) AuthorBalanceUsersViewSet, CaptureEmail, FAQViewSet, UserGalleryViewSet)
router = DefaultRouter() router = DefaultRouter()
router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests') router.register(r'author-requests', AuthorRequestViewSet, base_name='author-requests')
@ -45,6 +45,7 @@ router.register(r'faq', FAQViewSet, base_name='faq')
router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules') router.register(r'school-schedules', SchoolScheduleViewSet, base_name='school-schedules')
router.register(r'users', UserViewSet, base_name='users') router.register(r'users', UserViewSet, base_name='users')
router.register(r'user-gallery', UserGalleryViewSet, base_name='user-gallery')
router.register(r'contests', ContestViewSet, base_name='contests') router.register(r'contests', ContestViewSet, base_name='contests')
router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works') router.register(r'contest-works', ContestWorkViewSet, base_name='contest_works')

@ -4,7 +4,7 @@ from decimal import Decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models import Q from django.db.models import Q
from rest_framework import status, views, viewsets, generics from rest_framework import status, views, viewsets, generics, mixins
from rest_framework.decorators import detail_route, list_route, action, permission_classes, authentication_classes from rest_framework.decorators import detail_route, list_route, action, permission_classes, authentication_classes
from rest_framework.response import Response from rest_framework.response import Response
@ -42,7 +42,7 @@ from .serializers.payment import (
from .serializers.user import ( from .serializers.user import (
AuthorRequestSerializer, AuthorRequestSerializer,
UserSerializer, UserPhotoSerializer, UserSerializer, UserPhotoSerializer,
) UserGallerySerializer, UserGalleryUpdateSerializer)
from .serializers.contest import ( from .serializers.contest import (
ContestCreateSerializer, ContestSerializer, ContestWorkSerializer, ContestWorkCreateSerializer ContestCreateSerializer, ContestSerializer, ContestWorkSerializer, ContestWorkCreateSerializer
) )
@ -475,6 +475,13 @@ class UserViewSet(ExtendedModelViewSet):
return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST) return Response({'success': False}, status=status.HTTP_400_BAD_REQUEST)
class UserGalleryViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = User.objects.all()
serializer_class = UserGalleryUpdateSerializer
# FIXME
authentication_classes = []
class SchoolScheduleViewSet(ExtendedModelViewSet): class SchoolScheduleViewSet(ExtendedModelViewSet):
queryset = SchoolSchedule.objects.all() queryset = SchoolSchedule.objects.all()
serializer_class = SchoolScheduleSerializer serializer_class = SchoolScheduleSerializer

@ -10,7 +10,9 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="timing__foot"> <div class="timing__foot">
{# include './pay_btn.html' #} {% if not is_purchased and not is_purchased_future %}
<a data-popup=".js-popup-buy" class="btn" href="#">Получить доступ на месяц</a>
{% endif %}
<a target="_blank" class="timing__print" href="{% url 'school:school_schedules-print' %}">Распечатать расписание <a target="_blank" class="timing__print" href="{% url 'school:school_schedules-print' %}">Распечатать расписание
<span class="bold">чтобы не забыть</span> <span class="bold">чтобы не забыть</span>
<svg class="icon icon-print"> <svg class="icon icon-print">

@ -1,4 +1,4 @@
{% extends "templates/lilcity/index.html" %} {% load jsonify from jsonify_queryset %} {% load static %} {% block content %} {% extends "templates/lilcity/index.html" %}{% load static %} {% block content %}
<div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});"> <div class="main" style="background-image: url({% static 'img/bg-1.jpg' %});">
<div class="main__center center"> <div class="main__center center">
<div class="main__title">Уроки онлайн-школы LilCity</div> <div class="main__title">Уроки онлайн-школы LilCity</div>

@ -20,38 +20,19 @@
</div> </div>
</div> </div>
<div class="casing__col"> <div class="casing__col">
{% if not profile %}
<div class="casing__head">
{% if is_previous %}
<div class="casing__title title">Запись уроков</div>
{% else %}
<div class="casing__title title">Новые уроки</div>
{% endif %}
<label class="casing__switcher switcher">
<span class="switcher__wrap">
<a href="{% url 'school:school' %}?is_previous=true" class="switcher__item{% if is_previous %} active{% endif %}">запись уроков</a>
<a href="{% url 'school:school' %}" class="switcher__item{% if not is_previous %} active{% endif %}">новые уроки</a>
</span>
</label>
</div>
{% endif %}
<div class="casing__timing timing js-timing"> <div class="casing__timing timing js-timing">
<div class="timing__week"> <div class="timing__week">
{% if is_previous and not live_lessons_exists %} <div class="subtitle2">Новые уроки</div>
Записей уроков пока нет {% for school_schedule in school_schedules_sorted %}
{% else %} {% include 'blocks/schedule_item.html' with school_schedule=school_schedule live_lesson=school_schedule.current_live_lesson %}
{% if is_previous %} {% endfor %}
{% for live_lesson in live_lessons %} {% if prev_live_lessons_exists %}
{% if live_lesson.school_schedule and live_lesson.title %} <div class="subtitle2">Прошедшие уроки</div>
{% include 'blocks/schedule_item.html' with school_schedule=live_lesson.school_schedule live_lesson=live_lesson %} {% for live_lesson in prev_live_lessons %}
{% endif %} {% if live_lesson.school_schedule and live_lesson.title %}
{% endfor %} {% include 'blocks/schedule_item.html' with school_schedule=live_lesson.school_schedule live_lesson=live_lesson %}
{% else %} {% endif %}
{% for school_schedule in school_schedules_sorted %} {% endfor %}
{% include 'blocks/schedule_item.html' with school_schedule=school_schedule live_lesson=school_schedule.current_live_lesson %}
{% endfor %}
{% endif %}
{% endif %} {% endif %}
</div> </div>
<div class="timing__week"> <div class="timing__week">

@ -110,9 +110,7 @@ class SchoolView(TemplateView):
def get_context_data(self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
is_previous = 'is_previous' in self.request.GET
date_now = now().date() date_now = now().date()
yesterday = date_now - timedelta(days=1)
now_time = now() now_time = now()
try: try:
school_schedule = SchoolSchedule.objects.get(weekday=now_time.isoweekday()) school_schedule = SchoolSchedule.objects.get(weekday=now_time.isoweekday())
@ -132,8 +130,8 @@ class SchoolView(TemplateView):
key=lambda ss: ss.current_live_lesson and ss.current_live_lesson.date) key=lambda ss: ss.current_live_lesson and ss.current_live_lesson.date)
except Exception: except Exception:
school_schedules_sorted = school_schedules school_schedules_sorted = school_schedules
live_lessons = [] prev_live_lessons = []
live_lessons_exists = False prev_live_lessons_exists = False
subscription_ends = None subscription_ends = None
school_payment_exists = False school_payment_exists = False
school_schedules_purchased = [] school_schedules_purchased = []
@ -178,31 +176,29 @@ class SchoolView(TemplateView):
weekdays__len__gt=0, weekdays__len__gt=0,
) )
if is_previous: # берем все подписки, которые были в периоде
# берем все подписки, которые были в периоде for sp in prev_school_payments:
for sp in prev_school_payments: # берем все уроки в оплаченном промежутке
# берем все уроки в оплаченном промежутке date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])]
date_range = [max(sp.date_start, prev_range[0]), min(sp.date_end, prev_range[1])] prev_live_lessons = LiveLesson.objects.filter(
live_lessons += LiveLesson.objects.filter( date__range=date_range,
date__range=date_range, deactivated_at__isnull=True,
deactivated_at__isnull=True, date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)),
date__week_day__in=list(map(lambda x: 1 if x == 7 else x+1, sp.weekdays)), ).values_list('id', flat=True)
).values_list('id', flat=True) prev_live_lessons = LiveLesson.objects.filter(id__in=set(prev_live_lessons)).order_by('-date')
live_lessons = LiveLesson.objects.filter(id__in=set(live_lessons)).order_by('-date') prev_live_lessons_exists = prev_live_lessons.exists()
live_lessons_exists = live_lessons.exists() if prev_live_lessons_exists:
if live_lessons_exists: school_schedules_dict = {ss.weekday: ss for ss in school_schedules}
school_schedules_dict = {ss.weekday: ss for ss in school_schedules} school_schedules_dict[0] = school_schedules_dict.get(7)
school_schedules_dict[0] = school_schedules_dict.get(7) for ll in prev_live_lessons:
for ll in live_lessons: ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday())
ll.school_schedule = school_schedules_dict.get(ll.date.isoweekday()) else:
else: prev_live_lessons = []
live_lessons = []
context.update({ context.update({
'online': online, 'online': online,
'live_lessons': live_lessons, 'prev_live_lessons': prev_live_lessons,
'live_lessons_exists': live_lessons_exists, 'prev_live_lessons_exists': prev_live_lessons_exists,
'is_previous': is_previous,
'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6], 'course_items': Course.objects.filter(status=Course.PUBLISHED)[:6],
'is_purchased': school_payment_exists, 'is_purchased': school_payment_exists,
'is_purchased_future': False, 'is_purchased_future': False,

@ -28,6 +28,7 @@ class UserEditForm(forms.ModelForm):
pinterest = forms.URLField(required=False) pinterest = forms.URLField(required=False)
youtube = forms.URLField(required=False) youtube = forms.URLField(required=False)
vkontakte = forms.URLField(required=False) vkontakte = forms.URLField(required=False)
site = forms.URLField(required=False)
photo = forms.ImageField(required=False) photo = forms.ImageField(required=False)
class Meta: class Meta:
@ -53,6 +54,7 @@ class UserEditForm(forms.ModelForm):
'pinterest', 'pinterest',
'youtube', 'youtube',
'vkontakte', 'vkontakte',
'site',
'photo', 'photo',
) )

@ -0,0 +1,18 @@
# Generated by Django 2.0.6 on 2018-11-15 09:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0025_merge_20180927_2353'),
]
operations = [
migrations.AddField(
model_name='user',
name='site',
field=models.URLField(blank=True, default='', null=True),
),
]

@ -0,0 +1,20 @@
# Generated by Django 2.0.6 on 2018-11-16 16:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0022_auto_20180815_2129'),
('user', '0026_user_site'),
]
operations = [
migrations.AddField(
model_name='user',
name='gallery',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.Gallery', verbose_name='Галерея'),
),
]

@ -71,6 +71,7 @@ class User(AbstractUser):
pinterest = models.URLField(default='', null=True, blank=True) pinterest = models.URLField(default='', null=True, blank=True)
youtube = models.URLField(default='', null=True, blank=True) youtube = models.URLField(default='', null=True, blank=True)
vkontakte = models.URLField('ВКонтакте', default='', null=True, blank=True) vkontakte = models.URLField('ВКонтакте', default='', null=True, blank=True)
site = models.URLField(default='', null=True, blank=True)
fb_id = models.BigIntegerField(null=True, blank=True, unique=True) fb_id = models.BigIntegerField(null=True, blank=True, unique=True)
fb_data = pgfields.JSONField(default={}, null=True, blank=True) fb_data = pgfields.JSONField(default={}, null=True, blank=True)
is_email_proved = models.BooleanField( is_email_proved = models.BooleanField(
@ -83,6 +84,10 @@ class User(AbstractUser):
allow_unicode=True, null=True, blank=True, allow_unicode=True, null=True, blank=True,
max_length=100, unique=True, db_index=True, max_length=100, unique=True, db_index=True,
) )
gallery = models.ForeignKey(
'content.Gallery', on_delete=models.CASCADE,
verbose_name='Галерея', null=True, blank=True,
)
objects = UserManager() objects = UserManager()

@ -0,0 +1,35 @@
{% extends "templates/lilcity/index.html" %}
{% load static %}
{% block head %}
{{ block.super }}
<style>
.kit__photo {
width: 140px;
height: 140px;
}
</style>
{% endblock head %}
{% block pre_app_js %}
<script>
window.LIL_STORE.data.user_gallery_images = [{% for img in user.gallery.gallery_images.reverse %}{
id: {{ img.id }},
image_thumbnail_url: '{{ img.img.image_thumbnail.url }}',
image_id: {{ img.img.id }},
},{% endfor %}];
</script>
<script src="{% static 'userGalleryEdit.js' %}"></script>
{% endblock pre_app_js %}
{% block content %}
<div class="section section_gray">
<div class="section__center center center_sm">
<div class="title">Редактировать работы</div>
<div>
<block-images :images="$root.store.data.user_gallery_images" :access-token="$root.store.accessToken" no-title="1"/>
</div>
<a href="#" id="gallery-save" class="btn">Сохранить</a>
</div>
</div>
{% endblock %}

@ -255,7 +255,16 @@
{% for error in form.vkontakte.errors %} {% for error in form.vkontakte.errors %}
<div class="field__error">{{ error }}</div> <div class="field__error">{{ error }}</div>
{% endfor %} {% endfor %}
</div> </div>
<div class="form__field field{% if form.site.errors %} error{% endif %}">
<div class="field__label">САЙТ</div>
<div class="field__wrap">
<input name='site' class="field__input" type="text" placeholder="https://site.com" value="{% if user.site %}{{ user.site }}{% endif %}">
</div>
{% for error in form.site.errors %}
<div class="field__error">{{ error }}</div>
{% endfor %}
</div>
</div> </div>
<div class="form__foot"> <div class="form__foot">
<button type="submit" class="form__btn btn btn_md">СОХРАНИТЬ</button> <button type="submit" class="form__btn btn btn_md">СОХРАНИТЬ</button>

@ -67,12 +67,13 @@
<div class="section__center center"> <div class="section__center center">
<div class="tabs js-tabs"> <div class="tabs js-tabs">
<div class="tabs__nav"> <div class="tabs__nav">
<button class="tabs__btn js-tabs-btn active">МОИ ПОКУПКИ</button> <a href="#purchases" class="tabs__btn js-tabs-btn active">МОИ ПОКУПКИ</a>
{% if is_author %} {% if is_author %}
<button class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ <a href="#courses" class="tabs__btn js-tabs-btn">ОПУБЛИКОВАННЫЕ
<span class="mobile-hide">КУРСЫ</span> <span class="mobile-hide">КУРСЫ</span>
</button> </a>
{% endif %} {% endif %}
<a href="#works" class="tabs__btn js-tabs-btn">МОИ РАБОТЫ</a>
</div> </div>
<div class="tabs__container"> <div class="tabs__container">
<div class="tabs__item js-tabs-item" style="display: block;"> <div class="tabs__item js-tabs-item" style="display: block;">
@ -138,6 +139,24 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="tabs__item js-tabs-item">
<div class="center center_sm">
<div style="text-align: right; margin-bottom: 15px;">
<a class="btn btn_md btn_stroke" href="{% url 'user-gallery-edit' %}">Редактировать</a>
</div>
<div class="examples gallery">
{% for image in user.gallery.gallery_images.reverse %}
<div class="examples__item">
<a href="{{ image.img.image.url }}">
{% thumbnail image.img.image "165x165" crop="center" as im %}
<img class="examples__pic" src="{{ im.url }}">
{% endthumbnail %}
</a>
</div>
{% endfor %}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -313,3 +313,8 @@ class BonusHistoryView(TemplateView):
'Перейдите по ссылке и получите скидку %d%% на первую покупку' \ 'Перейдите по ссылке и получите скидку %d%% на первую покупку' \
% (request.user.get_full_name(), config.REFERRAL_BONUS) % (request.user.get_full_name(), config.REFERRAL_BONUS)
return self.render_to_response(context) return self.render_to_response(context)
@method_decorator(login_required, name='dispatch')
class UserGalleryEditView(TemplateView):
template_name = 'user/edit-gallery.html'

@ -22,6 +22,7 @@
urls: { urls: {
courses: "{% url 'courses' %}", courses: "{% url 'courses' %}",
userProfileEdit: "{% url 'user-edit-profile' %}", userProfileEdit: "{% url 'user-edit-profile' %}",
userProfile: "{% url 'user-profile' %}",
userBonuses: "{% url 'user-bonuses' %}", userBonuses: "{% url 'user-bonuses' %}",
faq: "{% url 'faq' %}", faq: "{% url 'faq' %}",
}, },

@ -165,6 +165,6 @@
(function(w, d, s, h, id) { w.roistatProjectId = id; w.roistatHost = h; var p = d.location.protocol == "https:" ? "https://" : "http://"; var u = /^.*roistat_visit=[^;]+(.*)?$/.test(d.cookie) ? "/dist/module.js" : "/api/site/1.0/"+id+"/init"; var js = d.createElement(s); js.charset="UTF-8"; js.async = 1; js.src = p+h+u; var js2 = d.getElementsByTagName(s)[0]; js2.parentNode.insertBefore(js, js2);})(window, document, 'script', 'cloud.roistat.com', '{% setting "ROISTAT_COUNTER_ID" %}'); (function(w, d, s, h, id) { w.roistatProjectId = id; w.roistatHost = h; var p = d.location.protocol == "https:" ? "https://" : "http://"; var u = /^.*roistat_visit=[^;]+(.*)?$/.test(d.cookie) ? "/dist/module.js" : "/api/site/1.0/"+id+"/init"; var js = d.createElement(s); js.charset="UTF-8"; js.async = 1; js.src = p+h+u; var js2 = d.getElementsByTagName(s)[0]; js2.parentNode.insertBefore(js, js2);})(window, document, 'script', 'cloud.roistat.com', '{% setting "ROISTAT_COUNTER_ID" %}');
</script> </script>
{% block foot %}{% endblock foot %} {% block foot %}{% endblock foot %}
{% block body_js %}{% endblock body_js %} {% block foot_js %}{% endblock foot_js %}
</body> </body>
</html> </html>

@ -30,7 +30,7 @@ from apps.user.views import (
ProfileEditView, NotificationEditView, ProfileEditView, NotificationEditView,
PaymentHistoryView, resend_email_verify, PaymentHistoryView, resend_email_verify,
SubscribeView, ProfileView, SubscribeView, ProfileView,
BonusHistoryView) BonusHistoryView, UserGalleryEditView)
from apps.payment.views import ( from apps.payment.views import (
CourseBuySuccessView, CourseBuyView, CourseBuySuccessView, CourseBuyView,
PaymentwallCallbackView, SchoolBuySuccessView, PaymentwallCallbackView, SchoolBuySuccessView,
@ -75,6 +75,7 @@ urlpatterns = [
path('user/notifications', NotificationEditView.as_view(), name='user-edit-notifications'), path('user/notifications', NotificationEditView.as_view(), name='user-edit-notifications'),
path('user/payments', PaymentHistoryView.as_view(), name='user-edit-payments'), path('user/payments', PaymentHistoryView.as_view(), name='user-edit-payments'),
path('user/bonuses', BonusHistoryView.as_view(), name='user-bonuses'), path('user/bonuses', BonusHistoryView.as_view(), name='user-bonuses'),
path('user/gallery-edit', UserGalleryEditView.as_view(), name='user-gallery-edit'),
path('user/resend-email-verify', resend_email_verify, name='resend-email-verify'), path('user/resend-email-verify', resend_email_verify, name='resend-email-verify'),
path('subscribe', SubscribeView.as_view(), name='subscribe'), path('subscribe', SubscribeView.as_view(), name='subscribe'),
path('subscribe/success', TemplateView.as_view(template_name='templates/lilcity/subscribe_success.html'), name='subscribe-success'), path('subscribe/success', TemplateView.as_view(template_name='templates/lilcity/subscribe_success.html'), name='subscribe-success'),

@ -3,63 +3,38 @@
<div v-if="!courseLoading && !mounting"> <div v-if="!courseLoading && !mounting">
<form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit"> <form v-if="viewSection !== 'lessons-edit'" @submit.prevent="onSubmit">
<div class="info"> <div class="info">
<div class="info__section" :style="coverBackgroundStyle"> <div class="info__section">
<div class="info__main"> <div class="courses__item">
<div class="info__head"> <div class="courses__preview">
<div class="info__user"> <img class="courses__pic" :src="course.coverImage || defaultCover" width="300px" />
<div class="info__ava ava"> <div class="upload" v-if="! course.coverImage">
<img :src="authorPicture" alt="Аватар" class="ava__pic"> <div class="upload__title">Загрузить превью</div>
</div>
<div v-if="me" class="info__group info__field--light">
<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"> <input type="file" class="upload__file" @change="onCoverImageSelected">
</div> </div>
<a href="#" title="Удалить превью" class="course-delete-cover" v-if="course.coverImage" @click="removeCover">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</a>
</div> </div>
<div class="info__title"> <div class="courses__details">
<div class="info__field field field_info" <div class="courses__theme theme field info__field--light" v-if="!live" v-bind:class="{ error: (!$v.live && $v.course.category.$dirty || showErrors) && $v.course.category.$invalid }">
v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }"> <lil-select :value.sync="course.category" :options="categoryOptions"
<div class="field__label">{{titles.courseTitle}}</div>
<div class="field__wrap">
<textarea class="field__textarea"
rows="1"
:title="titles.courseTitle"
v-autosize="course.title"
@change="onCourseNameInput"
v-model="course.title"></textarea>
</div>
</div>
<div class="info__field field field_info field_short_description"
v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
<div class="field__label">{{titles.shortDescription}}</div>
<div class="field__wrap">
<vue-redactor :value="course.short_description"
v-on:update:value="(value) => { this.course.short_description = value; }" />
</div>
</div>
</div>
<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>
<div class="field__wrap">
<lil-select :value.sync="course.category" :options="categoryOptions"
placeholder="Выберите категорию"/> placeholder="Выберите категорию"/>
</div>
</div>
<div class="info__field field field_info"
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
@input="$v.course.duration.$touch()">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div> </div>
<div class="courses__price" v-if="course.is_paid && course.price">{{ course.price }}</div>
</div>
<div class="courses__title field field" v-bind:class="{ error: ($v.course.title.$dirty || showErrors) && $v.course.title.$invalid }">
<textarea class="field__textarea"
rows="1"
:title="titles.courseTitle"
v-autosize="course.title"
@change="onCourseNameInput"
v-model="course.title"
placeholder="Добавить заголовок"></textarea>
</div>
<div class="courses__content field" v-bind:class="{ error: ($v.course.short_description.$dirty || showErrors) && $v.course.short_description.$invalid }">
<vue-redactor :value.sync="course.short_description" placeholder="Добавить краткое описание"/>
</div> </div>
</div> </div>
</div> </div>
@ -90,6 +65,16 @@
</div> </div>
</div> --> </div> -->
<div class="info__field field field_info" v-if="!live"
v-bind:class="{ error: ($v.course.duration.$dirty || showErrors) && $v.course.duration.$invalid }">
<div class="field__label field__label_gray">ПРОДОЛЖИТЕЛЬНОСТЬ</div>
<div class="field__wrap field__wrap__appended">
<input type="text" class="field__input field__input__appended" v-model.number="course.duration"
@input="$v.course.duration.$touch()">
<button disabled class="field__append">{{pluralize(course.duration, ['день', 'дня', 'дней'])}}</button>
</div>
</div>
<div v-if="!live" class="info__field field"> <div v-if="!live" class="info__field field">
<div class="field__label field__label_gray">ДОСТУП</div> <div class="field__label field__label_gray">ДОСТУП</div>
<div class="field__wrap"> <div class="field__wrap">
@ -155,7 +140,7 @@
</div> </div>
<div class="section"> <div class="section">
<div class="section__center center"> <div class="section__center center">
<div class="kit"> <div class="kit" style="margin: 0 auto;">
<div v-if="!live" 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" <button class="kit__btn btn btn_lg"
v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }" v-bind:class="{ 'btn_stroke': viewSection === 'course', 'btn_gray': viewSection !== 'course' }"
@ -267,6 +252,7 @@
users: null, users: null,
ROLE_ADMIN: ROLE_ADMIN, ROLE_ADMIN: ROLE_ADMIN,
slugChanged: false, slugChanged: false,
defaultCover: '/img/no_cover.png',
course: { course: {
title: '', title: '',
status: null, status: null,
@ -444,6 +430,16 @@
} }
}, },
methods: { methods: {
removeCover() {
if(! this.course.coverImageId){
return;
}
api.removeImage(this.course.coverImageId, this.accessToken)
.then(response => {
this.course.coverImageId = null;
this.course.coverImage = null;
});
},
onCoverImageSelected(event) { onCoverImageSelected(event) {
let file = event.target.files[0]; let file = event.target.files[0];
let reader = new FileReader(); let reader = new FileReader();
@ -1005,9 +1001,6 @@
// this.updateViewSection(window.location); // this.updateViewSection(window.location);
}, },
computed: { computed: {
coverBackgroundStyle() {
return this.course.coverImage ? `background-image: url(${this.course.coverImage});` : '';
},
displayPrice: { displayPrice: {
get: function () { get: function () {
return this.course.is_paid ? (this.course.price || '') : ''; return this.course.is_paid ? (this.course.price || '') : '';
@ -1175,10 +1168,67 @@
overflow: scroll; overflow: scroll;
} }
.field_short_description .redactor-box { .courses__item {
flex: 0 0 300px;
}
.courses__item .field {
margin-bottom: 0;
}
.courses__content .redactor-box {
overflow-x: visible; overflow-x: visible;
overflow-y: auto; overflow-y: auto;
max-height: 200px; max-height: 200px;
background: none;
margin-top: 10px;
}
.courses__content .redactor-layer{
background: none;
}
.courses__theme {
flex: 1;
}
.courses__price {
margin-left: 20px;
}
.courses__preview {
.upload {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.upload__title {
color: #888888;
font-size: 16px;
margin-top: 100px;
width: 100%;
text-align: center;
}
.upload__file {
width: 100%;
height: 100%;
}
}
.course-delete-cover {
left: 5px;
position: absolute;
bottom: 3px;
}
.select__head {
font-size: inherit;
line-height: inherit;
height: auto;
} }
</style> </style>

@ -1,7 +1,29 @@
<template> <template>
<div> <div>
<vue-draggable :list="content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }"> <vue-draggable :list="content" @start="drag=true" @end="drag=false" :options="{ handle: '.sortable__handle' }">
<div v-for="(block, index) in content" :key="block.id ? block.id : block.uuid"> <div v-for="(block, index) in content" :key="block.id ? block.id : block.uuid" class="kit__section kit__section--block">
<div class="kit__section-remove">
<button if="index != 0" type="button" @click="moveUp(index)">
<svg class="icon icon-arrow-up">
<use xlink:href="/static/img/sprite.svg#icon-arrow-down"></use>
</svg>
</button>
<button if="index < (content.length - 1)" type="button" @click="moveDown(index)">
<svg class="icon icon-arrow-down">
<use xlink:href="/static/img/sprite.svg#icon-arrow-down"></use>
</svg>
</button>
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<button type="button" @click="onBlockRemoved(index)">
<svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg>
</button>
</div>
<block-text v-if="block.type === 'text'" <block-text v-if="block.type === 'text'"
:index="index" :index="index"
:title.sync="block.data.title" :title.sync="block.data.title"
@ -55,6 +77,22 @@
name: 'block-content', name: 'block-content',
props: ['content'], props: ['content'],
methods: { methods: {
moveUp(blockIndex) {
if(blockIndex <= 0){
return;
}
const block = this.content[blockIndex];
const prevBlock = this.content[blockIndex - 1];
this.content.splice(blockIndex - 1, 2, block, prevBlock);
},
moveDown(blockIndex) {
if(blockIndex >= this.content.length - 1){
return;
}
const block = this.content[blockIndex];
const nextBlock = this.content[blockIndex + 1];
this.content.splice(blockIndex, 2, nextBlock, block);
},
onBlockRemoved(blockIndex) { onBlockRemoved(blockIndex) {
const remove = () => { const remove = () => {
// Удаляем блок из Vue // Удаляем блок из Vue

@ -1,17 +1,5 @@
<template> <template>
<div class="kit__section kit__section--block"> <div>
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<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="kit__field field">
<div class="field__wrap field__wrap--title"> <div class="field__wrap field__wrap--title">
<input type="text" <input type="text"
@ -44,9 +32,6 @@
onUpdateImageId(newValue) { onUpdateImageId(newValue) {
this.$emit('update:imageId', newValue); this.$emit('update:imageId', newValue);
}, },
onRemove() {
this.$emit('remove', this.index);
}
}, },
components: { components: {
'lil-image': LilImage, 'lil-image': LilImage,
@ -56,4 +41,4 @@
<style scoped> <style scoped>
</style> </style>

@ -1,17 +1,5 @@
<template> <template>
<div class="kit__section kit__section--block"> <div>
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<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"> <div class="kit__row">
<lil-image :image-id="imageId" :image-url="imageUrl" v-on:update:imageUrl="onUpdateImageUrl" <lil-image :image-id="imageId" :image-url="imageUrl" v-on:update:imageUrl="onUpdateImageUrl"
v-on:update:imageId="onUpdateImageId" :access-token="accessToken"/> v-on:update:imageId="onUpdateImageId" :access-token="accessToken"/>
@ -55,9 +43,6 @@
onUpdateImageId(newValue) { onUpdateImageId(newValue) {
this.$emit('update:imageId', newValue); this.$emit('update:imageId', newValue);
}, },
onRemove() {
this.$emit('remove', this.index);
}
}, },
components: { components: {
'lil-image': LilImage, 'lil-image': LilImage,

@ -1,18 +1,6 @@
<template> <template>
<div class="kit__section kit__section--block"> <div>
<div v-if="!readOnly" class="kit__section-remove"> <div v-if="! noTitle" class="kit__field field">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<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 field__wrap--title"> <div class="field__wrap field__wrap--title">
<input :readonly="readOnly" type="text" <input :readonly="readOnly" type="text"
:value="title" :value="title"
@ -24,7 +12,7 @@
<div class="kit__gallery"> <div class="kit__gallery">
<div class="kit__preview" v-for="(image, index) in images" v-bind:class="{ 'kit__preview--loading': image.loading }"> <div class="kit__preview" v-for="(image, index) in images" v-bind:class="{ 'kit__preview--loading': image.loading }">
<img :src="image.image_thumbnail_url" class="kit__pic"> <img :src="image.image_thumbnail_url" class="kit__pic">
<button type="button" @click="onRemoveImage(index)"> <button class="kit__delete-photo" type="button" @click="onRemoveImage(index)">
<svg class="icon icon-delete"> <svg class="icon icon-delete">
<use xlink:href="/static/img/sprite.svg#icon-delete"></use> <use xlink:href="/static/img/sprite.svg#icon-delete"></use>
</svg> </svg>
@ -45,36 +33,31 @@
export default { export default {
name: "block-images", name: "block-images",
props: ["index", "title", "images", "accessToken", "readOnly"], props: ["index", "title", "images", "accessToken", "readOnly", "longSide", "noTitle"],
methods: { methods: {
onTitleChange(event) { onTitleChange(event) {
this.$emit('update:title', event.target.value); this.$emit('update:title', event.target.value);
}, },
onImageAdded(event) { onImageAdded(event) {
const longSide = +this.longSide || 1600;
Array.from(event.target.files).forEach((file) => { Array.from(event.target.files).forEach((file) => {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = () => { reader.onload = () => {
let images = this.images; let images = this.images;
console.log('images before before', JSON.stringify(images));
images.push({ images.push({
src: reader.result,
loading: true, loading: true,
}); });
this.$emit('update:images', images); this.$emit('update:images', images);
const index = images.length - 1;
api.uploadImage(reader.result, this.accessToken) api.uploadImage(reader.result, this.accessToken)
.then((response) => { .then((response) => {
let images = this.images; let images = this.images;
images.forEach((image, index) => { images[index].image_id = response.data.id;
if (image.src === reader.result) { images[index].loading = false;
images[index].image_id = response.data.id; images[index].image_url = response.data.image;
images[index].loading = false; images[index].image_thumbnail_url = response.data.image_thumbnail;
images[index].image_url = response.data.image;
images[index].image_thumbnail_url = response.data.image_thumbnail;
}
});
console.log('images after', JSON.stringify(images));
this.$emit('update:images', images); this.$emit('update:images', images);
}) })
.catch((error) => { .catch((error) => {
@ -86,9 +69,6 @@
} }
}); });
}, },
onRemove() {
this.$emit('remove', this.index);
},
onRemoveImage(index) { onRemoveImage(index) {
let images = this.images; let images = this.images;
let id = images[index].image_id; let id = images[index].image_id;

@ -1,17 +1,5 @@
<template> <template>
<div class="kit__section kit__section--block"> <div>
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<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="kit__field field">
<div class="field__wrap field__wrap--title"> <div class="field__wrap field__wrap--title">
<input type="text" <input type="text"
@ -42,9 +30,6 @@
onTextChange(newValue) { onTextChange(newValue) {
this.$emit('update:text', newValue); this.$emit('update:text', newValue);
}, },
onRemove() {
this.$emit('remove', this.index);
}
}, },
components: { components: {
'vue-redactor': VueRedactor, 'vue-redactor': VueRedactor,

@ -1,17 +1,5 @@
<template> <template>
<div class="kit__section kit__section--block"> <div>
<div class="kit__section-remove">
<button class="sortable__handle" type="button">
<svg class="icon icon-hamburger">
<use xlink:href="/static/img/sprite.svg#icon-hamburger"></use>
</svg>
</button>
<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="kit__field field">
<div class="field__wrap field__wrap--title"> <div class="field__wrap field__wrap--title">
<input type="text" <input type="text"
@ -46,13 +34,10 @@
onVideoUrlChange(event) { onVideoUrlChange(event) {
this.$emit('update:videoUrl', event.target.value); this.$emit('update:videoUrl', event.target.value);
}, },
onRemove() {
this.$emit('remove', this.index);
}
} }
} }
</script> </script>
<style scoped> <style scoped>
</style> </style>

@ -20,6 +20,7 @@ import "./modules/mixpanel";
import "../sass/app.sass"; import "../sass/app.sass";
import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import Vuelidate from 'vuelidate'; import Vuelidate from 'vuelidate';
import VueAutosize from '../components/directives/autosize' import VueAutosize from '../components/directives/autosize'
@ -63,7 +64,6 @@ const app = new Vue({
data() { data() {
return { return {
store: window.LIL_STORE, store: window.LIL_STORE,
} } },
},
components: components components: components
}); });

@ -199,6 +199,14 @@ export const api = {
gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]}, gallery: {images: (courseJSON.gallery) ? courseJSON.gallery.gallery_images:[]},
} }
}, },
convertGalleryImagesJson: (images) => {
return images.map((galleryImage) => {
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.image_id,
}
});
},
convertContentJson: (contentJson, forSaving) => { convertContentJson: (contentJson, forSaving) => {
if(forSaving){ if(forSaving){
return contentJson.map((block, index) => { return contentJson.map((block, index) => {
@ -244,12 +252,7 @@ export const api = {
'uuid': block.uuid, 'uuid': block.uuid,
'position': ++index, 'position': ++index,
'title': block.data.title, 'title': block.data.title,
'images': block.data.images.map((galleryImage) => { 'images': api.convertGalleryImagesJson(block.data.images),
return {
'id': galleryImage.id ? galleryImage.id : null,
'img': galleryImage.image_id,
}
}),
} }
} }
} else if (block.type === 'video') { } else if (block.type === 'video') {
@ -486,5 +489,14 @@ export const api = {
'Authorization': `Token ${window.LIL_STORE.accessToken}`, 'Authorization': `Token ${window.LIL_STORE.accessToken}`,
} }
}); });
} },
saveUserGallery: (images) => {
return api.patch(`/api/v1/user-gallery/${window.LIL_STORE.user.id}/`, {
images: api.convertGalleryImagesJson(images),
}, {
headers: {
'Authorization': `Token ${window.LIL_STORE.accessToken}`,
}
});
},
}; };

@ -0,0 +1,14 @@
import BlockImages from '../components/blocks/BlockImages.vue';
import $ from 'jquery';
import {api} from "./modules/api";
window.LIL_STORE.components['block-images'] = BlockImages;
$(document).ready(function () {
$('#gallery-save').click(e => {
e.preventDefault();
api.saveUserGallery(window.LIL_STORE._user_gallery_images).then(response => {
window.location.href = '/user/profile/#works';
});
});
});

@ -2162,11 +2162,13 @@ a.grey-link
color: $pink color: $pink
border-bottom: 1px $pink solid border-bottom: 1px $pink solid
.error.field .redactor-box
border-bottom: 1px $pink solid
.info__field--light .info__field--light
.select .select
.select__head .select__head
color: #525252 color: #525252
font-size: 15px
&:after &:after
border-color: #525252 transparent transparent transparent border-color: #525252 transparent transparent transparent
&.selected &.selected
@ -3266,6 +3268,7 @@ a.grey-link
justify-content: center justify-content: center
&__btn &__btn
height: 56px height: 56px
line-height: 56px
border-bottom: 1px solid $border border-bottom: 1px solid $border
+fb +fb
font-size: 12px font-size: 12px
@ -3476,22 +3479,10 @@ a.grey-link
&__section &__section
display: flex display: flex
position: relative position: relative
background-position: 50% 50% flex: 0 0 50%
background-size: cover
flex: 0 0 calc(50% + 169px)
justify-content: flex-end justify-content: flex-end
z-index: 4
&:before
content: ''
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background-image: linear-gradient(135deg, rgba(255, 226, 235, 0.75) 0%, rgba(216, 245, 246, 0.75) 100%)
z-index: -2
&__sidebar &__sidebar
flex: 0 0 calc(50% - 169px) flex: 0 0 50%
&__main &__main
display: flex display: flex
max-width: 675px max-width: 675px
@ -3616,19 +3607,23 @@ a.grey-link
&__section &__section
margin-bottom: 60px margin-bottom: 60px
&--block &--block
box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.06) /* box-shadow: 0 10px 50px 0 rgba(0, 0, 0, 0.06) */
border-radius: 10px /* border-radius: 10px */
padding: 15px padding: 15px
position: relative position: relative
&-remove &-remove
position: absolute position: absolute
right: 15px right: 15px
top: 20px top: 20px
.icon-delete button:not(:last-child)
margin-right: 10px
.icon-delete, .icon-arrow-up, .icon-arrow-down
width: 1.3em width: 1.3em
height: 1.3em height: 1.3em
transition: fill 0.3s ease transition: fill 0.3s ease
&:hover .icon-arrow-up, .icon-arrow-down
fill: #C8C8C8
.icon-delete:hover, .icon-arrow-up:hover, .icon-arrow-down:hover
fill: #000 fill: #000
&__nav &__nav
display: flex display: flex
@ -3659,6 +3654,10 @@ a.grey-link
flex: 0 0 140px flex: 0 0 140px
+m +m
flex: 0 0 110px flex: 0 0 110px
&__delete-photo
position: absolute
bottom: 2px
left: 3px
&__photo &__photo
display: flex display: flex
position: relative position: relative
@ -3704,6 +3703,7 @@ a.grey-link
flex-wrap: wrap flex-wrap: wrap
&__gallery &__photo, &__gallery &__photo,
&__preview &__preview
position: relative
margin: 0 10px 20px margin: 0 10px 20px
flex: 0 0 calc(25% - 20px) flex: 0 0 calc(25% - 20px)
&--loading &--loading
@ -3712,6 +3712,9 @@ a.grey-link
display: block display: block
width: 100% width: 100%
object-fit: contain object-fit: contain
transform: translateY(-50%)
top: 50%
position: relative
&__theme &__theme
margin-bottom: 30px margin-bottom: 30px
padding-bottom: 5px padding-bottom: 5px
@ -3721,6 +3724,8 @@ a.grey-link
text-align: center text-align: center
&__submit &__submit
width: 220px width: 220px
&__field .field__wrap--title
margin-right: 125px;
.editor .editor
position: relative position: relative
@ -4062,6 +4067,11 @@ a.grey-link
letter-spacing: 1px letter-spacing: 1px
+m +m
margin-right: 0 margin-right: 0
&__item a.timing__btn
opacity: 0;
transition: 0.5s ease;
&__item:hover a.timing__btn
opacity: 1;
&__pic &__pic
display: block display: block
width: 100% width: 100%

@ -8,6 +8,11 @@
height: 1em; height: 1em;
fill: #333; fill: #333;
} }
.icon-arrow-up {
width: 1.71em;
height: 1em;
fill: #333;
}
.icon-arrow-left { .icon-arrow-left {
width: 1.33em; width: 1.33em;
height: 1em; height: 1em;

@ -10,6 +10,7 @@ module.exports = {
app: "./src/js/app.js", app: "./src/js/app.js",
courseRedactor: "./src/js/course-redactor.js", courseRedactor: "./src/js/course-redactor.js",
contestRedactor: "./src/js/contest-redactor.js", contestRedactor: "./src/js/contest-redactor.js",
userGalleryEdit: "./src/js/user-gallery-edit.js",
mixpanel: "./src/js/third_party/mixpanel-2-latest.js", mixpanel: "./src/js/third_party/mixpanel-2-latest.js",
sprite: glob('./src/icons/*.svg'), sprite: glob('./src/icons/*.svg'),
images: glob('./src/img/*.*'), images: glob('./src/img/*.*'),

Loading…
Cancel
Save