Vue

by joplomacedo

HTML

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.1/css/all.min.css">
<div id="app">
  <div class="sms_edit__box">
    <div class="sms_edit__info_row">
      <div class="sms_edit__character_count_info">
        <span>Character count</span> <span>{{ smsBodyCharCount }}/{{ CHARS_COUNT_PER_SEGMENT * smsSegmentCount}}</span>
      </div>

      <div v-if="smsSegmentCount > 1" class="sms_edit__segment_count_info">
        <i class="sms_edit__info_warning_icon fas fa-exclamation-triangle"></i>
        Counts as {{smsSegmentCount}} texts
      </div>

      <div v-if="smsContainsCustomVars">
        <i class="sms_edit__info_warning_icon fas fa-exclamation-triangle"></i>
        You are using dynamic variables in your text
        (
        <span>
          <span class="sms_edit__info_custom_var" v-for="(smsCustomVar, i) in smsCustomVars" :key="i">#{{smsCustomVar}}</span><span v-if=""
        </span>
        ). Because the text's length will depend on the actual value of that variable (likely different for each appointment) Reoodle cannot determine the actual size of the text and the corresponding text count. The presented text count is just an estimation (each dynamic variable is counted as 10 characters)
      </div>
    </div>

    <div class="sms_edit__form_row">
      <textarea class="sms_edit__sms_body_field field field--texarea" v-model="smsBody"></textarea>
    </div>
  </div>
</div>

CSS

*,
*:before,
*:after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 0;
  font: inherit;
  color: inherit;
}

html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  line-height: 1.5;
}


ol,
ul {
  list-style: none;
}

a,
button,
input[type="button"],
input[type="submit"] {
  cursor: pointer;
}

a {
  text-decoration: none;
}

textarea {
  display: block;
  width: 100%;
}


body {
  padding: 2em;
}

.field {
  border: 1px solid #ccc;
  border-radius: 2px;
  padding: 1em;
}

.field--texarea {
  min-height: 17em;
}

.sms_edit__info_warning_icon {
    color: #e8b030;
}

.sms_edit__info_custom_var {
  display: inline-block;
  line-height: 1.2;
  padding: .1em .3em;
  border-radius: 2px;
  background-color: #eee;
  margin-right: .3em;
}

.sms_edit__info_custom_var:last-child {
  margin-right: 0;
}

.sms_edit__info_custom_var:after {
    content: ",";
}

.sms_edit__info_custom_var:last-child:after {
    content: "";
}

Vue

//utils
function removeWhitespace(str) {
	return str.trim().replace(/^ +| +$/gm, "");
}

function removeWhitespaceAndNewLines(str) {
	return str.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, " ");
}

function isAlphaNumeric(str) {
	var code, i, len;

	for (i = 0, len = str.length; i < len; i++) {
		code = str.charCodeAt(i);
		if (
			!(code > 47 && code < 58) && // numeric (0-9)
			!(code > 64 && code < 91) && // upper alpha (A-Z)
			!(code > 96 && code < 123)
		) {
			// lower alpha (a-z)
			return false;
		}
	}
	return true;
}

new Vue({
	el: "#app",

	data: {
		REOODLE_VARS: ["dt", "datetime", "date", "time"],
        CHARS_COUNT_PER_CUSTOM_VAR_ESTIMATATION: 10,
        CHARS_COUNT_PER_SEGMENT: 160,

        smsBody: removeWhitespace(`
            Hello Rita,

            This is just a reminder of your hairstyling appointment at 15:00 at 31/12/2020.

            If you cannot attend please call us today at 888-808-505.

            Thank you,
            Sandra's Hair Magic
        `),

		
	},

	computed: {
		cleanSmsBody() {
			return removeWhitespace(this.smsBody);
		},

		smsBodyCharCount() {
			const smsCustomVarsKeysCharCount = this.smsCustomVars.reduce(
				(acc, smsCustomVars) => {
					//1 is to count with the '#'
					return acc + 1 + smsCustomVars.length;
				},
				0
			);

			const smsCustomVarsCharCountEstimation =
				this.CHARS_COUNT_PER_CUSTOM_VAR_ESTIMATATION *
				this.smsCustomVars.length;

			return (
				this.cleanSmsBody.length -
				smsCustomVarsKeysCharCount +
				smsCustomVarsCharCountEstimation
			);
		},

		smsSegmentCount() {
			return ( Math.ceil(this.smsBodyCharCount / this.CHARS_COUNT_PER_SEGMENT) || 1  );
		},

		smsCustomVars() {
			let scannableSmsBody = removeWhitespaceAndNewLines(this.smsBody);

			const customVars = [];
			let currHashStartIdx = null;

			scannableSmsBody.split("").forEach((char, idx) => {
				if (currHashStartIdx !== null) {
					if (!isAlphaNumeric(char)) {
						let foundVar =...