From ab67f6726ff4c45f9fc3eeff31a1a7bc9265c255 Mon Sep 17 00:00:00 2001 From: Immanuel Onyeka <immanuel@onyeka.ca> Date: Sun, 18 Jul 2021 23:06:12 -0400 Subject: [PATCH] Add more ES6 code --- javascript-vue/js/app.js | 1 + javascript-vue/js/bootstrap.js | 28 +++ javascript-vue/js/icons/eye-fill.vue | 8 + javascript-vue/js/icons/instagram.vue | 67 ++++++ javascript-vue/js/icons/loading.vue | 3 + javascript-vue/js/icons/plus-fill.vue | 5 + javascript-vue/js/icons/plus.vue | 6 + javascript-vue/js/icons/youtube.vue | 39 ++++ javascript-vue/js/main.js | 116 +++++++++++ javascript-vue/js/panel/admin.vue | 0 javascript-vue/js/panel/credits.vue | 188 +++++++++++++++++ javascript-vue/js/panel/edit-cards.vue | 82 ++++++++ javascript-vue/js/panel/order-item.vue | 71 +++++++ javascript-vue/js/panel/orders.vue | 100 +++++++++ {vue => javascript-vue/js/panel}/panel.vue | 3 +- javascript-vue/js/panel/payment-card.vue | 42 ++++ javascript-vue/js/panel/payment-slider.vue | 17 ++ javascript-vue/js/panel/saved-cards.vue | 28 +++ javascript-vue/js/panel/service-pane.vue | 28 +++ javascript-vue/js/panel/services.vue | 196 ++++++++++++++++++ javascript-vue/js/panel/settings.vue | 119 +++++++++++ javascript-vue/js/panel/sidebar.vue | 46 ++++ javascript-vue/js/panel/summary.vue | 3 + javascript-vue/js/panel/support.vue | 82 ++++++++ .../js/panel/transaction-endpoint.vue | 33 +++ .../js/register-area/register-area.vue | 168 +++++++++++++++ 26 files changed, 1478 insertions(+), 1 deletion(-) create mode 100644 javascript-vue/js/app.js create mode 100644 javascript-vue/js/bootstrap.js create mode 100644 javascript-vue/js/icons/eye-fill.vue create mode 100644 javascript-vue/js/icons/instagram.vue create mode 100644 javascript-vue/js/icons/loading.vue create mode 100644 javascript-vue/js/icons/plus-fill.vue create mode 100644 javascript-vue/js/icons/plus.vue create mode 100644 javascript-vue/js/icons/youtube.vue create mode 100644 javascript-vue/js/main.js create mode 100644 javascript-vue/js/panel/admin.vue create mode 100644 javascript-vue/js/panel/credits.vue create mode 100644 javascript-vue/js/panel/edit-cards.vue create mode 100644 javascript-vue/js/panel/order-item.vue create mode 100644 javascript-vue/js/panel/orders.vue rename {vue => javascript-vue/js/panel}/panel.vue (95%) create mode 100644 javascript-vue/js/panel/payment-card.vue create mode 100644 javascript-vue/js/panel/payment-slider.vue create mode 100644 javascript-vue/js/panel/saved-cards.vue create mode 100644 javascript-vue/js/panel/service-pane.vue create mode 100644 javascript-vue/js/panel/services.vue create mode 100644 javascript-vue/js/panel/settings.vue create mode 100644 javascript-vue/js/panel/sidebar.vue create mode 100644 javascript-vue/js/panel/summary.vue create mode 100644 javascript-vue/js/panel/support.vue create mode 100644 javascript-vue/js/panel/transaction-endpoint.vue create mode 100644 javascript-vue/js/register-area/register-area.vue diff --git a/javascript-vue/js/app.js b/javascript-vue/js/app.js new file mode 100644 index 0000000..40c55f6 --- /dev/null +++ b/javascript-vue/js/app.js @@ -0,0 +1 @@ +require('./bootstrap'); diff --git a/javascript-vue/js/bootstrap.js b/javascript-vue/js/bootstrap.js new file mode 100644 index 0000000..6922577 --- /dev/null +++ b/javascript-vue/js/bootstrap.js @@ -0,0 +1,28 @@ +window._ = require('lodash'); + +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +window.axios = require('axios'); + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo'; + +// window.Pusher = require('pusher-js'); + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: process.env.MIX_PUSHER_APP_KEY, +// cluster: process.env.MIX_PUSHER_APP_CLUSTER, +// forceTLS: true +// }); diff --git a/javascript-vue/js/icons/eye-fill.vue b/javascript-vue/js/icons/eye-fill.vue new file mode 100644 index 0000000..726eed7 --- /dev/null +++ b/javascript-vue/js/icons/eye-fill.vue @@ -0,0 +1,8 @@ +<template> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" +fill="currentColor" class="bi bi-eye-fill" viewBox="0 0 16 16"> + <path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/> + <path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 + 1 0 0-7 3.5 3.5 0 0 0 0 7z"/> +</svg> +</template> diff --git a/javascript-vue/js/icons/instagram.vue b/javascript-vue/js/icons/instagram.vue new file mode 100644 index 0000000..cd4ec42 --- /dev/null +++ b/javascript-vue/js/icons/instagram.vue @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 551.034 551.034" style="enable-background:new 0 0 551.034 551.034;" xml:space="preserve"> +<g> + + <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="275.517" y1="4.57" x2="275.517" y2="549.72" gradientTransform="matrix(1 0 0 -1 0 554)"> + <stop offset="0" style="stop-color:#E09B3D"/> + <stop offset="0.3" style="stop-color:#C74C4D"/> + <stop offset="0.6" style="stop-color:#C21975"/> + <stop offset="1" style="stop-color:#7024C4"/> + </linearGradient> + <path style="fill:url(#SVGID_1_);" d="M386.878,0H164.156C73.64,0,0,73.64,0,164.156v222.722 + c0,90.516,73.64,164.156,164.156,164.156h222.722c90.516,0,164.156-73.64,164.156-164.156V164.156 + C551.033,73.64,477.393,0,386.878,0z M495.6,386.878c0,60.045-48.677,108.722-108.722,108.722H164.156 + c-60.045,0-108.722-48.677-108.722-108.722V164.156c0-60.046,48.677-108.722,108.722-108.722h222.722 + c60.045,0,108.722,48.676,108.722,108.722L495.6,386.878L495.6,386.878z"/> + + <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="275.517" y1="4.57" x2="275.517" y2="549.72" gradientTransform="matrix(1 0 0 -1 0 554)"> + <stop offset="0" style="stop-color:#E09B3D"/> + <stop offset="0.3" style="stop-color:#C74C4D"/> + <stop offset="0.6" style="stop-color:#C21975"/> + <stop offset="1" style="stop-color:#7024C4"/> + </linearGradient> + <path style="fill:url(#SVGID_2_);" d="M275.517,133C196.933,133,133,196.933,133,275.516s63.933,142.517,142.517,142.517 + S418.034,354.1,418.034,275.516S354.101,133,275.517,133z M275.517,362.6c-48.095,0-87.083-38.988-87.083-87.083 + s38.989-87.083,87.083-87.083c48.095,0,87.083,38.988,87.083,87.083C362.6,323.611,323.611,362.6,275.517,362.6z"/> + + <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="418.31" y1="4.57" x2="418.31" y2="549.72" gradientTransform="matrix(1 0 0 -1 0 554)"> + <stop offset="0" style="stop-color:#E09B3D"/> + <stop offset="0.3" style="stop-color:#C74C4D"/> + <stop offset="0.6" style="stop-color:#C21975"/> + <stop offset="1" style="stop-color:#7024C4"/> + </linearGradient> + <circle style="fill:url(#SVGID_3_);" cx="418.31" cy="134.07" r="34.15"/> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +</svg> diff --git a/javascript-vue/js/icons/loading.vue b/javascript-vue/js/icons/loading.vue new file mode 100644 index 0000000..10fd5d3 --- /dev/null +++ b/javascript-vue/js/icons/loading.vue @@ -0,0 +1,3 @@ +<template> +<svg class="loading-icon" data-set="loaders" data-loading="lazy" width="30px" height="30px" data-src="https://s2.svgbox.net/loaders.svg?ic=oval" data-icon="oval" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" color="" data-attributes-set="viewBox,xmlns,stroke,color" data-rendered="true"><g transform="translate(1 1)" stroke-width="2" fill="none" fill-rule="evenodd"><circle stroke-opacity=".5" cx="18" cy="18" r="18"></circle><path d="M36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"></animateTransform></path></g></svg> +</template> diff --git a/javascript-vue/js/icons/plus-fill.vue b/javascript-vue/js/icons/plus-fill.vue new file mode 100644 index 0000000..4a86388 --- /dev/null +++ b/javascript-vue/js/icons/plus-fill.vue @@ -0,0 +1,5 @@ +<template> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-square-fill" viewBox="0 0 16 16"> + <path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z"/> +</svg> +</template> diff --git a/javascript-vue/js/icons/plus.vue b/javascript-vue/js/icons/plus.vue new file mode 100644 index 0000000..b917ed6 --- /dev/null +++ b/javascript-vue/js/icons/plus.vue @@ -0,0 +1,6 @@ +<template> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-square" viewBox="0 0 16 16"> + <path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/> + <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/> +</svg> +</template> diff --git a/javascript-vue/js/icons/youtube.vue b/javascript-vue/js/icons/youtube.vue new file mode 100644 index 0000000..1bf5548 --- /dev/null +++ b/javascript-vue/js/icons/youtube.vue @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 461.001 461.001" style="enable-background:new 0 0 461.001 461.001;" xml:space="preserve"> +<path style="fill:#F61C0D;" d="M365.257,67.393H95.744C42.866,67.393,0,110.259,0,163.137v134.728 + c0,52.878,42.866,95.744,95.744,95.744h269.513c52.878,0,95.744-42.866,95.744-95.744V163.137 + C461.001,110.259,418.135,67.393,365.257,67.393z M300.506,237.056l-126.06,60.123c-3.359,1.602-7.239-0.847-7.239-4.568V168.607 + c0-3.774,3.982-6.22,7.348-4.514l126.06,63.881C304.363,229.873,304.298,235.248,300.506,237.056z"/> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +</svg> diff --git a/javascript-vue/js/main.js b/javascript-vue/js/main.js new file mode 100644 index 0000000..4ba0b70 --- /dev/null +++ b/javascript-vue/js/main.js @@ -0,0 +1,116 @@ +import RegisterArea from './register-area/register-area.vue' +import Panel from './panel/panel.vue' +import '../scss/main.scss' +import { createApp } from 'vue' +importAll(require.context('../images', false, /\.(png|jpe?g|svg)$/)) + +let heroText = document.querySelectorAll(".landing-hero h2,.landing-hero p") +let registerToggles = document.querySelectorAll(".register-btn, .register-area\ + .cancel-button, .services-cards button") +let token = getCookie('XSRF-TOKEN') + +function importAll(r) { + return r.keys().map(r) +} + +function getCookie(name) { + var re = new RegExp(name + "=([^;]+)") + var value = re.exec(document.cookie) + + return (value != null) ? unescape(value[1]) : null +} + +function getToken() { + return fetch("/sanctum/csrf-cookie", { + method: 'GET' + }).then( () => { + token = getCookie('XSRF-TOKEN') + return token + }) +} + +function login(event) { + getToken().then(fetch("/login", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': token}, + body: JSON.stringify({"email": + document.getElementById("login_email").value, + "password": document.getElementById("login_password").value}), + }).then(response => { + if (response.ok) { + window.location.assign("/panel") + } else { + document.querySelector("#login_form .error").innerText = + "Invalid credentials." + } + })) + event.preventDefault() + // event.stopPropogation() +} + + +//Attempt to resend the verification link +function resendLink(event) { + fetch("/resend-verification", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': token}, + }).then(response => { + if (response.ok) { + event.target.parentNode.getElementsByTagName('h3')[0].innerText = + "The link has been resent." + } else { + event.target.parentNode.getElementsByTagName('h3')[0].innerText = + `${response.status} : ${response.statusText}` + }}) + event.preventDefault(); +} + +function toggleNav() { + heroText.forEach(item => { + item.classList.toggle("hidden") + }) + document.querySelector("nav form.login").classList.toggle("active") + this.classList.toggle("toggled") +} + + +if (window.location.pathname == '/') { + document.getElementById('nav_toggle').addEventListener('click', toggleNav) + document.querySelector('#login_form button').addEventListener('click', login) + const app = createApp(RegisterArea).mount('#app') + // app.token = token + if (!token) {app.token = getToken()} + + //Triggers for registration menu + for (let i = 0; i < registerToggles.length; i++) { + registerToggles[i].addEventListener("click", function() { + document.querySelector(".register-area").classList.add("active") + app.active = 'register' + }) + } + document.getElementById("forgot-password-btn").onclick = event => { + document.querySelector(".register-area").classList.add("active") + app.active = 'forgot' + event.preventDefault() + } + + //FAQ collapsibles + let cols = document.getElementsByClassName("collapsible"); + + for (let i = 0; i < cols.length; i++) { + cols[i].addEventListener("click", function() { + this.classList.toggle("active"); + }); + } +} else if (window.location.pathname == '/verify-email') { + document.getElementById('resend_verification').addEventListener("click", resendLink) +} else if (window.location.pathname == '/panel') { + const app = createApp(Panel).mount('#panel') + getToken().then(()=> {app.token = token}) + window.onhashchange = ()=>{app.active = location.hash} +} + diff --git a/javascript-vue/js/panel/admin.vue b/javascript-vue/js/panel/admin.vue new file mode 100644 index 0000000..e69de29 diff --git a/javascript-vue/js/panel/credits.vue b/javascript-vue/js/panel/credits.vue new file mode 100644 index 0000000..90e1fc0 --- /dev/null +++ b/javascript-vue/js/panel/credits.vue @@ -0,0 +1,188 @@ +<template> +<section class="select-credits"> +<div class="credits-pane"><h2>10 Credits</h2> +<h3>$10.99</h3><div><span>Qty</span><input min="0" max="1000" v-model="packs.credits10" type="number"></div> +</div> + +<div class="credits-pane"><div><h2>50 Credits</h2><span>+5 Free Credits</span></div> +<h3>$54.99 </h3><div><span>Qty</span><input min="0" max="1000" v-model="packs.credits50" type="number"></div> +</div> + +<div class="credits-pane"><div><h2>100 Credits</h2><span>+10 Free Credits</span></div> +<h3>$109.99</h3> <div><span>Qty</span><input min="0" max="1000" v-model="packs.credits100" type="number"></div> +</div> + +<div class="credits-pane"><div><h2>1000 Credits</h2><span>+150 Free Credits</span></div> +<h3>$1010.00</h3> <div><span>Qty</span><input min="0" max="1000" v-model="packs.credits1000" type="number"></div> +</div> + <h3>Total: ${{total.toLocaleString('en')}}</h3> + <div id="credits-errors"></div> +</section> + +<section id="payment-section"> + <h4>Select a payment method</h4> + + <div class="sliding-menu"> + <a @click="method = 'payeer'" :class="{selected: method == 'payeer'}">Payeer</a> + <a @click="method = 'pm'" :class="{selected: method == 'pm'}">Perfect Money</a> + <div :class="{right: (method == 'pm')}" class="menu-slider"><div></div></div> + </div> + + <div v-if="method == 'payeer'" class="payment-window"> + <img src="../../images/payeer.png" alt=""> + <p>Payeer allows you to pay securely by transfering your choice of cryptocurrency + to a temporary address. + </p> + </div> + + <div v-if="method == 'pm'" class="payment-window"> + <img src="../../images/perfect_money.svg" alt=""> + <p>Pay by transfering USD from your Perfect Money wallet.</p> + </div> + + <div id="agreement-check"> + <input v-model="agreed" type="checkbox"><label>I have read and agree to the <a + href="/terms-and-policy">Terms and Policy</a> and will not pursue a dispute or chargeback.</label> + <div id="payment-error"></div> + </div> +</section> + +<section class="credits-confirm"> + <button @click="pay" :disabled="!ready" + class="brand-btn">Buy<loading v-if="loading"></loading></button> +</section> + +</template> + +<script> +import Loading from '../icons/loading.vue' + +function total() { + return this.packs.credits10*10.99 + this.packs.credits50*54.99 + + this.packs.credits100*109.99 + this.packs.credits1000*1010 +} + +//Gets the secret key specific to chosen payment amount and user +function getSecret() { + document.getElementById('credits-errors').textContent = '' + this.loading = true + return fetch('/panel/secret', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'packs': this.packs}) + }).then((response) => { + if (response.ok) { + return response.text() + } else { + document.getElementById('credits-errors').textContent = + `${response.status}: ${response.statusText}` + } + }).then(secret => { + this.loading = false + return secret + }) +} + +function pay() { + if (this.method == 'payeer') { + this.payPayeer() + } else if (this.method == 'pm') { + this.payPm() + } +} + +function makeInput(name, value) { + let input = document.createElement('input') + input.type = 'hidden' + input.name = name + input.value = value + return input +} + +function payPayeer() { + fetch('/panel/payeer', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'packs': this.packs}) + }).then(response => {return response.json()}).then(data => { + let form = document.createElement('form') + document.body.appendChild(form) + form.method = 'POST' + form.action = 'https://payeer.com/merchant/' + form.appendChild(this.makeInput('m_shop', data.shop)) + form.appendChild(this.makeInput('m_orderid', data.transaction)) + form.appendChild(this.makeInput('m_amount', data.amount)) + form.appendChild(this.makeInput('m_curr', 'USD')) + form.appendChild(this.makeInput('m_desc', data.description)) + form.appendChild(this.makeInput('m_sign', data.signature)) + form.appendChild(this.makeInput('m_params', data.params)) + form.appendChild(this.makeInput('m_cipher_method', 'AES-256-CBC')) + form.submit() + /* console.log(data.signature) */ + }) + +} + +function payPm() { + fetch('/panel/pm', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'packs': this.packs}) + }).then(response => {return response.json()}).then(data => { + let form = document.createElement('form') + document.body.appendChild(form) + form.method = 'POST' + form.action = 'https://perfectmoney.is/api/step1.asp' + form.appendChild(this.makeInput('PAYEE_ACCOUNT', data.account)) + form.appendChild(this.makeInput('PAYEE_NAME', 'Trendplays Network')) + form.appendChild(this.makeInput('PAYMENT_AMOUNT', data.amount)) + form.appendChild(this.makeInput('PAYMENT_UNITS', 'USD')) + form.appendChild(this.makeInput('PAYMENT_ID', data.transaction)) + form.appendChild(this.makeInput('STATUS_URL', + 'https://trendplays.com/hooks/pm-transaction')) + form.appendChild(this.makeInput('PAYMENT_URL', + 'https://trendplays.com/panel/transaction-complete')) + form.appendChild(this.makeInput('PAYMENT_URL_METHOD', 'POST')) + form.appendChild(this.makeInput('NOPAYMENT_URL', + 'https://trendplays.com/panel/transaction-failed')) + form.appendChild(this.makeInput('NOPAYMENT_URL_METHOD', 'GET')) + form.appendChild(this.makeInput('SUGGESTED_MEMO', data.description)) + form.appendChild(this.makeInput('SUGGESTED_MEMO_NOCHANGE', true)) + form.submit() + }) +} + +function ready() { + if (this.packs.credis10 < 0) { + return false + } else if (this.packs.credis50 < 0) { + return false + } else if (this.packs.credis100 < 0) { + return false + } else if (this.packs.credis1000 < 0) { + return false + } + + return this.total > 0 && !this.loading && this.agreed +} + +export default { + components:{Loading}, + data() { + return {packs: {credits10: 0, credits50: 0, + credits100: 0, credits1000: 0}, loading: false, method: 'payeer', + agreed: false + } + }, + computed: {total, ready}, + methods: {getSecret, pay, payPm, payPayeer, makeInput}, + props: ['preferred', 'token'], + emits: ['purchaseComplete'], +} +</script> diff --git a/javascript-vue/js/panel/edit-cards.vue b/javascript-vue/js/panel/edit-cards.vue new file mode 100644 index 0000000..31bf0ac --- /dev/null +++ b/javascript-vue/js/panel/edit-cards.vue @@ -0,0 +1,82 @@ +<template> +<div v-if="cards && cards.length > 0"> +<div class="saved-cards-heading"> +<h5>Card</h5> <h5>Default</h5> <h5>Delete</h5> +</div> +<div v-for="card in cards" :key="card.id" class="saved-card"> + <span>{{card.card.brand[0].toUpperCase() + card.card.brand.substring(1)}} + (••••{{card.card.last4}})</span> + <span><input :checked="card.id == preferred" :value="card.id" name="selected-card" type="radio" + @change="change(card.id)"></span> + <span><img @click="remove(card.id)" src="../../images/close-icon-black.svg"/></span> +</div> +<p id="billing-error"></p> +</div> +</template> + +<script> +function get() { + fetch('/panel/cards', { + method: 'GET', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token} + }).then((response) => {response.json().then(data => { + this.cards = data.data + })}) +} + +function change(card) { + fetch('/panel/change-card', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'card': card}) + }).then((response) => { + if (response.ok) { + response.json().then((data) => { + this.cards = data.data + }) + } else { + console.log('bad') + document.getElementById("billing-error").textContent = + `${response.status}: ${response.statusText}` + } + }) +} + +function remove(card) { + if (card.length == 1) { + return + } + + fetch('/panel/delete-card', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'card': card}) + }).then((response) => { + if (response.ok) { + response.json().then((data) => { + this.cards = data.data + }) + } else { + document.getElementById("billing-error").textContent = + `${response.status}: ${response.statusText}` + } + }) +} + +export default { + data() { + return {cards: null} + }, + methods: {get, change, remove}, + created() { + this.get() + }, + props: ['token', 'preferred'], +} +</script> diff --git a/javascript-vue/js/panel/order-item.vue b/javascript-vue/js/panel/order-item.vue new file mode 100644 index 0000000..171dad3 --- /dev/null +++ b/javascript-vue/js/panel/order-item.vue @@ -0,0 +1,71 @@ +<template> +<div id="overlay" v-if="selected"> + + <img @click="$emit('close')" class="cancel icon" + src="../../images/cancel-icon2.svg" alt=""/> + <div class="overlay-item"> + <img v-if="selected.service.site == 'youtube'" class="icon" + src="../../images/youtube-icon.svg" alt=""/> + <img v-if="selected.service.site == 'instagram'" class="icon" + src="../../images/instagram-icon.svg" alt=""/> + <img v-if="selected.service.site == 'twitter'" class="icon" + src="../../images/twitter.svg" alt=""/> + <img v-if="selected.service.site == 'tiktok'" class="icon" + src="../../images/tik-tok.svg" alt=""/> + <h3>{{selected.service.name}}</h3> + + <div class="details"> + <ul> + <li><b>Status:</b> <span>{{selected.status.charAt(0).toUpperCase() + + selected.status.slice(1)}}</span></li> + <li><b>Quantity:</b> <span>{{selected.quantity}}</span></li> + <li><b>Remaining:</b> <span>{{selected.remaining}}</span></li> + <li><b>URL:</b> <span>{{selected.url}}</span></li> + </ul> + </div> + + <div v-if="selected.status == 'processing' || selected.status == + 'error'" class="change-url"> + <h4>URL</h4> + <div><input v-model="url" type="url" id="url"></div> + <button @click="saveURL" :disabled="loading">Save + <loading-icon v-if="loading"></loading-icon></button> + <p id="overlay-error">{{errorMessage}}</p> + </div> + + </div> + + </div> +</template> + +<script> +import LoadingIcon from '../icons/loading.vue' + +function saveURL() { + fetch('/panel/save-url', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'url': this.url, 'order': this.selected.id}) + }).then(response => { + if (response.ok) { + this.errorMessage = 'Saved' + this.$emit('changeUrl', this.url) + } else { + this.errorMessage = 'An error occured' + } + }) + +} + +export default { + data() { + return {loading: false, errorMessage: '', url: this.selected.url} + }, + components: {LoadingIcon}, + methods: {saveURL}, + props: ['selected', 'token'], + emits: ['changeUrl', 'close'] +} +</script> diff --git a/javascript-vue/js/panel/orders.vue b/javascript-vue/js/panel/orders.vue new file mode 100644 index 0000000..c743c09 --- /dev/null +++ b/javascript-vue/js/panel/orders.vue @@ -0,0 +1,100 @@ +<template> +<div> + <section class="pending-pane"> + <div class="actions"><a class="new-order" href="#new-order">New</a><a + class="new-order" href="#credits">Add Credits</a></div> + <h4>Pending Orders</h4> + <ul> + <template v-bind:key='order.id' v-for="order in orders"> + <div class="pending-item" v-if="order.status == 'pending'"> + <div class="pending-heading"> + <li @click="togglePending($event)">{{order.service.name}} ({{order.updated_at}})</li> + <img class="chevron" src="../../images/chevron-down.svg" alt=""> + </div> + <div class="pending-content"> + <p>ID: {{order.id}}<br>URL: {{order.url}}<br>Quantity: + {{order.quantity}}<br>Note: {{order.note}}</p> + </div> + </div> + </template> + </ul> + </section> + <div class="info-grey"><p>Orders are typically completed within 1-5 + days.</p><div></div></div> + <section class="history-pane"> + <h4>Order History</h4> + + <div class="table-scroller"> + <table> + <thead><th>Date</th><th>ID</th><th>Name</th><th>Status</th> + <th>Quantity</th></thead> + <tbody> + <tr v-bind:key='order.id' v-for='order in + orders.slice(historyPage*10-10, historyPage*10)'> + <td>{{order.updated_at}}</td> + <td>{{order.id}}</td> + <td>{{order.service.name}}</td> + <td :class="order.status" + class="status"><span>{{order.status.charAt(0).toUpperCase() + + order.status.slice(1)}}</span></td> + <td>{{order.quantity}}</td> + <td> <eye @click="select(order)"></eye> </td> + </tr> + </tbody> + </table> + </div> + + <img @click="moveHistory(false)" class="nav-btn left" + src="../../images/arrow-left-circle-fill.svg" alt=""/> + <p class="nav-legend">{{historyPage}}/{{Math.ceil(orders.length/10)}}</p> + <img @click="moveHistory(true)" class="nav-btn right" + src="../../images/arrow-right-circle-fill.svg" alt=""/> + + </section> + + <order-item v-if="selected" @close="close" :selected="selected" + :token="token" @change-url="(url) => selected.url = url"></order-item> + +</div> +</template> + +<script> +import Eye from '../icons/eye-fill.vue' +import OrderItem from './order-item.vue' + +function togglePending(event) { + event.target.parentNode.parentNode.classList.toggle('selected') +} + +function moveHistory(forward) { + if (forward) { + this.historyPage += 1 + } else { + this.historyPage -= 1 + } + if (this.historyPage < 1) { + this.historyPage = 1 + return + } else if (this.historyPage > this.orders.length/10+1) { + this.historyPage -= 1 + return + } +} + +function close() { + this.selected = null +} + +function select(order) { + this.selected = order +} + +export default { + components: {Eye, OrderItem}, + data() {return {historyPage: 1, selected: null}}, + methods: { + togglePending, moveHistory, close, select + }, + props: ['orders', 'token'], +} +</script> diff --git a/vue/panel.vue b/javascript-vue/js/panel/panel.vue similarity index 95% rename from vue/panel.vue rename to javascript-vue/js/panel/panel.vue index 08e866a..3dcde3c 100644 --- a/vue/panel.vue +++ b/javascript-vue/js/panel/panel.vue @@ -9,7 +9,8 @@ {{(user.credits/100).toLocaleString('en')}}</p></section> <section class="alerts-pane"><h4>News and Announcements</h4> <p>We've just launched. Thanks for joining us! Some features are still - being tested.</p> + being tested. If you experience a delay in credits being added to your + account, please wait 24 hours before contacting support@trendplays.com.</p> </section> <section class="recent-pane"><h4>Recent Activity</h4> <table> diff --git a/javascript-vue/js/panel/payment-card.vue b/javascript-vue/js/panel/payment-card.vue new file mode 100644 index 0000000..7bdb90a --- /dev/null +++ b/javascript-vue/js/panel/payment-card.vue @@ -0,0 +1,42 @@ +<template> + <form id="payment-form" action=""> + <label for="name">Name on Card</label> + <input @input="$emit('updateBillingName', $event)" id="billing-name" type="name"> + <div id="card-element"></div> + <div id="card-errors"></div> + <div id=save-card> + <input name="save-card" type="checkbox" checked="true"> + <label for="">Save Card</label> + </div> + </form> +</template> + +<script> +function mountPaymentForm() { + let card = this.stripe.elements().create('card') + card.mount('#card-element') + this.$emit('setCard', card) + + card.on('change', function(event) { + let displayError = document.getElementById('card-errors'); + if (event.error) { + displayError.textContent = event.error.message; + displayError.textContent = ''; + this.$emit('cardValid', true) + } else { + displayError.textContent = ''; + this.$emit('cardValid', true) + } + }.bind(this)); +} + +export default { + data() { + return {billingName: null} + }, + methods: {mountPaymentForm}, + props: ['stripe'], + mounted: mountPaymentForm, + emits: ['updateBillingName', 'cardValid', 'setCard'] +} +</script> diff --git a/javascript-vue/js/panel/payment-slider.vue b/javascript-vue/js/panel/payment-slider.vue new file mode 100644 index 0000000..cba5483 --- /dev/null +++ b/javascript-vue/js/panel/payment-slider.vue @@ -0,0 +1,17 @@ +<template> + <div class="services-menu"> + <a href="#new-order" :class="{selected: page == 'new-order'}">Services</a> + <a href="#credits" :class="{selected: page == 'credits'}">Credits</a> + <div :class="page" class="menu-slider"><div></div></div> + </div> +</template> + +<script> +export default { + data() { + return { + items: null + } + } +} +</script> diff --git a/javascript-vue/js/panel/saved-cards.vue b/javascript-vue/js/panel/saved-cards.vue new file mode 100644 index 0000000..3a6a6dc --- /dev/null +++ b/javascript-vue/js/panel/saved-cards.vue @@ -0,0 +1,28 @@ +<template> +<div v-if="cards"> +<div v-for="(card, index) in cards" :key="card.id" class="saved-card"> + <span>{{card.card.brand[0].toUpperCase() + card.card.brand.substring(1)}} + (••••{{card.card.last4}})</span> + <input :checked="index === 0 || card.id == preferred" :value="card.id" name="selected-card" type="radio" + @change="$emit('update:pickedCard', card.id)"> +</div> +</div> +</template> + +<script> + export default { + data() { + return {} + }, + mounted() { + if (this.cards && this.cards.length > 0) { + this.$emit('update:pickedCard', this.cards[0].id) + } + }, + unmounted() { + this.$emit('update:pickedCard', null) + }, + props: ['token', 'cards', 'preferred'], + emits: ['update:pickedCard'] + } +</script> diff --git a/javascript-vue/js/panel/service-pane.vue b/javascript-vue/js/panel/service-pane.vue new file mode 100644 index 0000000..c96b1ab --- /dev/null +++ b/javascript-vue/js/panel/service-pane.vue @@ -0,0 +1,28 @@ +<template> +<section class="services-pane youtube" > + <h4>{{site.charAt(0).toUpperCase() + site.slice(1)}}</h4> + <ul :key="service.id" v-for="service in filter"> + <li v-if="service.available"><span>{{service.name}}</span><span>{{(service.price/100).toLocaleString('en')}}</span><span>{{service.minimum.toLocaleString('en')}}</span><span>{{service.maximum.toLocaleString('en')}}</span> +<svg @click="$emit('select', service)" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-square-fill" viewBox="0 0 16 16"> + <path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z"/> +</svg> + </li> + </ul> + </section> +</template> + +<script> +function filter() { + if (!this.services || !this.site) {return} + + return this.services.filter((s) => { + return s.site == this.site + }) +} + +export default { + props: ['services', 'site'], + emits: ['select'], + computed: {filter} +} +</script> diff --git a/javascript-vue/js/panel/services.vue b/javascript-vue/js/panel/services.vue new file mode 100644 index 0000000..899db6d --- /dev/null +++ b/javascript-vue/js/panel/services.vue @@ -0,0 +1,196 @@ +<template> +<div> + <div class="sliding-menu"> + <a href="#new-order" :class="{selected: page == 'new-order'}">Services</a> + <a href="#credits" :class="{selected: page == 'credits'}">Credits</a> + <div :class="page" class="menu-slider"><div></div></div> + </div> + <h4 class="credits-display"><img class="icon" src="../../images/coin-stack.svg" alt=""><span> {{(credits/100).toLocaleString('en')}}</span></h4> + + <template v-if="page == 'new-order'"> + + <div class="services-legend"> + <h5>Name</h5><h5>Credits per 1000</h5><h5>Min Qt.</h5><h5>Max Qt.</h5> + </div> + + <ServicePane :site="'youtube'" :services="services" + @select="select"></ServicePane> + + <ServicePane :site="'instagram'" :services="services" + @select="select"></ServicePane> + + <ServicePane :site="'twitter'" :services="services" + @select="select"></ServicePane> + + <ServicePane :site="'tiktok'" :services="services" + @select="select"></ServicePane> + + <div id="overlay" v-if="selected"> + + <div v-if="!completed" class="overlay-item"> + <img @click="completed = false; selected = null" class="cancel icon" + src="../../images/cancel-icon2.svg" alt=""/> + <img v-if="selected.site == 'youtube'" class="icon" + src="../../images/youtube-icon.svg" alt=""/> + <img v-if="selected.site == 'instagram'" class="icon" + src="../../images/instagram-icon.svg" alt=""/> + <img v-if="selected.site == 'twitter'" class="icon" + src="../../images/twitter.svg" alt=""/> + <img v-if="selected.site == 'tiktok'" class="icon" + src="../../images/tik-tok.svg" alt=""/> + <h3>{{selected.name}}</h3> + <h4>Cost: {{(cost).toLocaleString('en')}}</h4> + <h4>Quantity</h4> + <div><input required :min="selected.minimum" :max="selected.maximum" + type="number" v-model="amount" id="selQty"><span> / + {{selected.maximum.toLocaleString('en')}}</span></div> + + <template v-if="selected.modifier == 'location'"> + <h4>Location</h4> + <div><select required id="country" name=""> + <option value="usa">USA</option> + <option value="canada">Canada</option> + <option value="uk">United Kingdom</option> + <option value="germany">Germany</option> + <option value="france">France</option> + </select> + </div> + </template> + + <template v-if="selected.modifier == 'language'"> + <h4>Location</h4> + <div><select required id="language" name=""> + <option value="english">English</option> + <option value="french">French</option> + <option value="spanish">Spanish</option> + <option value="german">German</option> + <option value="arabic">Arabic</option> + </select> + </div> + </template> + + <h4>URL</h4> + <div><input required type="url" id="url" v-model="url"></div> + <button @click="buyService" :disabled="paying">Submit<loading + v-if="paying"></loading></button> + <p id="overlay-error">{{errorText}}</p> + </div> + + <div class="overlay-item" v-else-if="completed"> + <img @click="completed = false; selected = null" class="cancel icon" + src="../../images/cancel-icon2.svg" alt=""/> + <img class="icon" src="../../images/checked2.svg" alt=""/> + <h3>Success!</h3> + </div> + + </div> + + </template> + + <credits @purchase-complete="$emit('updateUser')" :preferred="preferred" :token="token" v-if="page == 'credits'"></credits> + +</div> +</template> + +<script> +import ServicePane from './service-pane.vue' +import Credits from './credits.vue' +import Loading from '../icons/loading.vue' + +function select(service) { + this.completed = false + if (this.amount < service.minimum){ + this.amount = service.minimum; + } + if (this.amount > service.maximum){ + this.amount = service.maximum; + } + + this.selected = service + this.errorText = '' +} + +function cost() { + return (this.selected.price * this.amount / 100000).toFixed(2) +} + +function buyService() { + if (!this.url) { + this.errorText = "You must provide a URL." + return + } else if (Math.ceil(this.cost > this.credits)) { + this.errorText = 'Insuficient Credits' + return + } else if (this.amount < this.selected.minimum || this.amount > this.selected.maximum) { + this.errorText = 'Invalid amount' + return + } + + this.paying = true + let note = '' + let country = document.getElementById('country') + let language = document.getElementById('language') + + if (country) { + note = JSON.stringify({'location': country.value}) + } else if (language) { + note = JSON.stringify({'language': language.value}) + } + + fetch('/panel/orders', { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'service': this.selected.id, + 'quantity': this.amount, 'url': this.url, 'note': note}), }).then( + response => { + if (response.ok) { + this.errorText = `Success!` + this.completed = true + this.$emit('updateUser') + this.$emit('updateOrders') + } else if (response.status == 520) { + this.errorText = 'Insuficient Credits' + } else { + this.errorText = `Error ${response.status}: + ${response.statusText}` + } + + this.paying = false + } + ) + +} + +function page() { + switch (this.active) { + case '#new-order': + return 'new-order' + case '#credits': + return 'credits' + } +} + +export default { + data() { + return {servicePane: true, services: null, selected: null, amount: 0, + paying: false, url: '', completed: false, errorText: ''} + }, + components: {ServicePane, Loading, Credits}, + props: ['token', 'credits', 'active', 'preferred'], + created() { + fetch("/panel/services", { + method: 'GET', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token} + }).then(response => { + response.json().then(data => {this.services = data}) + }) + }, + methods: {select, buyService}, + computed: {cost, page}, + emits: ['updateUser', 'updateOrders'] +} +</script> diff --git a/javascript-vue/js/panel/settings.vue b/javascript-vue/js/panel/settings.vue new file mode 100644 index 0000000..21df833 --- /dev/null +++ b/javascript-vue/js/panel/settings.vue @@ -0,0 +1,119 @@ +<template> +<div> + <h2>Settings</h2> + <section class="change-name-pane"> + <h4>Name</h4> + <input :value="user.name" name="name" id="changed_name" type="text"> + <button @click="changeName">Save <loading src="../../images/loading-white.svg" alt=""></loading></button> + <span></span> + </section> + <section class="change-email-pane"> + <h4>Email</h4> + <input :value="user.email" name="email" type="text" id="changed_email"> + <button @click="changeEmail">Save<img class="loading-icon" src="../../images/loading-white.svg" alt=""></button> + <span></span> + </section> + <section class="change-password-pane"> + <h4>Change Password</h4> + <h5>Current Password</h5><input name="current_passowrd" id="current_password" type="password"> + <h5>New Password</h5><input id="new_password" name="password" type="password"> + <h5>Confirm Password</h5><input id="confirm_password" name="confirm_passowrd" type="password"> + <button @click="changePassword">Save<img class="loading-icon" src="../../images/loading-white.svg" alt=""></button> + <span></span> + </section> +</div> +</template> + +<script> +import Loading from '../icons/loading.vue' + +function changeName() { + let name = document.getElementById('changed_name').value + let info = document.querySelector('.change-name-pane span') + let pane = document.querySelector('.change-name-pane') + + pane.classList.add('loading') + pane.classList.remove('error') + fetch("/panel/change-name", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'name': name}), + }).then(response => { + if (response.ok) { + pane.classList.add('completed') + info.textContent = 'Completed' + } else { + pane.classList.add('error') + info.textContent = 'Error: ' + response.status + } + + pane.classList.remove('loading') + }) +} + +function changeEmail() { + let email = document.getElementById('changed_email').value + let info = document.querySelector('.change-email-pane span') + let pane = document.querySelector('.change-email-pane') + + pane.classList.add('loading') + pane.classList.remove('error') + fetch("/panel/change-email", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'email': email}), + }).then(response => { + if (response.ok) { + pane.classList.add('completed') + info.textContent = 'Verification link sent' + } else { + pane.classList.add('error') + info.textContent = 'Error: ' + response.status + } + + pane.classList.remove('loading') + }) +} + +function changePassword() { + let info = document.querySelector('.change-password-pane span') + let pane = document.querySelector('.change-password-pane') + + pane.classList.add('loading') + pane.classList.remove('error') + fetch("/panel/change-password", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'current_password': + document.getElementById('current_password').value, + 'password': document.getElementById('new_password').value, + 'password_confirmation': + document.getElementById('confirm_password').value}), + }).then(response => { + response.json().then(data => {console.log(data)}) + if (response.ok) { + pane.classList.add('completed') + info.textContent = 'Completed' + } else { + pane.classList.add('error') + info.textContent = 'Error: ' + response.status + } + + pane.classList.remove('loading') + }) +} + +export default { + components: {Loading,}, + methods: { + changePassword, changeName, changeEmail + }, + props: ['user', 'token'] +} +</script> diff --git a/javascript-vue/js/panel/sidebar.vue b/javascript-vue/js/panel/sidebar.vue new file mode 100644 index 0000000..4404fdb --- /dev/null +++ b/javascript-vue/js/panel/sidebar.vue @@ -0,0 +1,46 @@ +<template> +<nav id="sidebar"> +<a :class="{selected: active == ''}" href="/panel#"> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-house-door-fill" viewBox="0 0 16 16"> + <path d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5z"/> +</svg> +</a> +<a :class="{selected: active == '#orders'}" href="/panel#orders"> +<svg fill="currentColor" enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m16.12 1.929-10.891 5.576-4.329-2.13 10.699-5.283c.24-.122.528-.122.78 0z"/><path d="m23.088 5.375-11.082 5.49-4.15-2.045-.6-.305 10.903-5.575.6.304z"/><path d="m11.118 12.447-.012 11.553-10.614-5.539c-.3-.158-.492-.475-.492-.816v-10.688l4.498 2.216v3.896c0 .499.408.913.9.913s.9-.414.9-.913v-2.995l.6.292z"/><path d="m23.988 6.969-11.07 5.466-.012 11.553 11.094-5.793z"/></svg> +</a> + +<a :class="{selected: active == '#new-order' || active == '#credits'}" href="/panel#new-order"> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10c5.514,0,10-4.486,10-10S17.514,2,12,2z M17,13h-4v4h-2v-4H7v-2h4V7h2v4h4V13z"></path></svg> +</a> + +<a :class="{selected: active == '#settings'}" href="/panel#settings"> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-gear-fill" viewBox="0 0 16 16"> + <path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/> +</svg> +</a> + +<a :class="{selected: active == '#support'}" href="/panel#support"> +<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-life-preserver" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M14.43 10.772l-2.788-1.115a4.015 4.015 0 0 1-1.985 1.985l1.115 2.788a7.025 7.025 0 0 0 3.658-3.658zM5.228 14.43l1.115-2.788a4.015 4.015 0 0 1-1.985-1.985L1.57 10.772a7.025 7.025 0 0 0 3.658 3.658zm9.202-9.202a7.025 7.025 0 0 0-3.658-3.658L9.657 4.358a4.015 4.015 0 0 1 1.985 1.985l2.788-1.115zm-8.087-.87L5.228 1.57A7.025 7.025 0 0 0 1.57 5.228l2.788 1.115a4.015 4.015 0 0 1 1.985-1.985zM8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm0-5a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/> +</svg> +</a> + +<a v-if="role == 'admin'" :class="{selected: active == '#admin'}" href="/telescope"> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-key-fill" viewBox="0 0 16 16"> + <path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1 1H6.663a3.5 3.5 0 0 1-3.163 2zM2.5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/> +</svg> +</a> +<a :class="{selected: active == '#exit'}" href="/panel#exit"> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-door-open-fill" viewBox="0 0 16 16"> + <path d="M1.5 15a.5.5 0 0 0 0 1h13a.5.5 0 0 0 0-1H13V2.5A1.5 1.5 0 0 0 11.5 1H11V.5a.5.5 0 0 0-.57-.495l-7 1A.5.5 0 0 0 3 1.5V15H1.5zM11 2h.5a.5.5 0 0 1 .5.5V15h-1V2zm-2.5 8c-.276 0-.5-.448-.5-1s.224-1 .5-1 .5.448.5 1-.224 1-.5 1z"/> +</svg> +</a> +</nav> +</template> + +<script> +export default { + props: ['active', 'role'] +} +</script> + diff --git a/javascript-vue/js/panel/summary.vue b/javascript-vue/js/panel/summary.vue new file mode 100644 index 0000000..9a8bdf5 --- /dev/null +++ b/javascript-vue/js/panel/summary.vue @@ -0,0 +1,3 @@ +<template> +<div id="main">important info here</div> +</template> diff --git a/javascript-vue/js/panel/support.vue b/javascript-vue/js/panel/support.vue new file mode 100644 index 0000000..b9af205 --- /dev/null +++ b/javascript-vue/js/panel/support.vue @@ -0,0 +1,82 @@ +<template> +<div class="support-section" id="main"> + +<h2>Support</h2> + +<loading v-if="loading"></loading> + +<div v-if="!loading && complete" class="dialog"> + <img class="icon" src="../../images/checked2.svg" alt=""> + <h3>Ticket sent. An administrator will contact you soon.</h3> +</div> + +<div v-if="!loading && !complete" id="support-form"> + +<label for="">Topic</label> +<select id="support-topic" name="" v-model="topic"> + <option value="order">Order</option> + <option value="service">Service</option> + <option value="credits">Credits</option> + <option value="payment">Payment</option> + <option value="other">Other</option> +</select> + +<label for="">Details</label> +<textarea id="" name="" cols="30" rows="10" v-model="message"></textarea> +<span class="note-grey">Include any relevant information like the order number, +service name, etc</span> + +<button @click="send">Submit</button> + +<p class="error-message">{{errorMessage}}</p> + +</div> + +</div> +</template> + + +<script> +import Loading from '../icons/loading.vue' + +function send() { + this.errorMessage = '' + + if (!this.topic || !this.message) { + this.errorMessage = 'Topic and details cannot be blank.' + return + } + + this.loading = true + + fetch("/panel/support", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({'topic': this.topic, 'message': this.message})}). + then(response => { + if (response.ok) { + this.complete = true + } else { + this.complete = false + this.error = true + this.errorMessage = `${response.status}: + ${response.statusText}` + } + + this.loading = false + }) + +} + +export default { + components: {Loading}, + props: ['user', 'token'], + data() { + return {loading: false, complete: false, error: false, errorMessage: + '', topic: null, message: ''} + }, + methods: {send} +} +</script> diff --git a/javascript-vue/js/panel/transaction-endpoint.vue b/javascript-vue/js/panel/transaction-endpoint.vue new file mode 100644 index 0000000..68d6da6 --- /dev/null +++ b/javascript-vue/js/panel/transaction-endpoint.vue @@ -0,0 +1,33 @@ +<template> +<div v-once id="main"> + +<div v-if="active == '#transaction-complete' && user.paying" class="status-dialog"> + <img class="icon" src="../../images/checked2.svg" alt=""/> + <h3>Purchase complete.</h3> +</div> + +<div v-if="active == '#transaction-failed' && user.paying" class="status-dialog"> + <img class="icon" src="../../images/warning-colored.svg" alt=""/> + <h3>Purchase failed.</h3> +</div> + +</div> +</template> + +<script> +export default { + props: ['token', 'user', 'active'], + emits: ['purchaseComplete'], + + //Should check that user is actualling in the paying state. If so, send get + //request to panel/transaction-end. It's then() should emit a payment + //complete to panel for a user state refresh. + mounted() { + fetch('/panel/clear-paying', { + method: 'GET', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}}).then(() => this.$emit('purchaseComplete')) + } +} +</script> diff --git a/javascript-vue/js/register-area/register-area.vue b/javascript-vue/js/register-area/register-area.vue new file mode 100644 index 0000000..311fa3a --- /dev/null +++ b/javascript-vue/js/register-area/register-area.vue @@ -0,0 +1,168 @@ +<template> + <form v-if="active === 'register'" onsubmit = "event.preventDefault(); return false" id="register-form"> + <h3>Registration</h3> + <div>{{errorMessage}}</div> + <div> + <label for='sender_name'>Name</label> + <input id='register-name' required type='name' name='sender_name' placeholder='' + spellcheck='false'> + </div> + <div> + <label for='sender_email'>Email</label> + <input v-on:input="checkPasswords" id='register-email' required type='email' name='sender_email' placeholder='' + spellcheck='false'> + </div> + <div> + <label for='sender_password'>Password</label> + <input v-on:input="checkPasswords" id='register-password' required type='password' name='sender_password' + placeholder='' spellcheck='false'> + </div> + <div> + <label for='confirm_password'>Confirm Password</label> + <input v-on:input="checkPasswords" id='confirm-password' required type='password' + name='sender_password' placeholder='' spellcheck='false'> + </div> + <button @click="register($event)" class="submit-btn" type="submit">Submit</button> + </form> + <form v-if="active === 'forgot'" v-on:submit="forgotPassword" id="forgot-form"> + <h3>Forgot Password</h3> + <div> + <label for='sender_email'>Email</label> + <input id='forgot-email' required type='email' name='sender_email' placeholder='' + spellcheck='false'> + </div> + <button class="submit-btn" type="submit">Submit</button> + </form> + <img v-if="active === 'loading'" type="image/svg+xml" class="loading-icon" src="../../images/loading.svg" alt=""/> + <div v-if="active === 'register-completed'"> + <img class="medium-icon" src="../../images/checked2.svg" alt=""> + <h3>Success!</h3> + <p>A verification link has been sent to your inbox.</p> + </div> + <div v-if="active === 'forgot-completed'"> + <img class="medium-icon" src="../../images/checked2.svg" alt=""> + <h3>Success!</h3> + <p>A password reset link has been sent.</p> + </div> + <div v-if="active === 'error'"> + <img class="medium-icon" src="../../images/warning-colored.svg" alt=""> + <h3>An Error Occured.</h3> + <p>{{`${error}: ${errorMessage}`}}</p> + </div> + <div v-on:click="closeArea" class="cancel-button"></div> +</template> + +<script> +function getCookie(name) { + var re = new RegExp(name + "=([^;]+)") + var value = re.exec(document.cookie) + + let v = (value != null) ? unescape(value[1]) : null + return v +} + +function getToken() { + return fetch("/sanctum/csrf-cookie", { + method: 'GET' + }).then( () => { + this.token = this.getCookie('XSRF-TOKEN') + return this.token + }) +} + function register(event) { + event.preventDefault(); + event.stopPropagation(); + + this.errorMessage = '' + let name = document.getElementById("register-name").value + let email = document.getElementById("register-email").value + let password = document.getElementById("register-password").value + let password_confirmation = document.getElementById("confirm-password").value + + this.active = 'loading' + + this.getToken().then(() => {fetch("/register", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({"name": name, + "email": email, + "password": password, + "password_confirmation": password_confirmation})}) + .then(response => { + //Give completed or error + if (response.ok) { + this.active = 'register-completed' + } else if (response.status != 500) { + response.json().then((e) => { + try { + for (let i in e.errors) { + this.errorMessage = e.errors[i].flat().join("\n") + + "\n" + this.errorMessage + } + } catch (x) { + this.errorMessage = e.message + } + }) + this.active = 'register' + } else { + this.errorMessage = response.statusText + this.active = 'register' + } + }); + }) + return false + } + + function checkPasswords() { + let passInput = document.getElementById('register-password') + let passInput2 = document.getElementById('confirm-password') + if (passInput.value != passInput2.value) { + passInput2.setCustomValidity('Passwords must be matching') + } else { + passInput2.setCustomValidity(''); + } + } + + function forgotPassword(event) { + let email = document.getElementById("forgot-email").value + this.active = 'loading' + fetch("/forgot-password", { + method: 'POST', + headers: {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-XSRF-TOKEN': this.token}, + body: JSON.stringify({"email": email})}) + .then(response => { + if (response.ok) { + this.active = 'forgot-completed' + } else { + this.error = response.status + this.errorMessage = response.statusText + this.active = 'error' + } + /* console.log(response.json()) */ + }); + event.preventDefault(); + } + + module.exports = { + data() { + return {active: 'register', token: '', errorMessage: ''} + }, + methods: { + getToken, + getCookie, + register, + checkPasswords, + forgotPassword, + closeArea() { + document.querySelector(".register-area").classList.remove("active") + }, + }, + mounted() { + this.getToken() + } + } +</script>