Add notifications on work page, rebase already exists with notifications.js
This commit is contained in:
parent
83939f6681
commit
cff6443eca
@ -1,14 +1,13 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta, datetime, date
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import timezone
|
||||
from zenpy import Zenpy
|
||||
from zenpy.lib.exception import APIException
|
||||
|
||||
|
||||
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \
|
||||
ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, ACTRL_ZENDESK_SUBDOMAIN
|
||||
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
|
||||
@ -666,3 +665,13 @@ def log(user, admin=0):
|
||||
logger.addHandler(csvhandler)
|
||||
logger.setLevel('INFO')
|
||||
logger.info(users)
|
||||
|
||||
|
||||
def set_session_params_for_work_page(request, count=None, is_confirm=True):
|
||||
"""
|
||||
Функция для страницы получения прав
|
||||
Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов
|
||||
"""
|
||||
request.session['is_confirm'] = is_confirm
|
||||
request.session['count_tickets'] = count
|
||||
return redirect('work', request.user.id)
|
||||
|
@ -1,14 +0,0 @@
|
||||
<div class="mt-5">
|
||||
{% for message in messages %}
|
||||
<div
|
||||
class="alert alert-{{ message.tags }} alert-dismissible fade show p-2"
|
||||
role="alert"
|
||||
style="display: flex; align-items: center; justify-content: space-between;"
|
||||
>
|
||||
{{ message }}
|
||||
<div>
|
||||
<button type="button" class="btn btn-light p-2" data-bs-dismiss="alert" aria-label="Close">X</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
@ -6,18 +6,16 @@
|
||||
|
||||
{% block heading %}Управление{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}"/>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
{% endblock%}
|
||||
|
||||
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script>
|
||||
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
|
||||
<script src="{% static 'main/js/notifications.js' %}"></script>
|
||||
{% endblock%}
|
||||
{% block content %}
|
||||
<div class="container-md">
|
||||
|
||||
@ -25,6 +23,10 @@
|
||||
<p class="row page-description" id="licences_remaining">Свободных Мест:</p>
|
||||
</div>
|
||||
|
||||
{% for message in messages %}
|
||||
<script>create_notification('{{message}}','','{{message.tags}}',5000)</script>
|
||||
{% endfor %}
|
||||
|
||||
{% block form %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
@ -100,9 +102,6 @@
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% include 'base/success_messages.html' %}
|
||||
</div>
|
||||
|
||||
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -7,7 +7,12 @@
|
||||
{% block heading %}Управление правами{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}" xmlns="http://www.w3.org/1999/html">
|
||||
<link rel="stylesheet" href="{% static 'modules/notifications/dist/notifications.css' %}">
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}
|
||||
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script>
|
||||
<script src="{% static 'main/js/notifications.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -66,8 +71,10 @@
|
||||
<button type="submit" class="default-button">Взять тикеты в работу</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for message in messages %}
|
||||
<script>create_notification('{{message}}','','{{message.tags}}',5000)</script>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'base/success_messages.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -23,7 +23,7 @@ from zenpy.lib.api_objects import User as ZenpyUser
|
||||
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS
|
||||
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
|
||||
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
|
||||
StatisticData, log, ZendeskAdmin
|
||||
StatisticData, log, ZendeskAdmin, set_session_params_for_work_page
|
||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
|
||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||
from .models import UserProfile
|
||||
@ -31,7 +31,6 @@ from .models import UserProfile
|
||||
|
||||
def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False,
|
||||
registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False):
|
||||
|
||||
print(profile_lit, control_lit, work_lit, registration_lit, login_lit)
|
||||
|
||||
context = {
|
||||
@ -182,29 +181,37 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
"""
|
||||
users = get_users_list()
|
||||
if request.user.id == id:
|
||||
if request.session.get('is_confirm', None):
|
||||
messages.success(request, 'Изменения были применены')
|
||||
elif request.session.get('is_confirm', None) is not None:
|
||||
messages.error(request, 'Изменения не были применены')
|
||||
count = request.session.get('count_tickets', None)
|
||||
if count is not None:
|
||||
messages.success(request, f'{count} тикетов назначено')
|
||||
request.session['is_confirm'] = None
|
||||
request.session['count_tickets'] = None
|
||||
|
||||
engineers = []
|
||||
light_agents = []
|
||||
for user in users:
|
||||
|
||||
if user.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
engineers.append(user)
|
||||
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
light_agents.append(user)
|
||||
|
||||
context = setup_context(work_lit=True)
|
||||
context.update({
|
||||
'engineers': engineers,
|
||||
'agents': light_agents,
|
||||
'messages': messages.get_messages(request),
|
||||
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
|
||||
'pagename': 'Управление правами'
|
||||
'pagename': 'Управление правами',
|
||||
})
|
||||
return render(request, 'pages/work.html', context)
|
||||
return redirect("login")
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
def work_hand_over(request: WSGIRequest):
|
||||
"""
|
||||
Функция позволяет текущему пользователю (login_required) сдать права, а именно сменить в Zendesk роль с "engineer" на "light agent"
|
||||
и установить роль "agent" в БД. Действия выполняются, если исходная роль пользователя "engineer".
|
||||
@ -212,8 +219,8 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
:param request: данные текущего пользователя (login_required)
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
make_light_agent(request.user.userprofile,request.user)
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
make_light_agent(request.user.userprofile, request.user)
|
||||
return set_session_params_for_work_page(request)
|
||||
|
||||
|
||||
@login_required()
|
||||
@ -224,22 +231,25 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
:param request: данные текущего пользователя (login_required)
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
zenpy_user, admin = auth_user(request)
|
||||
make_engineer(request.user.userprofile, request.user)
|
||||
return set_session_params_for_work_page(request)
|
||||
|
||||
make_engineer(request.user.userprofile,request.user)
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
|
||||
@login_required()
|
||||
def work_get_tickets(request):
|
||||
zenpy_user, admin = auth_user(request)
|
||||
count_tickets = int(request.GET["count_tickets"])
|
||||
tickets = [ticket for ticket in admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None]
|
||||
for i in range(len(tickets)):
|
||||
if i == count_tickets:
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
tickets[i].assignee = zenpy_user
|
||||
admin.tickets.update(tickets[i])
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
tickets = [ticket for ticket in admin.search(type="ticket") if
|
||||
ticket.group.name == 'Сменная группа' and ticket.assignee is None]
|
||||
count = 0
|
||||
for i in range(len(tickets)):
|
||||
if i == request.GET.get('count_tickets'):
|
||||
return set_session_params_for_work_page(request, count)
|
||||
tickets[i].assignee = zenpy_user
|
||||
admin.tickets.update(tickets[i])
|
||||
count += 1
|
||||
return set_session_params_for_work_page(request, count)
|
||||
return set_session_params_for_work_page(request, is_confirm=False)
|
||||
|
||||
|
||||
def main_page(request: WSGIRequest) -> HttpResponse:
|
||||
|
14
static/main/js/notifications.js
Normal file
14
static/main/js/notifications.js
Normal file
@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
function create_notification(title,description,theme,time){
|
||||
const myNotification = window.createNotification({
|
||||
closeOnClick: true,
|
||||
displayCloseButton: true,
|
||||
positionClass: 'nfc-top-right',
|
||||
theme: theme,
|
||||
showDuration: Number(time),
|
||||
});
|
||||
myNotification({
|
||||
title: title,
|
||||
message: description
|
||||
});
|
||||
};
|
3
static/modules/notifications/.babelrc
Normal file
3
static/modules/notifications/.babelrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["es2015"]
|
||||
}
|
31
static/modules/notifications/.eslintrc.js
Normal file
31
static/modules/notifications/.eslintrc.js
Normal file
@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-console": 0,
|
||||
"no-undef": 0
|
||||
}
|
||||
};
|
30
static/modules/notifications/.gitignore
vendored
Normal file
30
static/modules/notifications/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# IDE files
|
||||
.idea/
|
||||
.DS_Store
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Lock files
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
3
static/modules/notifications/.travis.yml
Normal file
3
static/modules/notifications/.travis.yml
Normal file
@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "7"
|
7
static/modules/notifications/LICENSE.md
Normal file
7
static/modules/notifications/LICENSE.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Notifications license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
104
static/modules/notifications/__tests__/helpers.test.js
Normal file
104
static/modules/notifications/__tests__/helpers.test.js
Normal file
@ -0,0 +1,104 @@
|
||||
const { partial, append, isString, createElement, createParagraph } = require('../src/helpers');
|
||||
|
||||
const addNumbers = (x, y) => x + y;
|
||||
|
||||
const sum = (...numbers) => numbers.reduce((total, current) => total + current, 0);
|
||||
|
||||
describe('Helpers', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('Partial', () => {
|
||||
it('should return a partially applied function', () => {
|
||||
expect(typeof partial(addNumbers, 10)).toEqual('function');
|
||||
});
|
||||
|
||||
it('should execute function when partially applied function is called', () => {
|
||||
expect(partial(addNumbers, 20)(10)).toEqual(30);
|
||||
});
|
||||
|
||||
it('should gather argument', () => {
|
||||
expect(partial(sum, 5, 10)(15, 20, 25)).toEqual(75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Append', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const elementToAppend = document.createElement('h1');
|
||||
elementToAppend.classList.add('heading');
|
||||
elementToAppend.innerText = 'working';
|
||||
|
||||
append(container, elementToAppend);
|
||||
|
||||
const element = document.querySelector('.heading');
|
||||
expect(element);
|
||||
|
||||
expect(element.innerText).toEqual('working');
|
||||
});
|
||||
|
||||
describe('Is string', () => {
|
||||
expect(isString(1)).toEqual(false);
|
||||
expect(isString(null)).toEqual(false);
|
||||
expect(isString(undefined)).toEqual(false);
|
||||
expect(isString({})).toEqual(false);
|
||||
|
||||
expect(isString('')).toEqual(true);
|
||||
expect(isString('a')).toEqual(true);
|
||||
expect(isString('1')).toEqual(true);
|
||||
expect(isString('some string')).toEqual(true);
|
||||
});
|
||||
|
||||
describe('Create element', () => {
|
||||
it('should create an element', () => {
|
||||
expect(createElement('p')).toEqual(document.createElement('p'));
|
||||
expect(createElement('h1')).toEqual(document.createElement('h1'));
|
||||
expect(createElement('ul')).toEqual(document.createElement('ul'));
|
||||
expect(createElement('li')).toEqual(document.createElement('li'));
|
||||
expect(createElement('div')).toEqual(document.createElement('div'));
|
||||
expect(createElement('span')).toEqual(document.createElement('span'));
|
||||
});
|
||||
|
||||
it('should add class names', () => {
|
||||
expect(createElement('div', 'someclass1', 'someclass2').classList.contains('someclass2'));
|
||||
expect(createElement('p', 'para', 'test').classList.contains('para'));
|
||||
|
||||
const mockUl = document.createElement('ul');
|
||||
mockUl.classList.add('nav');
|
||||
mockUl.classList.add('foo');
|
||||
|
||||
expect(createElement('ul', 'nav', 'foo').classList).toEqual(mockUl.classList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create paragraph', () => {
|
||||
it('should create a paragraph', () => {
|
||||
const p = document.createElement('p');
|
||||
p.innerText = 'Some text';
|
||||
expect(createParagraph()('Some text')).toEqual(p);
|
||||
});
|
||||
|
||||
it('should add class names', () => {
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('body-text');
|
||||
p.classList.add('para');
|
||||
|
||||
expect(createParagraph('body-text', 'para')('')).toEqual(p);
|
||||
});
|
||||
|
||||
it('should set inner text', () => {
|
||||
const p = document.createElement('p');
|
||||
p.innerText = 'Hello world!';
|
||||
p.classList.add('text');
|
||||
|
||||
expect(createParagraph('text')('Hello world!')).toEqual(p);
|
||||
});
|
||||
|
||||
it('should append to DOM', () => {
|
||||
append(document.body, createParagraph('text')('hello'));
|
||||
expect(document.querySelector('.text').innerText).toEqual('hello');
|
||||
});
|
||||
});
|
||||
});
|
144
static/modules/notifications/__tests__/index.tests.js
Normal file
144
static/modules/notifications/__tests__/index.tests.js
Normal file
@ -0,0 +1,144 @@
|
||||
require('../src/index');
|
||||
|
||||
describe('Notifications', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should display a console warning if no title or message is passed', () => {
|
||||
jest.spyOn(global.console, 'warn');
|
||||
window.createNotification()();
|
||||
expect(console.warn).toBeCalled();
|
||||
});
|
||||
|
||||
it('should render a default notification', () => {
|
||||
const notification = window.createNotification();
|
||||
|
||||
const title = 'I am a title';
|
||||
|
||||
// Should initially not contain any notifications
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(0);
|
||||
|
||||
// Create a notification instance with a title
|
||||
notification({ title });
|
||||
|
||||
// Should be one notification with the title passed in
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(1);
|
||||
expect(document.querySelector('.ncf-title').innerText).toEqual(title);
|
||||
|
||||
// Create a second instance so there should now be two instances
|
||||
notification({ title });
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should close on click if the option is enabled', () => {
|
||||
const notification = window.createNotification({
|
||||
closeOnClick: true
|
||||
});
|
||||
|
||||
// Create a notification with a generic body
|
||||
notification({ message: 'some text' });
|
||||
|
||||
// Should be one notification instance
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(1);
|
||||
|
||||
// Click the notification
|
||||
document.querySelector('.ncf').click();
|
||||
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not close on click if the option is disabled', () => {
|
||||
const notification = window.createNotification({
|
||||
closeOnClick: false
|
||||
});
|
||||
|
||||
// Create a notification with a generic body
|
||||
notification({ message: 'some text' });
|
||||
|
||||
// Should be one notification instance
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(1);
|
||||
|
||||
// Click the notification
|
||||
document.querySelector('.ncf').click();
|
||||
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should set position class if valid', () => {
|
||||
const validPositions = [
|
||||
'nfc-top-left',
|
||||
'nfc-top-right',
|
||||
'nfc-bottom-left',
|
||||
'nfc-bottom-right'
|
||||
];
|
||||
|
||||
validPositions.forEach(position => {
|
||||
const notification = window.createNotification({
|
||||
positionClass: position
|
||||
});
|
||||
|
||||
notification({ title: 'title here' });
|
||||
|
||||
const className = `.${position}`;
|
||||
|
||||
expect(document.querySelectorAll(className).length).toEqual(1);
|
||||
|
||||
const container = document.querySelector(className);
|
||||
expect(container.querySelectorAll('.ncf').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should revert to default to default position and warn if class is invalid', () => {
|
||||
const notification = window.createNotification({
|
||||
positionClass: 'invalid-name'
|
||||
});
|
||||
|
||||
jest.spyOn(global.console, 'warn');
|
||||
|
||||
notification({ message: 'test' });
|
||||
|
||||
expect(console.warn).toBeCalled();
|
||||
|
||||
expect(document.querySelectorAll('.nfc-top-right').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should allow a custom onclick callback', () => {
|
||||
let a = 'not clicked';
|
||||
|
||||
const notification = window.createNotification({
|
||||
onclick: () => {
|
||||
a = 'clicked';
|
||||
}
|
||||
});
|
||||
|
||||
notification({ message: 'click test' });
|
||||
|
||||
expect(a).toEqual('not clicked');
|
||||
|
||||
// Click the notification
|
||||
document.querySelector('.ncf').click();
|
||||
|
||||
expect(a).toEqual('clicked');
|
||||
});
|
||||
|
||||
it('should show for correct duration', () => {
|
||||
const notification = window.createNotification({
|
||||
showDuration: 500
|
||||
});
|
||||
|
||||
notification({ message: 'test' });
|
||||
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(1);
|
||||
|
||||
// Should exist after 400ms
|
||||
setTimeout(() => {
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(1);
|
||||
}, 400);
|
||||
|
||||
// Should delete after 500ms
|
||||
setTimeout(() => {
|
||||
expect(document.querySelectorAll('.ncf').length).toEqual(0);
|
||||
});
|
||||
}, 501);
|
||||
});
|
34
static/modules/notifications/demo/demo.js
Normal file
34
static/modules/notifications/demo/demo.js
Normal file
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
// Written using ES5 JS for browser support
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.querySelector('form');
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Form elements
|
||||
var title = form.querySelector('#title').value;
|
||||
var message = form.querySelector('#message').value;
|
||||
var position = form.querySelector('#position').value;
|
||||
var duration = form.querySelector('#duration').value;
|
||||
var theme = form.querySelector('#theme').value;
|
||||
var closeOnClick = form.querySelector('#close').checked;
|
||||
var displayClose = form.querySelector('#closeButton').checked;
|
||||
|
||||
if(!message) {
|
||||
message = 'You did not enter a message...';
|
||||
}
|
||||
|
||||
window.createNotification({
|
||||
closeOnClick: closeOnClick,
|
||||
displayCloseButton: displayClose,
|
||||
positionClass: position,
|
||||
showDuration: duration,
|
||||
theme: theme
|
||||
})({
|
||||
title: title,
|
||||
message: message
|
||||
});
|
||||
});
|
||||
});
|
101
static/modules/notifications/demo/index.html
Normal file
101
static/modules/notifications/demo/index.html
Normal file
@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
</hea>
|
||||
<meta charset="UTF-8">
|
||||
<title>Notifications</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
background-color: floralwhite;
|
||||
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
text-align: left;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 20px;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
background: #2f96b4;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
margin-bottom: 0;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Notifications styling -->
|
||||
<link rel="stylesheet" href="../dist/notifications.css" type="text/css">
|
||||
<head>
|
||||
<body>
|
||||
<h1>Notifications</h1>
|
||||
<form>
|
||||
<label for="title">Title (optional)</label>
|
||||
<br/>
|
||||
<input type="text" id="title" placeholder="Enter a title..." value="Notification">
|
||||
|
||||
<label for="message">Message</label>
|
||||
<br/>
|
||||
<input type="text" id="message" placeholder="Enter a message..." value="I am a default message">
|
||||
|
||||
<label for="position">Notification position:</label>
|
||||
<br/>
|
||||
<select id="position">
|
||||
<option value="nfc-top-right">Top Right</option>
|
||||
<option value="nfc-bottom-right">Bottom Right</option>
|
||||
<option value="nfc-top-left">Top Left</option>
|
||||
<option value="nfc-bottom-left">Bottom Left</option>
|
||||
</select>
|
||||
|
||||
<label for="duration">Show Duration (ms)</label>
|
||||
<br/>
|
||||
<input id="duration" type="number" value="3000"/>
|
||||
|
||||
<label for="theme">Theme</label>
|
||||
<br/>
|
||||
<select id="theme">
|
||||
<option value="success">Success</option>
|
||||
<option value="info">Information</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
|
||||
<label for="close">Close on click</label>
|
||||
<input id="close" type="checkbox" value="Close on click" checked>
|
||||
|
||||
<br/>
|
||||
|
||||
<label for="closeButton">Display a close button</label>
|
||||
<input id="closeButton" type="checkbox" value="Display close button">
|
||||
|
||||
<input type="submit" value="Display notification">
|
||||
</form>
|
||||
|
||||
<script src="../dist/notifications.js" type="text/javascript"></script>
|
||||
<script src="./demo.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
58
static/modules/notifications/package.json
Normal file
58
static/modules/notifications/package.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "styled-notifications",
|
||||
"version": "1.0.1",
|
||||
"description": "A simple JavaScript notifications library",
|
||||
"main": "dist/notifications.js",
|
||||
"scripts": {
|
||||
"start": "webpack --watch",
|
||||
"build": "webpack -p",
|
||||
"test": "jest",
|
||||
"prepare": "yarn run test && yarn run build"
|
||||
},
|
||||
"pre-commit": [
|
||||
"prepare"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/JamieLivingstone/Notifications.git"
|
||||
},
|
||||
"keywords": [
|
||||
"notification",
|
||||
"popup",
|
||||
"alert",
|
||||
"toast"
|
||||
],
|
||||
"author": "Jamie Livingstone",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Jamie Livingstone (https://github.com/JamieLivingstone)"
|
||||
},
|
||||
{
|
||||
"name": "cavebeavis (https://github.com/cavebeavis)"
|
||||
}
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/JamieLivingstone/Notifications/issues"
|
||||
},
|
||||
"homepage": "https://github.com/JamieLivingstone/Notifications#readme",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-jest": "^21.0.2",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-es2015-ie": "^6.7.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.6.1",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"jest": "^21.0.2",
|
||||
"node-sass": "^4.5.3",
|
||||
"pre-commit": "^1.2.2",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"webpack": "^3.5.6"
|
||||
}
|
||||
}
|
82
static/modules/notifications/readme.md
Normal file
82
static/modules/notifications/readme.md
Normal file
@ -0,0 +1,82 @@
|
||||
[](https://travis-ci.org/JamieLivingstone/Notifications)
|
||||
|
||||
# Notifications
|
||||
**Notifications** is a Javascript library for notifications heavily inspired by toastr but does not require any dependencies such as jQuery.
|
||||
|
||||
Works on browsers: IE9+, Safari, Chrome, FireFox, opera, edge
|
||||
|
||||
## npm Installation
|
||||
Do either
|
||||
```
|
||||
npm i styled-notifications
|
||||
```
|
||||
or add the following to your `package.json`:
|
||||
```
|
||||
"dependencies": {
|
||||
"styled-notifications": "^1.0.1"
|
||||
},
|
||||
```
|
||||
|
||||
## Installation
|
||||
Download files from the dist folder and then:
|
||||
1. Link to notifications.css `<link href="notifications.css" rel="stylesheet"/>`
|
||||
|
||||
2. Link to notifications.js `<script src="notifications.js"></script>`
|
||||
|
||||
## Usage
|
||||
### Custom options
|
||||
- closeOnClick <bool> - Close the notification dialog when a click is invoked.
|
||||
- displayCloseButton <bool> - Display a close button in the top right hand corner of the notification.
|
||||
- positionClass <string> - Set the position of the notification dialog. Accepted positions: ('nfc-top-right', 'nfc-bottom-right', 'nfc-bottom-left', 'nfc-top-left').
|
||||
- onClick <function(event)> - Call a callback function when a click is invoked on a notification.
|
||||
- showDuration <integer> - Milliseconds the notification should be visible (0 for a notification that will remain open until clicked)
|
||||
- theme <string> - Set the position of the notification dialog. Accepted positions: ('success', 'info', 'warning', 'error', 'A custom clasName').
|
||||
```
|
||||
const defaultOptions = {
|
||||
closeOnClick: true,
|
||||
displayCloseButton: false,
|
||||
positionClass: 'nfc-top-right',
|
||||
onclick: false,
|
||||
showDuration: 3500,
|
||||
theme: 'success'
|
||||
};
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
### Success notification
|
||||
```
|
||||
// Create a success notification instance
|
||||
const successNotification = window.createNotification({
|
||||
theme: 'success',
|
||||
showDuration: 5000
|
||||
});
|
||||
|
||||
// Invoke success notification
|
||||
successNotification({
|
||||
message: 'Simple success notification'
|
||||
});
|
||||
|
||||
// Use the same instance but pass a title
|
||||
successNotification({
|
||||
title: 'Working',
|
||||
message: 'Simple success notification'
|
||||
});
|
||||
```
|
||||
|
||||
### Information notification
|
||||
```
|
||||
// Only running it once? Invoke immediately like this
|
||||
window.createNotification({
|
||||
theme: 'success',
|
||||
showDuration: 5000
|
||||
})({
|
||||
message: 'I have some information for you...'
|
||||
});
|
||||
```
|
||||
|
||||
### Todo
|
||||
~~1. Add to NPM~~
|
||||
2. Improve documentation
|
||||
3. Further device testing
|
||||
4. Add contributor instructions
|
24
static/modules/notifications/src/helpers.js
Normal file
24
static/modules/notifications/src/helpers.js
Normal file
@ -0,0 +1,24 @@
|
||||
export const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs);
|
||||
|
||||
export const append = (el, ...children) => children.forEach(child => el.appendChild(child));
|
||||
|
||||
export const isString = input => typeof input === 'string';
|
||||
|
||||
export const createElement = (elementType, ...classNames) => {
|
||||
const element = document.createElement(elementType);
|
||||
|
||||
if(classNames.length) {
|
||||
classNames.forEach(currentClass => element.classList.add(currentClass));
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
const setInnerText = (element, text) => {
|
||||
element.innerText = text;
|
||||
return element;
|
||||
};
|
||||
|
||||
const createTextElement = (elementType, ...classNames) => partial(setInnerText, createElement(elementType, ...classNames));
|
||||
|
||||
export const createParagraph = (...classNames) => createTextElement('p', ...classNames);
|
148
static/modules/notifications/src/index.js
Normal file
148
static/modules/notifications/src/index.js
Normal file
@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
// Polyfills
|
||||
import './polyfills/classList';
|
||||
|
||||
import {
|
||||
append,
|
||||
createElement,
|
||||
createParagraph,
|
||||
isString
|
||||
} from './helpers';
|
||||
|
||||
(function Notifications(window) {
|
||||
// Default notification options
|
||||
const defaultOptions = {
|
||||
closeOnClick: true,
|
||||
displayCloseButton: false,
|
||||
positionClass: 'nfc-top-right',
|
||||
onclick: false,
|
||||
showDuration: 3500,
|
||||
theme: 'success'
|
||||
};
|
||||
|
||||
function configureOptions(options) {
|
||||
// Create a copy of options and merge with defaults
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
// Validate position class
|
||||
function validatePositionClass(className) {
|
||||
const validPositions = [
|
||||
'nfc-top-left',
|
||||
'nfc-top-right',
|
||||
'nfc-bottom-left',
|
||||
'nfc-bottom-right'
|
||||
];
|
||||
|
||||
return validPositions.indexOf(className) > -1;
|
||||
}
|
||||
|
||||
// Verify position, if invalid reset to default
|
||||
if (!validatePositionClass(options.positionClass)) {
|
||||
console.warn('An invalid notification position class has been specified.');
|
||||
options.positionClass = defaultOptions.positionClass;
|
||||
}
|
||||
|
||||
// Verify onClick is a function
|
||||
if (options.onclick && typeof options.onclick !== 'function') {
|
||||
console.warn('Notification on click must be a function.');
|
||||
options.onclick = defaultOptions.onclick;
|
||||
}
|
||||
|
||||
// Verify show duration
|
||||
if(typeof options.showDuration !== 'number') {
|
||||
options.showDuration = defaultOptions.showDuration;
|
||||
}
|
||||
|
||||
// Verify theme
|
||||
if(!isString(options.theme) || options.theme.length === 0) {
|
||||
console.warn('Notification theme must be a string with length');
|
||||
options.theme = defaultOptions.theme;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// Create a new notification instance
|
||||
function createNotification(options) {
|
||||
// Validate options and set defaults
|
||||
options = configureOptions(options);
|
||||
|
||||
// Return a notification function
|
||||
return function notification({ title, message } = {}) {
|
||||
const container = createNotificationContainer(options.positionClass);
|
||||
|
||||
if(!title && !message) {
|
||||
return console.warn('Notification must contain a title or a message!');
|
||||
}
|
||||
|
||||
// Create the notification wrapper
|
||||
const notificationEl = createElement('div', 'ncf', options.theme);
|
||||
|
||||
// Close on click
|
||||
if(options.closeOnClick === true) {
|
||||
notificationEl.addEventListener('click', () => container.removeChild(notificationEl));
|
||||
}
|
||||
|
||||
// Custom click callback
|
||||
if(options.onclick) {
|
||||
notificationEl.addEventListener('click', (e) => options.onclick(e));
|
||||
}
|
||||
|
||||
// Display close button
|
||||
if(options.displayCloseButton) {
|
||||
const closeButton = createElement('button');
|
||||
closeButton.innerText = 'X';
|
||||
|
||||
// Use the wrappers close on click to avoid useless event listeners
|
||||
if(options.closeOnClick === false){
|
||||
closeButton.addEventListener('click', () =>container.removeChild(notificationEl));
|
||||
}
|
||||
|
||||
append(notificationEl, closeButton);
|
||||
}
|
||||
|
||||
// Append title and message
|
||||
isString(title) && title.length && append(notificationEl, createParagraph('ncf-title')(title));
|
||||
isString(message) && message.length && append(notificationEl, createParagraph('nfc-message')(message));
|
||||
|
||||
// Append to container
|
||||
append(container, notificationEl);
|
||||
|
||||
// Remove element after duration
|
||||
if(options.showDuration && options.showDuration > 0) {
|
||||
const timeout = setTimeout(() => {
|
||||
container.removeChild(notificationEl);
|
||||
|
||||
// Remove container if empty
|
||||
if(container.querySelectorAll('.ncf').length === 0) {
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
}, options.showDuration);
|
||||
|
||||
// If close on click is enabled and the user clicks, cancel timeout
|
||||
if(options.closeOnClick || options.displayCloseButton) {
|
||||
notificationEl.addEventListener('click', () => clearTimeout(timeout));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createNotificationContainer(position) {
|
||||
let container = document.querySelector(`.${position}`);
|
||||
|
||||
if(!container) {
|
||||
container = createElement('div', 'ncf-container', position);
|
||||
append(document.body, container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Add Notifications to window to make globally accessible
|
||||
if (window.createNotification) {
|
||||
console.warn('Window already contains a create notification function. Have you included the script twice?');
|
||||
} else {
|
||||
window.createNotification = createNotification;
|
||||
}
|
||||
})(window);
|
68
static/modules/notifications/src/polyfills/classList.js
Normal file
68
static/modules/notifications/src/polyfills/classList.js
Normal file
@ -0,0 +1,68 @@
|
||||
(function () {
|
||||
if (typeof window.Element === 'undefined' || 'classList' in document.documentElement) return;
|
||||
|
||||
var prototype = Array.prototype,
|
||||
push = prototype.push,
|
||||
splice = prototype.splice,
|
||||
join = prototype.join;
|
||||
|
||||
function DOMTokenList(el) {
|
||||
this.el = el;
|
||||
// The className needs to be trimmed and split on whitespace
|
||||
// to retrieve a list of classes.
|
||||
var classes = el.className.replace(/^\s+|\s+$/g,'').split(/\s+/);
|
||||
for (var i = 0; i < classes.length; i++) {
|
||||
push.call(this, classes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
DOMTokenList.prototype = {
|
||||
add: function(token) {
|
||||
if(this.contains(token)) return;
|
||||
push.call(this, token);
|
||||
this.el.className = this.toString();
|
||||
},
|
||||
contains: function(token) {
|
||||
return this.el.className.indexOf(token) != -1;
|
||||
},
|
||||
item: function(index) {
|
||||
return this[index] || null;
|
||||
},
|
||||
remove: function(token) {
|
||||
if (!this.contains(token)) return;
|
||||
for (var i = 0; i < this.length; i++) {
|
||||
if (this[i] == token) break;
|
||||
}
|
||||
splice.call(this, i, 1);
|
||||
this.el.className = this.toString();
|
||||
},
|
||||
toString: function() {
|
||||
return join.call(this, ' ');
|
||||
},
|
||||
toggle: function(token) {
|
||||
if (!this.contains(token)) {
|
||||
this.add(token);
|
||||
} else {
|
||||
this.remove(token);
|
||||
}
|
||||
|
||||
return this.contains(token);
|
||||
}
|
||||
};
|
||||
|
||||
window.DOMTokenList = DOMTokenList;
|
||||
|
||||
function defineElementGetter (obj, prop, getter) {
|
||||
if (Object.defineProperty) {
|
||||
Object.defineProperty(obj, prop,{
|
||||
get : getter
|
||||
});
|
||||
} else {
|
||||
obj.__defineGetter__(prop, getter);
|
||||
}
|
||||
}
|
||||
|
||||
defineElementGetter(Element.prototype, 'classList', function () {
|
||||
return new DOMTokenList(this);
|
||||
});
|
||||
})();
|
134
static/modules/notifications/src/style.scss
Normal file
134
static/modules/notifications/src/style.scss
Normal file
@ -0,0 +1,134 @@
|
||||
// Base colors
|
||||
$success: #51A351;
|
||||
$info: #2F96B4;
|
||||
$warning: #f87400;
|
||||
$error: #BD362F;
|
||||
$grey: #999999;
|
||||
|
||||
.ncf-container {
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
|
||||
&.nfc-top-left {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
&.nfc-top-right {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
&.nfc-bottom-right {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
&.nfc-bottom-left {
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.ncf {
|
||||
background: #ffffff;
|
||||
transition: .3s ease;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
margin: 0 0 6px;
|
||||
padding: 30px;
|
||||
width: 300px;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
box-shadow: 0 0 12px $grey;
|
||||
color: #000000;
|
||||
opacity: 0.9;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=90);
|
||||
filter: alpha(opacity=90);
|
||||
background-position: 15px center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
|
||||
// Prevent annoying text selection
|
||||
-webkit-user-select: none; /* Chrome all / Safari all */
|
||||
-moz-user-select: none; /* Firefox all */
|
||||
-ms-user-select: none; /* IE 10+ */
|
||||
user-select: none; /* Likely future */
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 12px #000000;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ncf-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
margin-top: 0;
|
||||
margin-bottom: 6px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.nfc-message {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
// Themes
|
||||
.success {
|
||||
background: $success;
|
||||
color: #ffffff;
|
||||
padding: 15px 15px 15px 50px;
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.info {
|
||||
background: $info;
|
||||
color: #ffffff;
|
||||
padding: 15px 15px 15px 50px;
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: $warning;
|
||||
color: #ffffff;
|
||||
padding: 15px 15px 15px 50px;
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.error {
|
||||
background: $error;
|
||||
color: #ffffff;
|
||||
padding: 15px 15px 15px 50px;
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
right: -0.3em;
|
||||
top: -0.3em;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 0 1px 0 #ffffff;
|
||||
opacity: 0.8;
|
||||
line-height: 1;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
41
static/modules/notifications/webpack.config.js
Normal file
41
static/modules/notifications/webpack.config.js
Normal file
@ -0,0 +1,41 @@
|
||||
const webpack = require('webpack');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
|
||||
const extractSass = new ExtractTextPlugin({
|
||||
filename: 'notifications.css',
|
||||
disable: process.env.NODE_ENV === 'development'
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
entry: ['./src/index.js', './src/style.scss'],
|
||||
output: {
|
||||
path: __dirname + '/dist',
|
||||
filename: 'notifications.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['babel-preset-es2015', 'es2015-ie']
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: extractSass.extract({
|
||||
use: [{
|
||||
loader: 'css-loader'
|
||||
}, {
|
||||
loader: 'sass-loader'
|
||||
}],
|
||||
// use style-loader in development
|
||||
fallback: 'style-loader'
|
||||
})
|
||||
}
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
extractSass
|
||||
]
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user