chore: Modernize frontend

- Ditch gulp and move to rspack
 - Upgrade to node v20
This commit is contained in:
nikolark committed 2025-05-31 22:34:34 +02:00
1 parent eaaf7b38c4
commit b9aebcec13
15 files changed
+7108 -10374

No files matched your search

+1 -1
View File
@@ -7,4 +7,4 @@ venv
node_modules
bower_components
staticfiles
static/dist/
static/dist/
+43
View File
@@ -0,0 +1,43 @@
variables:
PIP_CACHE_DIR: "${CI_PROJECT_DIR}/.cache/pip"
default:
image: nikolaik/python-nodejs:python3.12-nodejs20
cache:
paths:
- .cache/pip
- .venv
django-checks:
stage: build
script:
- uv run manage.py migrate
- uv run manage.py check
tests:
stage: test
script:
- uv run bin/test
lint:
stage: test
script:
- uv run bin/pre-commit
deploy:
stage: deploy
before_script:
# Install ssh if not already
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
# Make SSH key available
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
# Add known host keys
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh gitdeploy@dreamcast.neuf.no "$(< bin/deploy)"
only:
- main
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -eu
cd static
npm i
npm run build
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
set -eu
source "$HOME/.local/bin/env"
export DJANGO_SETTINGS_MODULE=kassasite.settings
PROJECT_PATH=/var/www/neuf.no/kassa
PROCESS_NAME="kassa.neuf.no"
echo "Deploying kassa to ${PROJECT_PATH}"
cd "$PROJECT_PATH" || exit 1
git pull
# Frontend
bin/build-frontend
# Backend
uv sync
umask 022; uv run manage.py collectstatic --noinput -i node_modules -c # Collect static (including frontend)
uv run manage.py migrate # Run DB migrations
# Reload backend
echo "Reloading supervisor processes ${PROCESS_NAME} ${PROCESS_NAME}-celery"
sudo /usr/bin/supervisorctl pid "$PROCESS_NAME" | xargs kill -HUP
-4
View File
@@ -1,4 +0,0 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
-29
View File
@@ -1,29 +0,0 @@
{
"env": {
"browser": true
},
"extends": [
"airbnb-base",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"rules": {
"no-plusplus": "off",
"no-console": "off",
"no-underscore-dangle": "off",
"no-param-reassign": "off",
"no-restricted-syntax": "off",
},
"settings": {
"import/resolver": {
"node": {}, // https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-509384041
"webpack": {
"config": "webpack.config.js"
}
}
}
,
"plugins": ["prettier"],
"parser": "babel-eslint"
}
+30
View File
@@ -0,0 +1,30 @@
module.exports = {
env: {
browser: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
extends: ['plugin:prettier/recommended', 'plugin:import/errors', 'plugin:import/warnings'],
rules: {
'no-console': 'off',
'prefer-destructuring': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['**/*.test.js', '**/*.spec.js', '**/__tests__/**'],
},
],
'import/prefer-default-export': 'off',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
},
plugins: ['prettier'],
};
+1
View File
@@ -0,0 +1 @@
v20
+1 -3
View File
@@ -1,5 +1,3 @@
{
"singleQuote": true,
"printWidth": 120,
"arrowParens": "always"
"printWidth": 120
}
+165 -166
View File
@@ -1,15 +1,16 @@
import * as $ from 'jquery';
import moment from 'moment';
import nunjucks from 'nunjucks';
import debounce from 'lodash.debounce';
import sortBy from 'lodash.sortby';
import fromPairs from 'lodash.frompairs';
import $ from "jquery";
window.$ = window.jQuery = $; // Make sure jQuery is available globally
import debounce from "lodash.debounce";
import fromPairs from "lodash.frompairs";
import sortBy from "lodash.sortby";
import moment from "moment";
import nunjucks from "nunjucks";
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
import "bootstrap-sass/assets/javascripts/bootstrap/tooltip";
import "bootstrap-sass/assets/javascripts/bootstrap/popover";
import './templates';
import './index.scss';
import "./templates";
import "./index.scss";
/* Init and global vars */
let _dom;
@@ -20,29 +21,29 @@ let membershipOrder = null; /* selected order object */
const cardForm = {
fields: {
cardNumber: false,
phoneNumber: false
}
phoneNumber: false,
},
};
const STATE_ICON = {
success: 'ok',
warning: 'warning-sign',
error: 'remove'
success: "ok",
warning: "warning-sign",
error: "remove",
};
// TODO: replace with lodash template
const nunjucksEnv = new nunjucks.Environment();
const urls = {
userSearch: '/user-search/',
checkPhoneNumber: '/check-phone-number/',
checkCard: '/check-card/',
registerCardAndMembership: '/register-card-membership/',
renewMembership: '/renew-membership/'
userSearch: "/user-search/",
checkPhoneNumber: "/check-phone-number/",
checkCard: "/check-card/",
registerCardAndMembership: "/register-card-membership/",
renewMembership: "/renew-membership/",
};
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = $.trim(cookies[i]);
// Does this cookie string begin with the name we want?
@@ -56,12 +57,12 @@ function getCookie(name) {
}
function getParameterByName(name) {
return new URLSearchParams(window.location.search).get(name) || '';
return new URLSearchParams(window.location.search).get(name) || "";
}
function formatPhoneNumber(str) {
if (!str || str.length <= 1) {
return 'No phone number';
return "No phone number";
}
if (str.length === 11) {
return `${str.slice(0, 3)} ${str.slice(3, 6)} ${str.slice(6, 8)} ${str.slice(8)}`;
@@ -83,7 +84,7 @@ function sortSearchResults(results) {
}
function formatResults(results) {
return nunjucksEnv.render('search_results.html', { results });
return nunjucksEnv.render("search_results.html", { results });
}
function renderSelectedUser(userData) {
@@ -93,7 +94,7 @@ function renderSelectedUser(userData) {
} else {
context = { res: userData, checked: true, no_user_actions: true };
}
const selectedUserHtml = nunjucksEnv.render('search_result.html', context);
const selectedUserHtml = nunjucksEnv.render("search_result.html", context);
_dom.selectedEntityWrap.html(selectedUserHtml);
}
@@ -102,7 +103,7 @@ function renderSelectedOrder(order) {
return;
}
const context = { order, checked: true, no_user_actions: true };
const selectedOrderHtml = nunjucksEnv.render('selected_order.html', context);
const selectedOrderHtml = nunjucksEnv.render("selected_order.html", context);
_dom.selectedEntityWrap.html(selectedOrderHtml);
}
@@ -110,31 +111,29 @@ function setToast(message, messageType) {
const context = {
message,
message_type: messageType,
icon: STATE_ICON[messageType]
icon: STATE_ICON[messageType],
};
const html = nunjucksEnv.render('toast.html', context);
const html = nunjucksEnv.render("toast.html", context);
_dom.toastWrap.html(html);
_dom.toastWrap.addClass('visible');
_dom.toastWrap.addClass("visible");
setTimeout(() => {
_dom.toastWrap.removeClass('visible');
_dom.toastWrap.removeClass("visible");
}, 5000);
}
function updateMemberShipButton(user, order) {
const todayPlusThirtyDays = moment()
.add(30, 'days')
.format('YYYY-MM-DD');
// const todayPlusThirtyDays = moment().add(30, 'days').format('YYYY-MM-DD');
/* User with valid membership */
if (user && user.is_member) {
/* ...which is lifelong cannot renew */
if (user.last_membership.membership_type === 'lifelong') {
_dom.membershipSubmitButton.prop('disabled', true);
if (user.last_membership.membership_type === "lifelong") {
_dom.membershipSubmitButton.prop("disabled", true);
}
/* ...or expiring soon and the user has a card */
// else if (user.last_membership.end_date <= todayPlusThirtyDays && user.active_member_card) {
// FIXME: ponas workaround 2019-12-17, block below commented out
_dom.membershipSubmitButton.prop('disabled', false);
_dom.membershipSubmitButton.prop("disabled", false);
/* ...with a membership that expires soon can renew */
// else if (user.last_membership.end_date <= todayPlusThirtyDays) {
@@ -144,10 +143,10 @@ function updateMemberShipButton(user, order) {
/* User with expired membership can renew */
// else if (user && !user.is_member && user.active_member_card) {
else if (user && !user.is_member) {
_dom.membershipSubmitButton.prop('disabled', false);
_dom.membershipSubmitButton.prop("disabled", false);
} else if (order) {
// FIXME: ponas workaround 2019-12-17, block below commented out
_dom.membershipSubmitButton.prop('disabled', false);
_dom.membershipSubmitButton.prop("disabled", false);
/* Order */
/* If the membership has expired it can be renewed */
@@ -155,7 +154,7 @@ function updateMemberShipButton(user, order) {
// _dom.membershipSubmitButton.prop('disabled', false);
// }
} else {
_dom.membershipSubmitButton.prop('disabled', true);
_dom.membershipSubmitButton.prop("disabled", true);
}
}
@@ -163,14 +162,14 @@ function setSelectedUser(user, updateSearchResult) {
selectedUser = user;
updateMemberShipButton(user, null);
_dom.userIdField.val((user && user.id) || '').trigger('change');
_dom.userIdField.val((user && user.id) || "").trigger("change");
if (updateSearchResult) {
const staleSearchResult = _dom.results.find('.search-result').removeClass('selected');
staleSearchResult.find('input').prop('checked', false);
const staleSearchResult = _dom.results.find(".search-result").removeClass("selected");
staleSearchResult.find("input").prop("checked", false);
/* User is still in search result, mark selected */
if (user && users && users.find(({ id }) => id === user.id)) {
const searchResult = _dom.results.find(`[data-user-id="${user.id}"]`).toggleClass('selected');
searchResult.find('input').prop('checked', true);
const searchResult = _dom.results.find(`[data-user-id="${user.id}"]`).toggleClass("selected");
searchResult.find("input").prop("checked", true);
}
}
}
@@ -180,40 +179,40 @@ function setSelectedOrder(order) {
if (order) {
updateMemberShipButton(null, order);
}
_dom.orderUuidField.val((order && order.uuid) || '').trigger('change');
_dom.orderUuidField.val((order && order.uuid) || "").trigger("change");
}
function setFieldState(fieldElement, state, helpText) {
if (!helpText) {
helpText = '';
helpText = "";
}
const helpSpan = `<span class="help-block ">${helpText}</span>`;
const feedbackSpan = `<span class="glyphicon glyphicon-${STATE_ICON[state]} form-control-feedback" aria-hidden="true"></span>`;
const cssClass = `has-${state}`;
const formGroup = fieldElement.parent().parent();
formGroup.removeClass('has-success has-warning has-error has-feedback'); // reset
if (['success', 'warning', 'error'].indexOf(state) !== -1) {
formGroup.removeClass("has-success has-warning has-error has-feedback"); // reset
if (["success", "warning", "error"].indexOf(state) !== -1) {
formGroup.addClass(`${cssClass} has-feedback`);
/* Feedback icon */
const feedbackEl = formGroup.find('.form-control-feedback');
const feedbackEl = formGroup.find(".form-control-feedback");
if (!feedbackEl.length) {
// add
formGroup.append(feedbackSpan);
} else {
// Update
feedbackEl.removeClass('glyphicon-ok glypicon-warning-sign glyphicon-remove');
feedbackEl.removeClass("glyphicon-ok glypicon-warning-sign glyphicon-remove");
feedbackEl.addClass(`glyphicon-${STATE_ICON[state]}`);
}
/* Help text */
const helpTextEl = formGroup.find('.help-block');
const helpTextEl = formGroup.find(".help-block");
if (!helpTextEl.length) {
formGroup.append(helpSpan);
} else {
helpTextEl.text(helpText);
}
} else {
formGroup.find('.form-control-feedback').remove();
formGroup.find('.help-block').remove();
formGroup.find(".form-control-feedback").remove();
formGroup.find(".help-block").remove();
}
}
function resetCardForm(resetNative) {
@@ -221,18 +220,18 @@ function resetCardForm(resetNative) {
_dom.registerCardForm.get(0).reset();
}
/* Clear field states */
setFieldState(_dom.phoneNumberField, '');
setFieldState(_dom.phoneNumberField, "");
cardForm.fields.phoneNumber = false;
setFieldState(_dom.cardNumberField, '');
setFieldState(_dom.cardNumberField, "");
cardForm.fields.cardNumber = false;
selectedUser = null;
membershipOrder = null;
_dom.userIdField.val('').trigger('change');
_dom.membershipTrialCheckBox.prop('checked', false);
_dom.userIdField.val("").trigger("change");
_dom.membershipTrialCheckBox.prop("checked", false);
/* Disable submit buttons */
_dom.registerSubmitButton.prop('disabled', true);
_dom.membershipSubmitButton.prop('disabled', true);
_dom.registerSubmitButton.prop("disabled", true);
_dom.membershipSubmitButton.prop("disabled", true);
}
function resetSearchForm(resetNative) {
if (resetNative) {
@@ -240,7 +239,7 @@ function resetSearchForm(resetNative) {
}
/* Clear search results */
users = null;
_dom.results.html('');
_dom.results.html("");
}
function cardFormIsValid() {
@@ -249,41 +248,41 @@ function cardFormIsValid() {
function updateSubmitButton() {
if (cardFormIsValid()) {
_dom.registerSubmitButton.prop('disabled', false);
_dom.registerSubmitButton.prop("disabled", false);
return;
}
_dom.registerSubmitButton.prop('disabled', true);
_dom.registerSubmitButton.prop("disabled", true);
}
function validatePhoneNumber(val) {
if (val.length === 0) {
cardForm.fields.phoneNumber = false;
updateSubmitButton();
return 'Phone number should not be empty.';
return "Phone number should not be empty.";
}
cardForm.fields.phoneNumber = true;
updateSubmitButton();
return '';
return "";
}
function validateCardNumber(val) {
if (val.length === 0) {
cardForm.fields.cardNumber = false;
updateSubmitButton();
return 'Card number should not be empty.';
return "Card number should not be empty.";
}
if (val.length > 0 && val[0] !== '1') {
if (val.length > 0 && val[0] !== "1") {
cardForm.fields.cardNumber = false;
updateSubmitButton();
return 'Card number should start with 1.';
return "Card number should start with 1.";
}
if (val.length !== 9) {
cardForm.fields.cardNumber = false;
updateSubmitButton();
return 'Card number should be 9 digits long.';
return "Card number should be 9 digits long.";
}
cardForm.fields.cardNumber = true;
updateSubmitButton();
return '';
return "";
}
function getFormData(formElement) {
@@ -307,7 +306,7 @@ function checkPhoneNumber() {
if (data.error) {
setSelectedUser(null, true);
setSelectedOrder(null);
setFieldState(_dom.phoneNumberField, 'error', data.error);
setFieldState(_dom.phoneNumberField, "error", data.error);
cardForm.fields.phoneNumber = false;
updateSubmitButton();
return;
@@ -315,12 +314,12 @@ function checkPhoneNumber() {
const { user } = data;
const { order } = data;
let message = '';
let message = "";
/* Has valid card membership */
if (!user && order && order.member_card && order.product.is_valid) {
message = `Phone number is already tied to card ${order.member_card} and is valid until ${order.product.end_date}.`;
setFieldState(_dom.phoneNumberField, 'error', message);
setFieldState(_dom.phoneNumberField, "error", message);
cardForm.fields.phoneNumber = false;
setSelectedOrder(order);
updateSubmitButton();
@@ -329,8 +328,8 @@ function checkPhoneNumber() {
if (user) {
/* Existing user */
message = 'Phone number belongs to existing user.';
setFieldState(_dom.phoneNumberField, 'success', message);
message = "Phone number belongs to existing user.";
setFieldState(_dom.phoneNumberField, "success", message);
setSelectedUser(user, true);
setSelectedOrder(null);
cardForm.fields.phoneNumber = true;
@@ -339,9 +338,9 @@ function checkPhoneNumber() {
setSelectedOrder(order);
/* Valid membership, no card associated */
if (order.product.is_valid) {
message = 'Phone number has a valid membership.';
message = "Phone number has a valid membership.";
} else {
message = 'Phone number has an expired membership.';
message = "Phone number has an expired membership.";
}
} else {
/* Valid phone number associated user or order */
@@ -349,61 +348,61 @@ function checkPhoneNumber() {
setSelectedUser(null);
}
setFieldState(_dom.phoneNumberField, 'success', message);
setFieldState(_dom.phoneNumberField, "success", message);
cardForm.fields.phoneNumber = true;
updateSubmitButton();
});
}
/* Add filter |phoneNumber */
nunjucksEnv.addFilter('phoneNumber', formatPhoneNumber);
nunjucksEnv.addFilter("phoneNumber", formatPhoneNumber);
/* On document ready */
$(document).ready(function() {
$(function () {
_dom = {
results: $('.results-wrap'),
query: $('#id_query'),
phoneNumberLabel: $('[for=id_phone_number]'),
phoneNumberField: $('#id_phone_number'),
cardNumberField: $('#id_card_number'),
registerCardForm: $('.register-card-form'),
registerSubmitButton: $('#register-submit-btn'),
usernameField: $('#id_username'),
userIdField: $('#id_user_id'),
orderUuidField: $('#id_order_uuid'),
selectedEntityWrap: $('.register-card-form--selected-entity-wrap'),
toastWrap: $('.toast-wrap'),
registerResetButton: $('.register-reset-btn'),
searchResetButton: $('.search-reset-btn'),
membershipSubmitButton: $('#membership-submit-btn'),
membershipTrialCheckBox: $('#id_membership_trial')
results: $(".results-wrap"),
query: $("#id_query"),
phoneNumberLabel: $("[for=id_phone_number]"),
phoneNumberField: $("#id_phone_number"),
cardNumberField: $("#id_card_number"),
registerCardForm: $(".register-card-form"),
registerSubmitButton: $("#register-submit-btn"),
usernameField: $("#id_username"),
userIdField: $("#id_user_id"),
orderUuidField: $("#id_order_uuid"),
selectedEntityWrap: $(".register-card-form--selected-entity-wrap"),
toastWrap: $(".toast-wrap"),
registerResetButton: $(".register-reset-btn"),
searchResetButton: $(".search-reset-btn"),
membershipSubmitButton: $("#membership-submit-btn"),
membershipTrialCheckBox: $("#id_membership_trial"),
};
const lazyCheckPhoneNumber = debounce(checkPhoneNumber, 250);
_dom.phoneNumberField.on('input', (e) => {
_dom.phoneNumberField.on("input", (e) => {
const val = e.target.value.trim();
const validationMsg = validatePhoneNumber(val);
if (validationMsg !== '') {
if (validationMsg !== "") {
setSelectedUser(null, true);
setFieldState(_dom.phoneNumberField, 'error', validationMsg);
setFieldState(_dom.phoneNumberField, "error", validationMsg);
return;
}
lazyCheckPhoneNumber();
});
/* Card number as you type */
_dom.cardNumberField.on('input', (e) => {
_dom.cardNumberField.on("input", (e) => {
/* Card number should exist in the database and not tied to existing user */
const val = e.target.value.trim();
const validationMsg = validateCardNumber(val);
if (validationMsg !== '') {
setFieldState(_dom.cardNumberField, 'error', validationMsg);
if (validationMsg !== "") {
setFieldState(_dom.cardNumberField, "error", validationMsg);
return;
}
$.getJSON(urls.checkCard, { card_number: val }, (data) => {
if (data.error) {
updateSubmitButton();
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', data.error);
setFieldState(_dom.cardNumberField, "error", data.error);
return;
}
@@ -414,18 +413,18 @@ $(document).ready(function() {
if (!card) {
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', 'Cannot find card number in database.');
setFieldState(_dom.cardNumberField, "error", "Cannot find card number in database.");
} else if (card && card.registered !== null && user === null && order && !order.product.is_valid) {
/* An already registered card (with no user) can repurchase membership if expired */
setSelectedOrder(order);
cardForm.fields.cardNumber = true;
setFieldState(
_dom.cardNumberField,
'success',
"success",
`Card number is in use (not activated) and belongs to ${formatPhoneNumber(order.phone_number)}.`
);
} else if (card && card.registered !== null) {
let ownerString = '???';
let ownerString = "???";
if (user) {
ownerString = `existing user: ${user.first_name} ${user.last_name} (${formatPhoneNumber(user.phone_number)})`;
} else {
@@ -433,26 +432,26 @@ $(document).ready(function() {
ownerString = `phone number: ${formatPhoneNumber(order.phone_number)}`;
}
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', `Card number is in use and belongs to ${ownerString}.`);
setFieldState(_dom.cardNumberField, "error", `Card number is in use and belongs to ${ownerString}.`);
} else if (card && user === null) {
cardForm.fields.cardNumber = true;
setFieldState(_dom.cardNumberField, 'success');
setFieldState(_dom.cardNumberField, "success");
} else {
cardForm.fields.cardNumber = true;
setFieldState(_dom.cardNumberField, '');
setFieldState(_dom.cardNumberField, "");
}
updateSubmitButton();
}).fail(() => {
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', 'Cannot find card number in database.');
setFieldState(_dom.cardNumberField, "error", "Cannot find card number in database.");
});
});
/* User search as you type */
_dom.query.on('input', () => {
_dom.query.on("input", () => {
const val = _dom.query.val().trim();
if (val.length <= 2) {
_dom.results.html('');
_dom.results.html("");
return;
}
if (searching) {
@@ -465,38 +464,38 @@ $(document).ready(function() {
users = data.results;
} else if (data.error) {
_dom.results.html(data.error);
} else if (val !== '') {
} else if (val !== "") {
_dom.results.html(`Found no user matching: '${val}'`);
} else {
_dom.results.html('');
_dom.results.html("");
}
});
});
/* On search result click */
_dom.results.on('click', '.search-result input', () => {
_dom.results.on("click", ".search-result input", () => {
const label = $(this).parent();
let number = label.attr('data-phone-number');
const userId = label.attr('data-user-id');
let number = label.attr("data-phone-number");
const userId = label.attr("data-user-id");
/* Update selected state */
$('.search-result').removeClass('selected');
label.toggleClass('selected');
$(".search-result").removeClass("selected");
label.toggleClass("selected");
setSelectedUser(users.find(({ id }) => id === userId));
/* Update phone number field */
if (number === '-') {
number = '';
if (number === "-") {
number = "";
}
_dom.phoneNumberField.val(number).trigger('input');
_dom.phoneNumberField.val(number).trigger("input");
});
/* On submit button click */
_dom.registerSubmitButton.on('click', (e) => {
_dom.registerSubmitButton.on("click", (e) => {
e.preventDefault();
if (!cardFormIsValid()) {
setToast('Either phone number or card number is not valid', 'error');
setToast("Either phone number or card number is not valid", "error");
return;
}
const payload = getFormData(_dom.registerCardForm);
@@ -506,66 +505,66 @@ $(document).ready(function() {
// action: 'new_card_membership', 'update_card', 'add_or_renew', 'sms_card_notify'
if (selectedUser === null && membershipOrder === null) {
// Unknown phone / card, new membership
payload.action = 'new_card_membership';
payload.action = "new_card_membership";
} else if (selectedUser === null && membershipOrder !== null) {
if (membershipOrder.member_card === null && membershipOrder.product.is_valid) {
// Associate member card with a valid membership order
payload.action = 'sms_card_notify';
payload.action = "sms_card_notify";
payload.order_uuid = membershipOrder.uuid;
} else {
// New card for expired order -- treat as new card membership
payload.action = 'new_card_membership';
payload.action = "new_card_membership";
}
} else if (selectedUser !== null && selectedUser.is_member) {
// Add member card to a user
payload.action = 'update_card';
payload.action = "update_card";
} else {
// New order for user
payload.action = 'add_or_renew'; // Note: could also update card number
payload.action = "add_or_renew"; // Note: could also update card number
}
$.ajax(urls.registerCardAndMembership, {
data: JSON.stringify(payload),
contentType: 'application/json',
dataType: 'json',
type: 'post',
headers: { 'X-CSRFToken': getCookie('csrftoken') }
contentType: "application/json",
dataType: "json",
type: "post",
headers: { "X-CSRFToken": getCookie("csrftoken") },
})
.done(() => {
let successMessage = `New card registered ${payload.card_number}`;
if (payload.action === 'add_or_renew') {
if (payload.action === "add_or_renew") {
const fullName = `${selectedUser.first_name} ${selectedUser.last_name}`;
successMessage = `New membership and card number ${payload.card_number} registered to ${fullName} `;
} else if (payload.action === 'new_card_membership') {
} else if (payload.action === "new_card_membership") {
successMessage = `New membership and card registered to ${formatPhoneNumber(
payload.phone_number
)}. Activation SMS sent `;
} else if (payload.action === 'sms_card_notify') {
} else if (payload.action === "sms_card_notify") {
successMessage = `New card registered to ${formatPhoneNumber(
payload.phone_number
)}. Membership already paid ${membershipOrder.product.start_date}. Activation SMS sent `;
}
setToast(`${successMessage} :-)`, 'success');
setToast(`${successMessage} :-)`, "success");
resetCardForm(true);
})
.fail((data) => {
console.log('failed', data);
console.log("failed", data);
let errorText = data.responseText;
if (data.responseJSON) {
if (data.responseJSON.non_field_errors) {
errorText = data.responseJSON.non_field_errors.join('<br>');
errorText = data.responseJSON.non_field_errors.join("<br>");
} else {
errorText = 'Not sure why. :-(';
errorText = "Not sure why. :-(";
}
}
setToast(`Failed! ${errorText}`, 'error');
setToast(`Failed! ${errorText}`, "error");
});
});
/* Membership renewal button */
_dom.membershipSubmitButton.on('click', (e) => {
_dom.membershipSubmitButton.on("click", (e) => {
e.preventDefault();
if (!selectedUser && !membershipOrder) {
setToast('Computer says no.', 'error');
setToast("Computer says no.", "error");
return;
}
@@ -579,62 +578,62 @@ $(document).ready(function() {
$.ajax(urls.renewMembership, {
data: JSON.stringify(payload),
contentType: 'application/json',
dataType: 'json',
type: 'post',
headers: { 'X-CSRFToken': getCookie('csrftoken') }
contentType: "application/json",
dataType: "json",
type: "post",
headers: { "X-CSRFToken": getCookie("csrftoken") },
})
.done((data) => {
let name = 'unknown';
let name = "unknown";
if (data.user) {
name = `${selectedUser.first_name} ${selectedUser.last_name}`;
} else {
name = `phone number ${data.phone_number}`;
}
const successMessage = `Membership renewed for ${name} :-)`;
setToast(successMessage, 'success');
setToast(successMessage, "success");
resetCardForm(true);
})
.fail((data) => {
console.log('failed', data);
console.log("failed", data);
let errorText = data.responseText;
if (data.responseJSON) {
if (data.responseJSON.non_field_errors) {
errorText = data.responseJSON.non_field_errors.join('<br>');
errorText = data.responseJSON.non_field_errors.join("<br>");
} else {
errorText = 'Not sure why. :-(';
errorText = "Not sure why. :-(";
}
}
setToast(`Failed! ${errorText}`, 'error');
setToast(`Failed! ${errorText}`, "error");
});
});
/* On user id change */
_dom.userIdField.on('change', function() {
_dom.userIdField.on("change", function () {
/* Render selected user in search form */
renderSelectedUser(selectedUser);
});
/* On order uuid change */
_dom.orderUuidField.on('change', function() {
_dom.orderUuidField.on("change", function () {
/* Render selected order */
renderSelectedOrder(membershipOrder);
});
/* Reset buttons */
_dom.registerResetButton.on('click', function() {
_dom.registerResetButton.on("click", function () {
resetCardForm();
});
_dom.searchResetButton.on('click', function() {
_dom.searchResetButton.on("click", function () {
resetSearchForm();
});
/* Ninja add some icons to bootstrap form */
const iconMobile =
'<span class="input-group-addon"><span class="glyphicon glyphicon-phone glyphicon-phone-large"></span></span>';
_dom.phoneNumberField.parent().addClass('input-group');
_dom.phoneNumberField.parent().addClass("input-group");
_dom.phoneNumberField.before(iconMobile);
/* Render initial placeholder user */
const selectedUserHtml = nunjucksEnv.render('search_result.html', { placeholder: true });
const selectedUserHtml = nunjucksEnv.render("search_result.html", { placeholder: true });
_dom.selectedEntityWrap.html(selectedUserHtml);
/* Initial focus */
@@ -643,18 +642,18 @@ $(document).ready(function() {
}
/* Load query params */
if (getParameterByName('user_id')) {
_dom.userIdField.val(getParameterByName('user_id')).trigger('change');
if (getParameterByName("user_id")) {
_dom.userIdField.val(getParameterByName("user_id")).trigger("change");
}
if (getParameterByName('phone_number')) {
_dom.phoneNumberField.val(getParameterByName('phone_number')).trigger('input');
if (getParameterByName("phone_number")) {
_dom.phoneNumberField.val(getParameterByName("phone_number")).trigger("input");
}
if (getParameterByName('card_number')) {
_dom.cardNumberField.val(getParameterByName('card_number')).trigger('input');
if (getParameterByName("card_number")) {
_dom.cardNumberField.val(getParameterByName("card_number")).trigger("input");
}
$('[data-toggle="popover"]').popover({
html: true,
trigger: 'click'
trigger: "click",
});
});
-134
View File
@@ -1,134 +0,0 @@
/* global require */
'use strict';
var gulp = require('gulp');
// load plugins
var $ = require('gulp-load-plugins')();
var bowerFiles = require('main-bower-files');
var del = require('del');
var browserSync = require('browser-sync').create();
var reload = browserSync.reload;
gulp.task('styles', function () {
gulp.src('app/styles/*.scss')
.pipe($.sass({
includePaths: ['bower_components/foundation/scss'],
sourceComments: 'map' // TODO does this work?
}).on('error', $.sass.logError))
.pipe($.autoprefixer('last 1 version'))
.pipe(gulp.dest('dist/styles'))
.pipe(browserSync.stream())
.pipe($.size());
});
gulp.task('diststyles', function () {
return gulp.src('app/styles/*.scss')
.pipe($.sass({
includePaths: ['bower_components/foundation/scss']
}))
.pipe($.autoprefixer('last 1 version'))
.pipe($.csso())
.pipe(gulp.dest('dist/styles'))
.pipe(browserSync.stream())
.pipe($.size());
});
gulp.task('vendorscripts', function () {
return gulp.src(bowerFiles())
.pipe($.filter('**/*.js'))
//.pipe($.uglify())
.pipe($.concat('vendor.js'))
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('vendorstyles', function () {
// TODO: copy styles to scss-files instead?
return gulp.src(bowerFiles())
.pipe($.filter('**/*.css'))
.pipe($.concat('vendor.css'))
//.pipe($.csso())
.pipe(gulp.dest('dist/styles'))
.pipe($.size());
});
gulp.task('scripts', function () {
return gulp.src('app/scripts/**/*.js')
.pipe($.jshint())
.pipe($.jshint.reporter(require('jshint-stylish')))
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('distscripts', function () {
return gulp.src('app/scripts/**/*.js')
.pipe($.jshint())
.pipe($.jshint.reporter(require('jshint-stylish')))
//.pipe($.uglify())
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('images', function () {
return gulp.src('app/images/**/*')
.pipe($.imagemin({
optimizationLevel: 3,
progressive: true,
interlaced: true
}))
.pipe(gulp.dest('dist/images'))
.pipe($.size());
});
gulp.task('fonts', function () {
return gulp.src(bowerFiles().concat(['app/fonts/**/*.*']))
.pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
.pipe($.flatten())
.pipe(gulp.dest('dist/fonts'))
.pipe($.size());
});
gulp.task('extras', function () {
return gulp.src(['app/*.*', '!app/*.php'], { dot: true })
.pipe(gulp.dest('dist'));
});
gulp.task('templates', function () {
return gulp.src('app/templates/*.html')
.pipe($.nunjucks())
.pipe($.concat('templates.js'))
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('clean', function (cb) {
del(['dist'], cb);
});
gulp.task('build', ['vendorscripts', 'vendorstyles', 'distscripts', 'diststyles', 'images', 'fonts', 'extras', 'templates']);
gulp.task('default', ['clean'], function () {
gulp.start('build');
});
gulp.task('serve', ['styles'], function () {
});
gulp.task('watch', ['serve'], function () {
browserSync.init({
proxy: 'localhost:8001'
});
// watch for changes
gulp.watch([
'../apps/kassa/templates/**/*.html',
'dist/scripts/**/*.js',
'dist/images/**/*'
]).on('change', reload);
gulp.watch('app/styles/**/*.scss', ['styles']);
gulp.watch('app/scripts/**/*.js', ['scripts']);
gulp.watch('app/templates/**/*.html', ['templates']);
gulp.watch('app/images/**/*', ['images']);
gulp.watch('bower.json', ['vendorscripts']);
});
+6733 -9908
View File
File diff suppressed because it is too large. Load diff
+19 -33
View File
@@ -2,51 +2,37 @@
"name": "kassa",
"private": true,
"scripts": {
"build": "webpack -p --env production --display-error-details --mode production",
"build": "rspack build --mode production",
"build:templates": "echo '/* eslint-disable */' > app/templates.js && nunjucks-precompile app/templates >> app/templates.js",
"start": "webpack-dev-server --hot --env development --mode development",
"start": "rspack serve --mode development",
"format": "eslint --fix \"app/**/*.js\"",
"lint": "eslint --quiet \"app/**/*.js\""
},
"dependencies": {
"bootstrap-sass": "^3.4.1",
"jquery": "^3.4.1",
"bootstrap-sass": "^3.4.3",
"jquery": "^3.7.1",
"list.js": "^1.5.0",
"lodash.debounce": "^4.0.8",
"lodash.frompairs": "^4.0.1",
"lodash.sortby": "^4.7.0",
"moment": "^2.24.0",
"nunjucks": "^3.2.0"
"moment": "^2.30.1",
"nunjucks": "^3.2.4"
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.7",
"@babel/preset-react": "^7.7.4",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.4.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.7.0",
"eslint-import-resolver-babel-module": "^5.1.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-prettier": "^3.1.2",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.0",
"style-loader": "^1.1.2",
"url-loader": "^3.0.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.1"
"@rspack/cli": "^1.3.13",
"@rspack/core": "^1.3.13",
"css-loader": "^7.1.2",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"file-loader": "^6.2.0",
"prettier": "^2.6.2",
"sass-embedded": "^1.89.1",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0"
},
"engines": {
"node": "12"
"node": "20"
}
}
+85
View File
@@ -0,0 +1,85 @@
const path = require('path');
const { defineConfig } = require('@rspack/cli');
const { rspack } = require('@rspack/core');
module.exports = defineConfig({
entry: './app/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.jsx', '.js', '.json', '.scss', '.css'],
modules: ['node_modules'],
},
plugins: [
new rspack.CopyRspackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'app', 'images'),
to: path.resolve(__dirname, 'dist', 'images'),
},
],
}),
new rspack.CssExtractRspackPlugin(),
new rspack.ProvidePlugin({
jQuery: 'jquery',
}),
],
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: require.resolve('file-loader'),
options: {},
},
],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset',
},
{
test: /\.css$/,
use: [rspack.CssExtractRspackPlugin.loader, require.resolve('css-loader')],
},
{
test: /\.scss$/,
use: [
rspack.CssExtractRspackPlugin.loader,
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: false,
sourceMap: true,
},
},
{
loader: require.resolve('sass-loader'),
options: {
api: 'modern-compiler',
implementation: require.resolve('sass-embedded'),
sassOptions: {
silenceDeprecations: ['global-builtin', 'legacy-js-api', 'mixed-decls', 'color-functions', 'import'],
},
},
},
],
},
],
},
// FIXME(nikolark): This workflow does not work
devServer: {
port: 8001,
proxy: [
{
context: '/api',
target: 'http://localhost:8000',
},
],
},
});
-96
View File
@@ -1,96 +0,0 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const baseStyleLoader = (env) => (env === 'production' ? MiniCssExtractPlugin.loader : require.resolve('style-loader'));
module.exports = (env, argv) => {
return {
entry: './app/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
extensions: ['.jsx', '.js', '.json', '.scss', '.css'],
modules: ['node_modules']
},
plugins: [
new MiniCssExtractPlugin(),
env === 'production' ? new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) : null,
env !== 'production' ? new HtmlWebpackPlugin({ template: './app/index.html' }) : null,
new webpack.ProvidePlugin({
jQuery: 'jquery'
})
].filter(Boolean),
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: require.resolve('file-loader'),
options: {}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: require.resolve('file-loader')
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: require.resolve('babel-loader')
},
{
test: /\.css$/,
loaders: [baseStyleLoader(env), require.resolve('css-loader')]
},
{
test: /\.scss$/,
loaders: [
baseStyleLoader(env),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: false,
sourceMap: true
}
},
{
loader: require.resolve('sass-loader'),
options: {
sassOptions: {
precision: 8
}
}
}
]
}
]
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
historyApiFallback: true,
overlay: {
errors: true,
warnings: false
},
port: 8001,
publicPath: '/',
proxy: {
'/api': 'http://localhost:8000'
// TODO: get urls
},
stats: {
assets: false,
modules: false,
chunks: false
}
},
devtool: env === 'production' ? 'source-map' : 'cheap-module-eval-source-map'
};
};