updated to laravel 11
This commit is contained in:
BIN
resources/assets/background.webp
Normal file
BIN
resources/assets/background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
BIN
resources/assets/loader.gif
Normal file
BIN
resources/assets/loader.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
resources/assets/logo.webp
Normal file
BIN
resources/assets/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
resources/assets/sad-monster.webp
Normal file
BIN
resources/assets/sad-monster.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
281
resources/assets/script.js
Normal file
281
resources/assets/script.js
Normal file
@@ -0,0 +1,281 @@
|
||||
let SM = {
|
||||
alert: (title, text, type = 'info') =>{
|
||||
data = {
|
||||
position: 'top-end',
|
||||
timer: 7000,
|
||||
toast: true,
|
||||
title: title,
|
||||
text: text,
|
||||
showConfirmButton: false,
|
||||
showCloseButton: true,
|
||||
customClass: {
|
||||
container: type,
|
||||
}
|
||||
}
|
||||
|
||||
Swal.fire(data);
|
||||
},
|
||||
|
||||
copyToClipboard: (text) => {
|
||||
const copyContent = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
SM.alert('Link copied', 'The link has been copied to the clipboard.', 'success');
|
||||
} catch (err) {
|
||||
SM.alert('Copy failed', 'Could not copy the link to the clipboard. It may not have permission in your browser.', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
copyContent();
|
||||
},
|
||||
|
||||
updateBillingAddress: () => {
|
||||
const checkboxElement = document.querySelector('input[name="billing_same_home"]');
|
||||
|
||||
if (checkboxElement) {
|
||||
const itemNames = ['address', 'address2', 'city', 'state', 'postcode', 'country'];
|
||||
|
||||
if (checkboxElement.checked) {
|
||||
itemNames.forEach((itemName) => {
|
||||
const element = document.querySelector(`input[name="billing_${itemName}"]`);
|
||||
element.value = document.querySelector(`input[name="home_${itemName}"]`).value;
|
||||
element.setAttribute('readonly', 'true');
|
||||
});
|
||||
} else {
|
||||
itemNames.forEach((itemName) => {
|
||||
const element = document.querySelector(`input[name="billing_${itemName}"]`);
|
||||
element.removeAttribute('readonly');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
confirmDelete: (token, title, content, url) => {
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'warning',
|
||||
iconColor: '#b91c1c',
|
||||
title: title,
|
||||
html: content,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Delete',
|
||||
confirmButtonColor: '#b91c1c',
|
||||
cancelButtonText: 'Cancel',
|
||||
reverseButtons: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
axios.delete(url)
|
||||
.then((response) => {
|
||||
if(response.data.success){
|
||||
window.location.href = response.data.redirect;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
upload: (files, callback, titles = []) => {
|
||||
let uploadedFiles = [];
|
||||
|
||||
if(files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
title: "Checking...",
|
||||
text: "Please wait",
|
||||
imageUrl: "/loading.gif",
|
||||
imageHeight: 100,
|
||||
showConfirmButton: false,
|
||||
allowOutsideClick: false
|
||||
}
|
||||
Swal.fire(data);
|
||||
|
||||
const showError = (message) => {
|
||||
failed = true;
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'error',
|
||||
title: 'An error occurred',
|
||||
html: message,
|
||||
showConfirmButton: true,
|
||||
confirmButtonColor: '#b91c1c',
|
||||
}).then((result) => {
|
||||
if(callback) {
|
||||
callback({success: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for(const file of files) {
|
||||
if (file.size > SM.maxUploadSize()) {
|
||||
const size = SM.bytesToString(file.size);
|
||||
const maxSize = SM.bytesToString(SM.maxUploadSize());
|
||||
showError('The file size is too large (' + size + ').<br />Please upload a file less than ' + maxSize + '.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFile = (file, title, idx, count) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (title !== '') {
|
||||
formData.append('title', title);
|
||||
}
|
||||
|
||||
axios.post('/admin/media/', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
let percent = (progressEvent.loaded / progressEvent.total) * 100;
|
||||
Swal.update({
|
||||
title: 'Uploading...',
|
||||
html: `${file.name} - ${percent.toFixed(2)}%`,
|
||||
});
|
||||
}
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
uploadedFiles.push({file: file, title: title, data: response.data});
|
||||
|
||||
if (idx === count - 1) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Success',
|
||||
html: count > 1 ? `Uploaded ${count} files successfully` : `${file.name} uploaded successfully`,
|
||||
showConfirmButton: false,
|
||||
timer: 3000
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
window.setTimeout(() => {
|
||||
callback({success: true, files: uploadedFiles});
|
||||
}, 3000);
|
||||
}
|
||||
} else {
|
||||
idx += 1;
|
||||
uploadFile(files[idx], titles[idx] || '', idx, files.length);
|
||||
}
|
||||
} else {
|
||||
showError(response.data.message);
|
||||
}
|
||||
}).catch((error) => {
|
||||
showError('An error occurred while uploading the file.');
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(files[0], titles[0] || '', 0, files.length);
|
||||
},
|
||||
|
||||
bytesToString: (bytes) => {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
const size = parseFloat((bytes / Math.pow(1024, i)).toFixed(2));
|
||||
return size + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
maxUploadSize: () => {
|
||||
try {
|
||||
return parseInt(document.querySelector('meta[name="max-upload-size"]').getAttribute('content'));
|
||||
} catch (error) {
|
||||
/* Do nothing */
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms a string to title case.
|
||||
* @param {string} str The string to transform.
|
||||
* @returns {string} A string transformed to title case.
|
||||
*/
|
||||
toTitleCase: (str) => {
|
||||
// Remove leading and trailing spaces
|
||||
str = str.trim();
|
||||
|
||||
// Remove file extension
|
||||
str = str.replace(/\.[a-zA-Z0-9]{1,4}$/, "");
|
||||
|
||||
// Replace underscores and hyphens with spaces
|
||||
str = str.replace(/[_-]+/g, " ");
|
||||
|
||||
// Capitalize the first letter of each word and make the rest lowercase
|
||||
str = str.replace(/\b\w+\b/g, (txt) => {
|
||||
return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
// Replace "cdn" with "CDN"
|
||||
str = str.replace(/\bCdn\b/gi, "CDN");
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
mediaDetails: (name, callback) => {
|
||||
axios.get('/media/' + name, {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}).then((response) => {
|
||||
callback(response.data);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
callback(null);
|
||||
});
|
||||
},
|
||||
|
||||
mimeMatches: (fileMime, matchMimeList) => {
|
||||
for(const matchMime of matchMimeList.split(',')) {
|
||||
if (matchMime === '*' || matchMime === '*/*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const matchMimeArray = matchMime.split('/');
|
||||
const fileMimeArray = fileMime.split('/');
|
||||
|
||||
if (matchMimeArray[1] === '*' && matchMimeArray[0] === fileMimeArray[0]) {
|
||||
return true;
|
||||
} else if(fileMime === matchMime) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
arrayToString: (array, separator = ',') => {
|
||||
return array.map(item => {
|
||||
if (item.includes(separator)) {
|
||||
// If the item contains the separator, wrap it in quotes and escape any quotes within the string
|
||||
return `"${item.replace(/"/g, '\\"')}"`;
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
}).join(separator);
|
||||
},
|
||||
|
||||
stringToArray: (string, separator = ',') => {
|
||||
return string.split(separator).map(item => {
|
||||
// Remove quotes and unescape any escaped quotes within the string
|
||||
return item.replace(/^"|"$/g, '').replace(/\\"/g, '"');
|
||||
});
|
||||
},
|
||||
|
||||
decodeHtml: (html) => {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.innerHTML = html;
|
||||
return ta.value;
|
||||
},
|
||||
|
||||
toLocalISOString: (date) => {
|
||||
return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0') + 'T' + date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
SM.updateBillingAddress();
|
||||
});
|
||||
293
resources/css/app.css
Normal file
293
resources/css/app.css
Normal file
@@ -0,0 +1,293 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
@apply bg-gray-100;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Poppins, Roboto, Open Sans, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
a.link {
|
||||
@apply text-primary-color hover:underline hover:text-primary-color-light;
|
||||
}
|
||||
|
||||
input[type="text"]:read-only {
|
||||
@apply bg-gray-100 focus:border-gray-300 focus:border-gray-300;
|
||||
}
|
||||
|
||||
.list-circle {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.image-background {
|
||||
@apply bg-no-repeat bg-center bg-cover;
|
||||
background-image: url('/resources/assets/background.webp');
|
||||
}
|
||||
|
||||
body.swal2-height-auto {
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
.swal2-container {
|
||||
&.swal2-top {
|
||||
padding-top: 5rem;
|
||||
}
|
||||
|
||||
.swal2-title {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
.swal2-html-container {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
.swal2-actions button {
|
||||
@apply rounded-md text-white px-8 py-1.5 text-sm font-semibold leading-6 shadow-sm;
|
||||
}
|
||||
|
||||
.swal2-popup.swal2-toast {
|
||||
.swal2-title {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.swal2-html-container {
|
||||
@apply text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
&.success .swal2-popup {
|
||||
@apply bg-green-100 text-green-900;
|
||||
|
||||
.swal2-close {
|
||||
@apply text-green-900 hover:text-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger .swal2-popup {
|
||||
@apply bg-red-100 text-red-900;
|
||||
|
||||
.swal2-close {
|
||||
@apply text-red-900 hover:text-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning .swal2-popup {
|
||||
@apply bg-yellow-100 text-yellow-900;
|
||||
|
||||
.swal2-close {
|
||||
@apply text-yellow-900 hover:text-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
&.info .swal2-popup {
|
||||
@apply bg-blue-100 text-blue-900;
|
||||
|
||||
.swal2-close {
|
||||
@apply text-blue-900 hover:text-red-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
@apply bg-white rounded-lg border border-gray-300;
|
||||
|
||||
.menu {
|
||||
@apply border-b border-gray-300 flex px-2 py-1 gap-[1px] flex-wrap;
|
||||
|
||||
button {
|
||||
@apply text-xs p-1 min-w-5;
|
||||
|
||||
&:hover:not(.selected) {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@apply bg-primary-color text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
@apply p-2 min-h-96;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
h1 {
|
||||
@apply text-3xl font-semibold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl font-semibold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl font-semibold;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-primary-color hover:underline hover:text-primary-color-light;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply mx-4 bg-gray-100 rounded py-2 px-4 mb-4;
|
||||
|
||||
p:last-of-type {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply text-sm bg-gray-100 rounded py-2 px-4 mb-4;
|
||||
}
|
||||
|
||||
/* Color swatches */
|
||||
.color {
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
background-color: var(--color);
|
||||
border: 1px solid rgba(128, 128, 128, 0.3);
|
||||
border-radius: 2px;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
margin-bottom: 0.15em;
|
||||
margin-right: 0.1em;
|
||||
vertical-align: middle;
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Box */
|
||||
.box {
|
||||
@apply border text-sm px-3 py-2 rounded-lg my-4 mx-auto max-w-2xl relative pl-8;
|
||||
|
||||
&.success {
|
||||
@apply bg-green-100 text-green-900 border-green-600;
|
||||
|
||||
&:after {
|
||||
content: '\f058';
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
top: 0.5rem;
|
||||
left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply bg-blue-100 text-blue-900 border-blue-600;
|
||||
|
||||
&:after {
|
||||
content: '\f05a';
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
top: 0.5rem;
|
||||
left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply bg-yellow-100 text-yellow-900 border-yellow-600;
|
||||
|
||||
&:after {
|
||||
content: '\f071';
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
top: 0.5rem;
|
||||
left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@apply bg-red-100 text-red-900 border-red-600;
|
||||
|
||||
&:after {
|
||||
content: '\f057';
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
top: 0.5rem;
|
||||
left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.bug {
|
||||
@apply bg-purple-100 text-purple-900 border-purple-600;
|
||||
|
||||
&:after {
|
||||
content: '\f188';
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
top: 0.5rem;
|
||||
left: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sm-media-picker-container {
|
||||
grid-template-rows: minmax(min-content, auto) 1fr minmax(min-content, auto);
|
||||
}
|
||||
|
||||
.sm-media-picker {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 70rem;
|
||||
max-height: 50rem;
|
||||
|
||||
grid-template-rows: minmax(min-content, auto) 1fr minmax(min-content, auto);
|
||||
|
||||
.swal2-actions {
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sm-banner-open {
|
||||
@apply bg-green-800;
|
||||
}
|
||||
|
||||
.sm-banner-closed {
|
||||
@apply bg-red-600;
|
||||
}
|
||||
|
||||
.sm-banner-full {
|
||||
@apply bg-purple-600;
|
||||
}
|
||||
|
||||
.sm-banner-draft {
|
||||
@apply bg-yellow-600;
|
||||
}
|
||||
|
||||
.sm-banner-cancelled {
|
||||
@apply bg-purple-600 text-xs;
|
||||
}
|
||||
|
||||
.sm-registration-none, .sm-registration-email, .sm-registration-message, .sm-registration-scheduled, .sm-registration-draft {
|
||||
@apply text-xs rounded py-2 px-2.5 text-center mb-4 border border-yellow-400 text-yellow-800 bg-yellow-100;
|
||||
}
|
||||
|
||||
.sm-registration-closed {
|
||||
@apply text-xs rounded py-2 px-2.5 text-center mb-4 border border-red-400 text-red-800 bg-red-100;
|
||||
}
|
||||
|
||||
.sm-registration-full {
|
||||
@apply text-xs rounded py-2 px-2.5 text-center mb-4 border border-purple-400 text-purple-800 bg-purple-100;
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
*,
|
||||
:after,
|
||||
:before {
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
border: 0 solid #e5e7eb;
|
||||
}
|
||||
html {
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: auto;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
font-family: Poppins, Roboto, "Open Sans", ui-sans-serif, system-ui,
|
||||
sans-serif;
|
||||
font-size: 1rem;
|
||||
color: #000;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
min-width: 100%;
|
||||
overflow-x: hidden;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
p + p,
|
||||
p + ul,
|
||||
ul + p {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
small {
|
||||
display: inline-block;
|
||||
}
|
||||
.small {
|
||||
font-size: smaller;
|
||||
}
|
||||
.x-small {
|
||||
font-size: x-small;
|
||||
}
|
||||
.list-decimal,
|
||||
.list-disc,
|
||||
.list-circle {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
.list-decimal li,
|
||||
.list-disc li,
|
||||
.list-circle li {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
a:not([role="button"]) {
|
||||
color: #0284c7;
|
||||
}
|
||||
a:not([role="button"]):hover {
|
||||
color: #0ea5e9;
|
||||
}
|
||||
a[role="button"] {
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
input:disabled {
|
||||
background-color: rgba(243, 244, 246);
|
||||
}
|
||||
input[type="submit"]:disabled {
|
||||
background-color: rgba(209, 213, 219);
|
||||
}
|
||||
input {
|
||||
font-family: Poppins, Roboto, "Open Sans", ui-sans-serif, system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
.scrollbar-width-none {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-width-none::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.bg-center {
|
||||
background-position: center;
|
||||
}
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.spin {
|
||||
animation: rotate 1s infinite linear;
|
||||
}
|
||||
.text-xxs {
|
||||
font-size: 0.6rem;
|
||||
line-height: 0.75rem;
|
||||
}
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.sm-html .ProseMirror {
|
||||
outline: none;
|
||||
}
|
||||
.sm-html hr {
|
||||
border-top: 1px solid #aaa;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.sm-html pre {
|
||||
padding: 0 1rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
.sm-html blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
margin-left: 1rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.sm-html p.info,
|
||||
.sm-html p.success,
|
||||
.sm-html p.warning,
|
||||
.sm-html p.danger {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem 0.5rem 0.75rem;
|
||||
margin: 0.5rem;
|
||||
font-size: 80%;
|
||||
}
|
||||
.sm-html p.info::before,
|
||||
.sm-html p.success::before,
|
||||
.sm-html p.warning::before,
|
||||
.sm-html p.danger::before {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-top: 0.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sm-html p.info {
|
||||
border: 1px solid rgba(14, 165, 233, 1);
|
||||
background-color: rgba(14, 165, 233, 0.25);
|
||||
}
|
||||
.sm-html p.info::before {
|
||||
color: rgba(14, 165, 233, 1);
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z' fill='rgba(14,165,233,1)' /%3E%3C/svg%3E");
|
||||
}
|
||||
.sm-html p.success {
|
||||
border: 1px solid rgba(22, 163, 74, 1);
|
||||
background-color: rgba(22, 163, 74, 0.25);
|
||||
}
|
||||
.sm-html p.success::before {
|
||||
color: rgba(22, 163, 74, 1);
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z' fill='rgba(22,163,74,1)' /%3E%3C/svg%3E");
|
||||
}
|
||||
.sm-html p.warning,
|
||||
div.warning {
|
||||
border: 1px solid rgba(202, 138, 4, 1);
|
||||
background-color: rgba(250, 204, 21, 0.25);
|
||||
}
|
||||
.sm-html p.warning::before {
|
||||
color: rgba(202, 138, 4, 1);
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16' fill='rgba(202,138,4,1)' /%3E%3C/svg%3E");
|
||||
}
|
||||
.sm-html p.danger {
|
||||
border: 1px solid rgba(220, 38, 38, 1);
|
||||
background-color: rgba(220, 38, 38, 0.25);
|
||||
}
|
||||
.sm-html p.danger::before {
|
||||
color: rgba(220, 38, 38, 1);
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41' fill='rgba(220,38,38,1)' /%3E%3C/svg%3E");
|
||||
}
|
||||
.sm-html img {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.sm-html ul {
|
||||
list-style: disc;
|
||||
margin: 1rem 2rem;
|
||||
}
|
||||
.sm-html ol {
|
||||
list-style: decimal;
|
||||
margin: 1rem 2rem;
|
||||
}
|
||||
.sm-html ul li,
|
||||
.sm-html ol li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.sm-editor::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
width: 16px;
|
||||
}
|
||||
.sm-editor::-webkit-scrollbar-thumb {
|
||||
background-color: #aaa;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 8px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.selected-checked {
|
||||
border: 3px solid rgba(2, 132, 199, 1);
|
||||
position: relative;
|
||||
}
|
||||
.selected-checked::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border: 1px solid white;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
background-color: rgba(2, 132, 199, 1);
|
||||
top: -0.4rem;
|
||||
right: -0.4rem;
|
||||
content: "";
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M21,7L9,19L2.712,12.712L5.556,9.892L9.029,13.358L18.186,4.189L21,7Z' fill='rgba(255,255,255,1)' /%3E%3C/svg%3E");
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
4
resources/js/app.js
Normal file
4
resources/js/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import './bootstrap';
|
||||
import './media-picker.js';
|
||||
import './tooltip.js';
|
||||
import './editor/TipTap.js';
|
||||
4
resources/js/bootstrap.js
vendored
4
resources/js/bootstrap.js
vendored
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="{ name: 'article', params: { slug: props.article.slug } }"
|
||||
class="article-card bg-white border-1 border-rounded-xl text-black decoration-none hover:shadow-md transition min-w-72">
|
||||
<div
|
||||
class="h-48 bg-cover bg-center rounded-t-xl relative"
|
||||
:style="{
|
||||
backgroundImage: `url(${mediaGetVariantUrl(
|
||||
props.article.hero,
|
||||
'medium'
|
||||
)})`,
|
||||
}"></div>
|
||||
<div class="p-4 text-xs text-gray-7">
|
||||
{{ computedDate(props.article.publish_at) }}
|
||||
</div>
|
||||
<h3 class="px-4 mb-3 font-500 text-gray-7">
|
||||
{{ props.article.title }}
|
||||
</h3>
|
||||
<p class="p-4 text-sm text-gray-7">
|
||||
{{ excerpt(props.article.content) }}
|
||||
</p>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Article } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import { excerpt } from "../helpers/string";
|
||||
|
||||
const props = defineProps({
|
||||
article: {
|
||||
type: Object as () => Article,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const computedDate = (date) => {
|
||||
return new SMDate(date, { format: "yMd" }).format("d MMMM yyyy");
|
||||
};
|
||||
</script>
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<SMHeader
|
||||
v-if="showEditor || (modelValue && modelValue.length > 0)"
|
||||
:no-copy="props.showEditor"
|
||||
text="Files" />
|
||||
<p v-if="props.showEditor" class="small">
|
||||
{{ modelValue.length }} file{{ modelValue.length != 1 ? "s" : "" }}
|
||||
</p>
|
||||
<table
|
||||
v-if="modelValue && modelValue.length > 0"
|
||||
class="w-full border-1 rounded-2 bg-white text-sm mt-2">
|
||||
<tbody>
|
||||
<tr v-for="file of fileList" :key="file.id">
|
||||
<td class="py-2 pl-2 hidden sm:block relative">
|
||||
<img
|
||||
:src="getFileIconImagePath(file.name || file.title)"
|
||||
class="h-10 text-center" />
|
||||
<div
|
||||
v-if="
|
||||
file.security_type !== undefined &&
|
||||
file.security_type != ''
|
||||
"
|
||||
class="absolute right--1 top-0 h-4 w-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>locked</title>
|
||||
<path
|
||||
d="M18,8C19.097,8 20,8.903 20,10L20,20C20,21.097 19.097,22 18,22L6,22C4.903,22 4,21.097 4,20L4,10C4,8.89 4.9,8 6,8L7,8L7,6C7,3.257 9.257,1 12,1C14.743,1 17,3.257 17,6L17,8L18,8M12,3C10.354,3 9,4.354 9,6L9,8L15,8L15,6C15,4.354 13.646,3 12,3Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</td>
|
||||
<td class="pl-2 py-4 w-full">
|
||||
<a rel="nofollow" :href="file.url" target="_blank">{{
|
||||
file.title || file.name
|
||||
}}</a>
|
||||
<p
|
||||
v-if="
|
||||
file.security_type !== undefined &&
|
||||
file.security_type != ''
|
||||
"
|
||||
class="text-xs color-gray">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="sm:hidden h-3.5 w-3.5 mb--0.5">
|
||||
<title>locked</title>
|
||||
<path
|
||||
d="M18,8C19.097,8 20,8.903 20,10L20,20C20,21.097 19.097,22 18,22L6,22C4.903,22 4,21.097 4,20L4,10C4,8.89 4.9,8 6,8L7,8L7,6C7,3.257 9.257,1 12,1C14.743,1 17,3.257 17,6L17,8L18,8M12,3C10.354,3 9,4.354 9,6L9,8L15,8L15,6C15,4.354 13.646,3 12,3Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
This file requires additional permission or a
|
||||
password to view
|
||||
</p>
|
||||
</td>
|
||||
<td class="pr-2">
|
||||
<a
|
||||
rel="nofollow"
|
||||
:href="addQueryParam(file.url, 'download', '1')"
|
||||
><svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-7 pt-1 text-gray">
|
||||
<path
|
||||
d="M12 10V20M12 20L9.5 17.5M12 20L14.5 17.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.3218 7.05726C7.12925 4.69709 9.36551 3 12 3C14.6345 3 16.8708 4.69709 17.6782 7.05726C19.5643 7.37938 21 9.02203 21 11C21 13.2091 19.2091 15 17 15H16C15.4477 15 15 14.5523 15 14C15 13.4477 15.4477 13 16 13H17C18.1046 13 19 12.1046 19 11C19 9.89543 18.1046 9 17 9C16.9776 9 16.9552 9.00037 16.9329 9.0011C16.4452 9.01702 16.0172 8.67854 15.9202 8.20023C15.5502 6.37422 13.9345 5 12 5C10.0655 5 8.44979 6.37422 8.07977 8.20023C7.98284 8.67854 7.55482 9.01702 7.06706 9.0011C7.04476 9.00037 7.02241 9 7 9C5.89543 9 5 9.89543 5 11C5 12.1046 5.89543 13 7 13H8C8.55228 13 9 13.4477 9 14C9 14.5523 8.55228 15 8 15H7C4.79086 15 3 13.2091 3 11C3 9.02203 4.43567 7.37938 6.3218 7.05726Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
<td v-if="props.showEditor" class="pr-2">
|
||||
<div
|
||||
class="cursor-pointer text-gray hover:text-red"
|
||||
@click.prevent="handleClickDelete(file.id)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-7 pt-1"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Delete</title>
|
||||
<path
|
||||
d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="text-xs text-gray whitespace-nowrap pr-2 py-2 hidden sm:table-cell">
|
||||
({{ bytesReadable(file.size) }})
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button
|
||||
v-if="props.showEditor"
|
||||
type="button"
|
||||
class="font-medium mt-4 px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
@click="handleClickAdd">
|
||||
Add File
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
import { getFileIconImagePath } from "../helpers/utils";
|
||||
import { addQueryParam } from "../helpers/url";
|
||||
import SMHeader from "../components/SMHeader.vue";
|
||||
import { openDialog } from "../components/SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { mediaGetWebURL } from "../helpers/media";
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
showEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const fileList = ref([]);
|
||||
|
||||
/**
|
||||
* Handle the user adding a new media item.
|
||||
*/
|
||||
const handleClickAdd = async () => {
|
||||
if (props.showEditor) {
|
||||
let result = await openDialog(SMDialogMedia, {
|
||||
initial: fileList.value,
|
||||
mime: "",
|
||||
accepts: "",
|
||||
allowUpload: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const mediaResult = result as Media[];
|
||||
let newValue = props.modelValue;
|
||||
let mediaIds = new Set(newValue.map((item) => (item as Media).id));
|
||||
|
||||
mediaResult.forEach((item) => {
|
||||
if (!mediaIds.has(item.id)) {
|
||||
newValue.push(item);
|
||||
mediaIds.add(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
emits("update:modelValue", newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickDelete = (id: string) => {
|
||||
if (props.showEditor == true) {
|
||||
const newList = props.modelValue.filter(
|
||||
(item) => (item as Media).id !== id,
|
||||
);
|
||||
emits("update:modelValue", newList);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
updateFileList(newValue as Array<Media>);
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue !== undefined) {
|
||||
updateFileList(props.modelValue as Array<Media>);
|
||||
}
|
||||
});
|
||||
|
||||
const updateFileList = (newFileList: Array<Media>) => {
|
||||
fileList.value = [];
|
||||
|
||||
for (const mediaItem of newFileList) {
|
||||
mediaItem.url = mediaGetWebURL(mediaItem);
|
||||
if (mediaItem.url != "") {
|
||||
fileList.value.push(mediaItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div v-if="slots.header" class="card-header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div v-if="slots.body || slots.default``" class="card-body">
|
||||
<slot name="body"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="slots.footer" class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
|
||||
const slots = useSlots();
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -1,248 +0,0 @@
|
||||
<template>
|
||||
<div class="sm-checkbox flex flex-col flex-1">
|
||||
<label :class="['control-label-checkbox', ,]" v-bind="{ for: id }"
|
||||
><input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
class="opacity-0 w-0 h-0 select-none"
|
||||
:disabled="disabled"
|
||||
:checked="value"
|
||||
@input="handleCheckbox" />
|
||||
<span
|
||||
:class="[
|
||||
'h-6',
|
||||
'w-6',
|
||||
'rounded',
|
||||
'border-1',
|
||||
'border-gray',
|
||||
'absolute',
|
||||
disabled ? 'bg-gray-2' : 'bg-white',
|
||||
]">
|
||||
<span
|
||||
:class="[
|
||||
'sm-check',
|
||||
'hidden',
|
||||
'absolute',
|
||||
'left-1.5',
|
||||
'top-0.2',
|
||||
'border-r-4',
|
||||
'border-b-4',
|
||||
'h-4',
|
||||
'w-2.5',
|
||||
|
||||
'rotate-45',
|
||||
disabled ? 'border-gray' : 'border-sky-5',
|
||||
]"></span> </span
|
||||
><span
|
||||
:class="[
|
||||
'pl-8',
|
||||
'pt-0.5',
|
||||
'inline-block',
|
||||
disabled ? 'text-gray' : 'text-black',
|
||||
]"
|
||||
>{{ label }}</span
|
||||
></label
|
||||
>
|
||||
<p v-if="slots.default" class="px-2 pt-2 text-xs text-gray-5">
|
||||
<slot></slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, useSlots, inject } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: "",
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: "",
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId(),
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const active = ref(value.value?.toString().length ?? 0 > 0);
|
||||
const focused = ref(false);
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
const handleCheckbox = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.checked;
|
||||
emits("update:modelValue", target.checked);
|
||||
|
||||
if (control) {
|
||||
control.value = target.checked;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
active.value = newValue.toString().length > 0 || focused.value == true;
|
||||
},
|
||||
);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
focused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value?.length ?? 0 > 0;
|
||||
focused.value = false;
|
||||
emits("blur");
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (control) {
|
||||
control.value = target.value;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-checkbox input:checked + span .sm-check {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div :class="['control-group', { 'control-invalid': invalid.length > 0 }]">
|
||||
<div class="control-row">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="!props.noHelp" class="control-help">
|
||||
<span v-if="invalid" class="control-feedback">
|
||||
{{ invalid }}
|
||||
</span>
|
||||
<span v-if="slots.help"><slot name="help"></slot></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, useSlots } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
invalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
noHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const invalid = ref(props.invalid);
|
||||
|
||||
watch(
|
||||
() => props.invalid,
|
||||
(newValue) => {
|
||||
invalid.value = newValue;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.control-group {
|
||||
width: 100%;
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.control-help {
|
||||
display: block;
|
||||
font-size: 70%;
|
||||
min-height: 32px;
|
||||
padding-top: 8px;
|
||||
|
||||
.control-feedback {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
span + span:before {
|
||||
content: "-";
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,124 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
AllowedComponentProps,
|
||||
Component,
|
||||
defineComponent,
|
||||
shallowReactive,
|
||||
VNodeProps,
|
||||
watch,
|
||||
} from "vue";
|
||||
|
||||
export interface DialogInstance {
|
||||
comp?: any;
|
||||
dialog: Component;
|
||||
wrapper: string;
|
||||
props: unknown;
|
||||
resolve: (data: unknown) => void;
|
||||
}
|
||||
const dialogRefs = shallowReactive<DialogInstance[]>([]);
|
||||
|
||||
export default defineComponent({
|
||||
name: "SMDialogList",
|
||||
template: `
|
||||
<div class="dialog-list">
|
||||
<div v-for="(dialogRef, index) in dialogRefList" :key="index" class="dialog-outer">
|
||||
<component
|
||||
:is="dialogRef.dialog"
|
||||
v-if="dialogRef && dialogRef.wrapper === name"
|
||||
v-bind="dialogRef.props"
|
||||
:ref="(ref) => (dialogRef.comp = ref)"></component>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
const dialogRefList = dialogRefs;
|
||||
|
||||
return {
|
||||
name: "default",
|
||||
transitionAttrs: {},
|
||||
dialogRefList,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Closes last opened dialog, resolving the promise with the return value of the dialog, or with the given
|
||||
* data if any.
|
||||
* @param {unknown} data The dialog return value.
|
||||
*/
|
||||
export function closeDialog(data?: unknown) {
|
||||
if (dialogRefs.length <= 1) {
|
||||
document.getElementsByTagName("html")[0].style.overflow = "";
|
||||
document.getElementsByTagName("body")[0].style.overflow = "";
|
||||
}
|
||||
|
||||
const lastDialog = dialogRefs.pop();
|
||||
if (data === undefined && lastDialog.comp && lastDialog.comp.returnValue) {
|
||||
data = lastDialog.comp.returnValue();
|
||||
}
|
||||
if (lastDialog && data !== undefined) {
|
||||
lastDialog.resolve(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the type of props from a component definition.
|
||||
*/
|
||||
type PropsType<C extends Component> = C extends new (...args: any) => any
|
||||
? Omit<
|
||||
InstanceType<C>["$props"],
|
||||
keyof VNodeProps | keyof AllowedComponentProps
|
||||
>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Extracts the return type of the dialog from the setup function.
|
||||
*/
|
||||
type BindingReturnType<C extends Component> = C extends new (
|
||||
...args: any
|
||||
) => any
|
||||
? InstanceType<C> extends { returnValue: () => infer Y }
|
||||
? Y
|
||||
: never
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Extracts the return type of the dialog either from the setup method or from the methods.
|
||||
*/
|
||||
type ReturnType<C extends Component> = BindingReturnType<C>;
|
||||
|
||||
/**
|
||||
* Opens a dialog.
|
||||
* @param {Component} dialog The dialog you want to open.
|
||||
* @param {PropsType} props The props to be passed to the dialog.
|
||||
* @param {string} wrapper The dialog wrapper you want the dialog to open into.
|
||||
* @returns {Promise} A promise that resolves when the dialog is closed
|
||||
*/
|
||||
export function openDialog<C extends Component>(
|
||||
dialog: C,
|
||||
props?: PropsType<C>,
|
||||
wrapper: string = "default",
|
||||
): Promise<ReturnType<C>> {
|
||||
if (dialogRefs.length === 0) {
|
||||
document.getElementsByTagName("html")[0].style.overflow = "hidden";
|
||||
document.getElementsByTagName("body")[0].style.overflow = "hidden";
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
dialogRefs.push({
|
||||
dialog,
|
||||
props,
|
||||
wrapper,
|
||||
resolve,
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
const autofocusElement = document.querySelector(
|
||||
"[autofocus]",
|
||||
) as HTMLInputElement;
|
||||
if (autofocusElement) {
|
||||
autofocusElement.focus();
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
<template>
|
||||
<div class="sm-dropdown flex flex-col flex-1">
|
||||
<div
|
||||
:class="[
|
||||
'relative',
|
||||
'w-full',
|
||||
'flex',
|
||||
{ 'input-active': active || focused },
|
||||
]">
|
||||
<label
|
||||
:for="id"
|
||||
class="absolute select-none pointer-events-none transform-origin-top-left text-gray block translate-x-4 top-2 scale-70 transition"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="absolute right-1 top-1 h-10 pointer-events-none">
|
||||
<path d="M480-360 280-559h400L480-360Z" fill="currentColor" />
|
||||
</svg>
|
||||
<select
|
||||
:class="[
|
||||
'appearance-none',
|
||||
'border-1',
|
||||
'border-gray',
|
||||
'rounded-2',
|
||||
'text-gray-6',
|
||||
'text-lg',
|
||||
'px-4',
|
||||
'pt-5',
|
||||
'flex-1',
|
||||
'bg-white',
|
||||
{ 'bg-gray-1': disabled },
|
||||
]"
|
||||
v-bind="{
|
||||
id: id,
|
||||
}"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
:value="value"
|
||||
:disabled="disabled">
|
||||
<option
|
||||
v-for="option in Object.entries(props.options)"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
:selected="option[0] == value">
|
||||
{{ option[1] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p v-if="slots.default" class="px-2 pt-2 text-xs text-gray-5">
|
||||
<slot></slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, useSlots, inject } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: "",
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: "",
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId(),
|
||||
);
|
||||
const active = ref(value.value?.toString().length ?? 0 > 0);
|
||||
const focused = ref(false);
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
active.value = newValue.toString().length > 0 || focused.value == true;
|
||||
},
|
||||
);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
focused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value?.length ?? 0 > 0;
|
||||
focused.value = false;
|
||||
emits("blur");
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (control) {
|
||||
control.value = target.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-dropdown {
|
||||
select {
|
||||
// appearance: none;
|
||||
// width: 100%;
|
||||
// padding: 20px 16px 8px 14px;
|
||||
// border: 1px solid var(--base-color-darker);
|
||||
// border-radius: 8px;
|
||||
// background-color: var(--base-color-light);
|
||||
// height: 52px;
|
||||
// color: var(--base-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
// label {
|
||||
// --un-translate-y: 0.85rem;
|
||||
// }
|
||||
// .input-active label {
|
||||
// transform: translate(16px, 6px) scale(0.7);
|
||||
// }
|
||||
</style>
|
||||
@@ -1,936 +0,0 @@
|
||||
<template>
|
||||
<div class="sm-html">
|
||||
<bubble-menu
|
||||
:editor="editor"
|
||||
:should-show="bubbleMenuShow"
|
||||
:tippy-options="{ hideOnClick: false }"
|
||||
v-if="editor">
|
||||
<button @click.prevent="setImageSize('small')">small</button>
|
||||
<button @click.prevent="setImageSize('medium')">medium</button>
|
||||
<button @click.prevent="setImageSize('large')">large</button>
|
||||
<button @click.prevent="setImageSize('scaled')">original</button>
|
||||
<button @click.prevent="editor.commands.deleteSelection()">
|
||||
remove
|
||||
</button>
|
||||
</bubble-menu>
|
||||
<div
|
||||
v-if="editor"
|
||||
class="flex flex-wrap bg-white border border-gray rounded-t-2">
|
||||
<div class="flex px-1 relative border-r">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="absolute right-1 top-1.5 h-6 pointer-events-none">
|
||||
<path
|
||||
d="M480-360 280-559h400L480-360Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<select
|
||||
class="appearance-none pl-3 pr-7 text-xs outline-none select-none bg-white"
|
||||
@change="updateNode">
|
||||
<option
|
||||
value="paragraph"
|
||||
:selected="editor.isActive('paragraph')">
|
||||
Paragraph
|
||||
</option>
|
||||
<option value="small" :selected="editor.isActive('small')">
|
||||
Small
|
||||
</option>
|
||||
<option
|
||||
value="h1"
|
||||
:selected="editor.isActive('heading', { level: 1 })">
|
||||
Heading 1
|
||||
</option>
|
||||
<option
|
||||
value="h2"
|
||||
:selected="editor.isActive('heading', { level: 2 })">
|
||||
Heading 2
|
||||
</option>
|
||||
<option
|
||||
value="h3"
|
||||
:selected="editor.isActive('heading', { level: 3 })">
|
||||
Heading 3
|
||||
</option>
|
||||
<option
|
||||
value="h4"
|
||||
:selected="editor.isActive('heading', { level: 4 })">
|
||||
Heading 4
|
||||
</option>
|
||||
<option
|
||||
value="h5"
|
||||
:selected="editor.isActive('heading', { level: 5 })">
|
||||
Heading 5
|
||||
</option>
|
||||
<option
|
||||
value="h6"
|
||||
:selected="editor.isActive('heading', { level: 6 })">
|
||||
Heading 6
|
||||
</option>
|
||||
<option value="info" :selected="editor.isActive('info')">
|
||||
Info
|
||||
</option>
|
||||
<option
|
||||
value="success"
|
||||
:selected="editor.isActive('success')">
|
||||
Success
|
||||
</option>
|
||||
<option
|
||||
value="warning"
|
||||
:selected="editor.isActive('warning')">
|
||||
Warning
|
||||
</option>
|
||||
<option
|
||||
value="danger"
|
||||
:selected="editor.isActive('danger')">
|
||||
Danger
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().toggleBold().run()"
|
||||
:disabled="!editor.can().chain().focus().toggleBold().run()"
|
||||
title="bold"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('bold')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M13.5,15.5H10V12.5H13.5A1.5,1.5 0 0,1 15,14A1.5,1.5 0 0,1 13.5,15.5M10,6.5H13A1.5,1.5 0 0,1 14.5,8A1.5,1.5 0 0,1 13,9.5H10M15.6,10.79C16.57,10.11 17.25,9 17.25,8C17.25,5.74 15.5,4 13.25,4H7V18H14.04C16.14,18 17.75,16.3 17.75,14.21C17.75,12.69 16.89,11.39 15.6,10.79Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().toggleItalic().run()"
|
||||
:disabled="
|
||||
!editor.can().chain().focus().toggleItalic().run()
|
||||
"
|
||||
title="italic"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('italic')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().toggleUnderline().run()
|
||||
"
|
||||
:disabled="
|
||||
!editor.can().chain().focus().toggleUnderline().run()
|
||||
"
|
||||
title="underline"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('underline')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().toggleStrike().run()"
|
||||
:disabled="
|
||||
!editor.can().chain().focus().toggleStrike().run()
|
||||
"
|
||||
title="strike"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('strike')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M23,12V14H18.61C19.61,16.14 19.56,22 12.38,22C4.05,22.05 4.37,15.5 4.37,15.5L8.34,15.55C8.37,18.92 11.5,18.92 12.12,18.88C12.76,18.83 15.15,18.84 15.34,16.5C15.42,15.41 14.32,14.58 13.12,14H1V12H23M19.41,7.89L15.43,7.86C15.43,7.86 15.6,5.09 12.15,5.08C8.7,5.06 9,7.28 9,7.56C9.04,7.84 9.34,9.22 12,9.88H5.71C5.71,9.88 2.22,3.15 10.74,2C19.45,0.8 19.43,7.91 19.41,7.89Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().toggleHighlight().run()
|
||||
"
|
||||
:disabled="
|
||||
!editor.can().chain().focus().toggleHighlight().run()
|
||||
"
|
||||
title="highlight"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('highlight')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M18.5,1.15C17.97,1.15 17.46,1.34 17.07,1.73L11.26,7.55L16.91,13.2L22.73,7.39C23.5,6.61 23.5,5.35 22.73,4.56L19.89,1.73C19.5,1.34 19,1.15 18.5,1.15M10.3,8.5L4.34,14.46C3.56,15.24 3.56,16.5 4.36,17.31C3.14,18.54 1.9,19.77 0.67,21H6.33L7.19,20.14C7.97,20.9 9.22,20.89 10,20.12L15.95,14.16"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().setTextAlign('left').run()
|
||||
"
|
||||
title="align left"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive({ textAlign: 'left' })
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M3,3H21V5H3V3M3,7H15V9H3V7M3,11H21V13H3V11M3,15H15V17H3V15M3,19H21V21H3V19Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().setTextAlign('center').run()
|
||||
"
|
||||
title="align center"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive({ textAlign: 'center' })
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M3,3H21V5H3V3M7,7H17V9H7V7M3,11H21V13H3V11M7,15H17V17H7V15M3,19H21V21H3V19Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().setTextAlign('right').run()
|
||||
"
|
||||
title="align right"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive({ textAlign: 'right' })
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M3,3H21V5H3V3M9,7H21V9H9V7M3,11H21V13H3V11M9,15H21V17H9V15M3,19H21V21H3V19Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().setTextAlign('justify').run()
|
||||
"
|
||||
title="align right"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive({ textAlign: 'justify' })
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="setLink()"
|
||||
title="link"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('link')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M3.9,12C3.9,10.29 5.29,8.9 7,8.9H11V7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12M8,13H16V11H8V13M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.71 18.71,15.1 17,15.1H13V17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().unsetLink().run()"
|
||||
title="unlink"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
'bg-white',
|
||||
'text-gray-6',
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.43 19.12,14.63 17.79,15L19.25,16.44C20.88,15.61 22,13.95 22,12A5,5 0 0,0 17,7M16,11H13.81L15.81,13H16V11M2,4.27L5.11,7.38C3.29,8.12 2,9.91 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12C3.9,10.41 5.11,9.1 6.66,8.93L8.73,11H8V13H10.73L13,15.27V17H14.73L18.74,21L20,19.74L3.27,3L2,4.27Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="setImage()"
|
||||
title="image"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('image')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().toggleBulletList().run()
|
||||
"
|
||||
title="bullet list"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('bulletList')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().toggleOrderedList().run()
|
||||
"
|
||||
title="ordered list"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('orderedList')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M7,13V11H21V13H7M7,19V17H21V19H7M7,7V5H21V7H7M3,8V5H2V4H4V8H3M2,17V16H5V20H2V19H4V18.5H3V17.5H4V17H2M4.25,10A0.75,0.75 0 0,1 5,10.75C5,10.95 4.92,11.14 4.79,11.27L3.12,13H5V14H2V13.08L4,11H2V10H4.25Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().toggleCodeBlock().run()
|
||||
"
|
||||
title="code block"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('codeBlock')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().toggleBlockquote().run()
|
||||
"
|
||||
title="blockquote"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('blockquote')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M10,7L8,11H11V17H5V11L7,7H10M18,7L16,11H19V17H13V11L15,7H18Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().setHorizontalRule().run()
|
||||
"
|
||||
title="horizontal rule"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'bg-white',
|
||||
'hover-bg-gray-3',
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M19,13H5V11H19V13Z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().unsetSuperscript().run();
|
||||
editor.chain().focus().toggleSubscript().run();
|
||||
"
|
||||
title="subscript"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('subscript')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,21.03H16.97V20.03L17.86,19.23C18.62,18.58 19.18,18.04 19.56,17.6C19.93,17.16 20.12,16.75 20.13,16.36C20.14,16.08 20.05,15.85 19.86,15.66C19.68,15.5 19.39,15.38 19,15.38C18.69,15.38 18.42,15.44 18.16,15.56L17.5,15.94L17.05,14.77C17.32,14.56 17.64,14.38 18.03,14.24C18.42,14.1 18.85,14 19.32,14C20.1,14.04 20.7,14.25 21.1,14.66C21.5,15.07 21.72,15.59 21.72,16.23C21.71,16.79 21.53,17.31 21.18,17.78C20.84,18.25 20.42,18.7 19.91,19.14L19.27,19.66V19.68H21.85V21.03Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().unsetSubscript().run();
|
||||
editor.chain().focus().toggleSuperscript().run();
|
||||
"
|
||||
title="Superscript"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
editor.isActive('superscript')
|
||||
? ['bg-sky-6', 'text-white']
|
||||
: ['bg-white', 'text-gray-6'],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,9H16.97V8L17.86,7.18C18.62,6.54 19.18,6 19.56,5.55C19.93,5.11 20.12,4.7 20.13,4.32C20.14,4.04 20.05,3.8 19.86,3.62C19.68,3.43 19.39,3.34 19,3.33C18.69,3.34 18.42,3.4 18.16,3.5L17.5,3.89L17.05,2.72C17.32,2.5 17.64,2.33 18.03,2.19C18.42,2.05 18.85,2 19.32,2C20.1,2 20.7,2.2 21.1,2.61C21.5,3 21.72,3.54 21.72,4.18C21.71,4.74 21.53,5.26 21.18,5.73C20.84,6.21 20.42,6.66 19.91,7.09L19.27,7.61V7.63H21.85V9Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().setHardBreak().run()"
|
||||
title="hard break"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
'bg-white',
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M10,11A4,4 0 0,1 6,7A4,4 0 0,1 10,3H18V5H16V21H14V5H12V21H10V11Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="
|
||||
editor.chain().focus().unsetAllMarks().run();
|
||||
editor.chain().focus().clearNodes().run();
|
||||
"
|
||||
title="Clear formatting"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
'bg-white',
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M6,5V5.18L8.82,8H11.22L10.5,9.68L12.6,11.78L14.21,8H20V5H6M3.27,5L2,6.27L8.97,13.24L6.5,19H9.5L11.07,15.34L16.73,21L18,19.73L3.55,5.27L3.27,5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex p-1 border-r">
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().undo().run()"
|
||||
title="Undo"
|
||||
:disabled="!editor.can().chain().focus().undo().run()"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
'bg-white',
|
||||
[
|
||||
'disabled-text-gray',
|
||||
'hover-disabled-bg-transparent',
|
||||
'disabled-cursor-not-allowed',
|
||||
],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="editor.chain().focus().redo().run()"
|
||||
title="Redo"
|
||||
:disabled="!editor.can().chain().focus().redo().run()"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'p-1',
|
||||
'hover-bg-gray-3',
|
||||
'bg-white',
|
||||
[
|
||||
'disabled-text-gray',
|
||||
'hover-disabled-bg-transparent',
|
||||
'disabled-cursor-not-allowed',
|
||||
],
|
||||
]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EditorContent
|
||||
:editor="editor"
|
||||
class="rounded-b-2 bg-white p-4 border-x border-b border-gray h-128 overflow-auto sm-editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, watch } from "vue";
|
||||
import { useEditor, EditorContent, BubbleMenu, isActive } from "@tiptap/vue-3";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import { Info } from "../extensions/info";
|
||||
import { Success } from "../extensions/success";
|
||||
import { Warning } from "../extensions/warning";
|
||||
import { Danger } from "../extensions/danger";
|
||||
import Subscript from "@tiptap/extension-subscript";
|
||||
import Superscript from "@tiptap/extension-superscript";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import { Small } from "../extensions/small";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { Media, MediaCollection } from "../helpers/api.types";
|
||||
import { api } from "../helpers/api";
|
||||
import { extractFileNameFromUrl } from "../helpers/url";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
TextAlign.configure({
|
||||
types: [
|
||||
"heading",
|
||||
"paragraph",
|
||||
"info",
|
||||
"success",
|
||||
"warning",
|
||||
"danger",
|
||||
],
|
||||
}),
|
||||
Highlight,
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Danger,
|
||||
Small,
|
||||
Subscript,
|
||||
Superscript,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
Image,
|
||||
BubbleMenu,
|
||||
],
|
||||
onUpdate: () => {
|
||||
emits("update:modelValue", editor.value.getHTML());
|
||||
},
|
||||
});
|
||||
|
||||
const bubbleMenuShow = ({ editor, view, state, oldState, from, to }) => {
|
||||
return isActive(state, "image");
|
||||
};
|
||||
|
||||
const updateNode = (event) => {
|
||||
if (event.target.value) {
|
||||
switch (event.target.value) {
|
||||
case "paragraph":
|
||||
editor.value.chain().focus().setParagraph().run();
|
||||
break;
|
||||
case "small":
|
||||
editor.value.chain().focus().setSmall().run();
|
||||
break;
|
||||
case "h1":
|
||||
editor.value.chain().focus().setHeading({ level: 1 }).run();
|
||||
break;
|
||||
case "h2":
|
||||
editor.value.chain().focus().setHeading({ level: 2 }).run();
|
||||
break;
|
||||
case "h3":
|
||||
editor.value.chain().focus().setHeading({ level: 3 }).run();
|
||||
break;
|
||||
case "h4":
|
||||
editor.value.chain().focus().setHeading({ level: 4 }).run();
|
||||
break;
|
||||
case "h5":
|
||||
editor.value.chain().focus().setHeading({ level: 5 }).run();
|
||||
break;
|
||||
case "h6":
|
||||
editor.value.chain().focus().setHeading({ level: 6 }).run();
|
||||
break;
|
||||
case "info":
|
||||
editor.value.chain().focus().toggleInfo().run();
|
||||
break;
|
||||
case "success":
|
||||
editor.value.chain().focus().toggleSuccess().run();
|
||||
break;
|
||||
case "warning":
|
||||
editor.value.chain().focus().toggleWarning().run();
|
||||
break;
|
||||
case "danger":
|
||||
editor.value.chain().focus().toggleDanger().run();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.value.getAttributes("link").href;
|
||||
const url = window.prompt("URL", previousUrl);
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === "") {
|
||||
editor.value.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
return;
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.value
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange("link")
|
||||
.setLink({ href: url })
|
||||
.run();
|
||||
};
|
||||
|
||||
const setImage = async () => {
|
||||
let result = await openDialog(SMDialogMedia, {
|
||||
allowUpload: true,
|
||||
allowUrl: true,
|
||||
});
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
editor.value
|
||||
.chain()
|
||||
.focus()
|
||||
.setImage({
|
||||
src: mediaResult.url,
|
||||
title: mediaResult.title,
|
||||
alt: mediaResult.description,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value.destroy();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
const isSame = editor.value.getHTML() === newValue;
|
||||
|
||||
if (isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.value.commands.setContent(newValue, false);
|
||||
},
|
||||
);
|
||||
|
||||
const getImageSize = async () => {
|
||||
let size = "default";
|
||||
|
||||
if (!editor.value.view.state.selection.node) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const src = editor.value.view.state.selection.node.attrs.src;
|
||||
const fileName = extractFileNameFromUrl(src);
|
||||
|
||||
let r = await api
|
||||
.get({
|
||||
url: "/media",
|
||||
params: {
|
||||
variants: extractFileNameFromUrl(src),
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
const data = result.data as MediaCollection;
|
||||
if (data.media.length > 0 && data.media[0].variants) {
|
||||
for (const [key, value] of Object.entries(
|
||||
data.media[0].variants,
|
||||
)) {
|
||||
if (value === fileName) {
|
||||
size = key;
|
||||
console.log(size);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("final", size);
|
||||
return size;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
return "xx";
|
||||
});
|
||||
|
||||
console.log(r);
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
const setImageSize = (size: string): void => {
|
||||
const { selection } = editor.value.view.state;
|
||||
const src = editor.value.view.state.selection.node.attrs.src;
|
||||
|
||||
api.get({
|
||||
url: "/media",
|
||||
params: {
|
||||
variants: extractFileNameFromUrl(src),
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
/*
|
||||
large
|
||||
medium
|
||||
scaled
|
||||
small,
|
||||
thumb
|
||||
xlarge
|
||||
xxlarge
|
||||
*/
|
||||
const newSrc = mediaGetVariantUrl(result.data.media[0], size);
|
||||
const transaction = editor.value.view.state.tr.setNodeMarkup(
|
||||
selection.from,
|
||||
undefined,
|
||||
{ src: newSrc },
|
||||
);
|
||||
editor.value.view.dispatch(transaction);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tippy-content div {
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
appearance: none;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(60, 60, 60, 1);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .tippy-arrow {
|
||||
// height: 0.75rem;
|
||||
// width: 0.75rem;
|
||||
// z-index: -1;
|
||||
|
||||
// &::after {
|
||||
// display: block;
|
||||
// content: "";
|
||||
// background-color: rgba(0, 0, 0, 1);
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
// transform: translateY(-50%) rotate(45deg);
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
@@ -1,269 +0,0 @@
|
||||
<template>
|
||||
<router-link
|
||||
rel="prefetch"
|
||||
:to="{ name: 'event', params: { id: props.event.id } }"
|
||||
class="event-card bg-white border-1 border-rounded-xl text-black decoration-none hover:shadow-md transition min-w-72">
|
||||
<div
|
||||
class="h-48 bg-cover bg-center rounded-t-xl relative"
|
||||
:style="{
|
||||
backgroundImage: `url('${mediaGetVariantUrl(
|
||||
props.event.hero,
|
||||
'medium',
|
||||
)}')`,
|
||||
}">
|
||||
<div
|
||||
:class="[
|
||||
'absolute',
|
||||
'top-2',
|
||||
'right-2',
|
||||
'text-xs',
|
||||
'font-bold',
|
||||
'uppercase',
|
||||
'px-4',
|
||||
'py-1',
|
||||
computedBanner(props.event)['bg-class'],
|
||||
computedBanner(props.event)['font-class'],
|
||||
]">
|
||||
{{ computedBanner(props.event)["banner"] }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center bg-white border-1 border-rounded absolute top-2 left-2 w-12 h-12 text-gray-6">
|
||||
<div class="font-bold line-height-none">
|
||||
{{ formatDateDay(props.event.start_at) }}
|
||||
</div>
|
||||
<div class="text-xs uppercase line-height-none">
|
||||
{{ formatDateMonth(props.event.start_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-3 font-500">{{ props.event.title }}</h3>
|
||||
<div class="flex items-center mb-2 text-gray-5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 pr-2"
|
||||
viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Zm300 230q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<span class="text-sm">{{ computedDate(props.event) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-2 text-gray-5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 pr-2"
|
||||
viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M480.089-490Q509-490 529.5-510.589q20.5-20.588 20.5-49.5Q550-589 529.411-609.5q-20.588-20.5-49.5-20.5Q451-630 430.5-609.411q-20.5 20.588-20.5 49.5Q410-531 430.589-510.5q20.588 20.5 49.5 20.5ZM480-159q133-121 196.5-219.5T740-552q0-117.79-75.292-192.895Q589.417-820 480-820t-184.708 75.105Q220-669.79 220-552q0 75 65 173.5T480-159Zm0 79Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Zm0-472Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<span class="text-sm">
|
||||
{{ computedLocation(props.event) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center mb-2 text-gray-5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 pr-2"
|
||||
viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M626-533q22.5 0 38.25-15.75T680-587q0-22.5-15.75-38.25T626-641q-22.5 0-38.25 15.75T572-587q0 22.5 15.75 38.25T626-533Zm-292 0q22.5 0 38.25-15.75T388-587q0-22.5-15.75-38.25T334-641q-22.5 0-38.25 15.75T280-587q0 22.5 15.75 38.25T334-533Zm146 272q66 0 121.5-35.5T682-393H278q26 61 81 96.5T480-261Zm0 181q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 340q142.375 0 241.188-98.812Q820-337.625 820-480t-98.812-241.188Q622.375-820 480-820t-241.188 98.812Q140-622.375 140-480t98.812 241.188Q337.625-140 480-140Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<span class="text-sm">
|
||||
{{ computedAges(props.event.ages) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center text-gray-5">
|
||||
<span class="block text-center w-4 mr-2">$</span>
|
||||
<span class="text-sm">
|
||||
{{ computedPrice(props.event.price) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Event } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
|
||||
const props = defineProps({
|
||||
event: {
|
||||
type: Object as () => Event,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a human readable Date string.
|
||||
* @param {Event} event The event to convert.
|
||||
* @returns The converted string.
|
||||
*/
|
||||
const computedDate = (event: Event) => {
|
||||
let str = "";
|
||||
|
||||
const start_at =
|
||||
event.start_at.length > 0
|
||||
? new SMDate(event.start_at, {
|
||||
format: "yMd",
|
||||
utc: true,
|
||||
}).format("dd/MM/yyyy @ h:mm aa")
|
||||
: "";
|
||||
|
||||
const end_at =
|
||||
event.end_at.length > 0
|
||||
? new SMDate(event.end_at, {
|
||||
format: "yMd",
|
||||
utc: true,
|
||||
}).format("dd/MM/yyyy")
|
||||
: "";
|
||||
|
||||
if (start_at.length > 0) {
|
||||
if (
|
||||
end_at.length > 0 &&
|
||||
start_at.substring(0, start_at.indexOf(" ")) != end_at
|
||||
) {
|
||||
str = start_at.substring(0, start_at.indexOf(" ")) + " - " + end_at;
|
||||
} else {
|
||||
str = start_at;
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a the event starting month day number.
|
||||
* @param {string} date The date to format.
|
||||
* @returns The converted string.
|
||||
*/
|
||||
const formatDateDay = (date: string) => {
|
||||
return new SMDate(date, { format: "yMd", utc: true }).format("dd");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a the event starting month name.
|
||||
* @param {string} date The date to format.
|
||||
* @returns The converted string.
|
||||
*/
|
||||
const formatDateMonth = (date: string) => {
|
||||
return new SMDate(date, { format: "yMd", utc: true }).format("MMM");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a human readable Location string.
|
||||
* @param {Event} event The event to convert.
|
||||
* @returns The converted string.
|
||||
*/
|
||||
const computedLocation = (event: Event): string => {
|
||||
if (event.location == "online") {
|
||||
return "Online";
|
||||
}
|
||||
|
||||
return event.address;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a human readable Ages string.
|
||||
* @param {string} ages The string to convert.
|
||||
* @returns The converted string.
|
||||
*/
|
||||
const computedAges = (ages: string): string => {
|
||||
const trimmed = ages.trim();
|
||||
const regex = /^(\d+)(\s*\+?\s*|\s*-\s*\d+\s*)?$/;
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
return "All ages";
|
||||
}
|
||||
|
||||
if (regex.test(trimmed)) {
|
||||
return `Ages ${trimmed}`;
|
||||
}
|
||||
|
||||
return ages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a human readable Price string.
|
||||
* @param {string} price The string to convert.
|
||||
* @returns The converted string.
|
||||
*/
|
||||
const computedPrice = (price: string): string => {
|
||||
if (price.toLowerCase() === "tbd" || price.toLowerCase() === "tbc") {
|
||||
return price.toUpperCase();
|
||||
}
|
||||
|
||||
const trimmed = parseInt(price.trim());
|
||||
if (isNaN(trimmed) || trimmed == 0) {
|
||||
return "Free";
|
||||
}
|
||||
|
||||
return trimmed.toString();
|
||||
};
|
||||
|
||||
type EventBanner = {
|
||||
banner: string;
|
||||
"bg-class": string;
|
||||
"font-class": string;
|
||||
};
|
||||
|
||||
const computedBanner = (event: Event): EventBanner => {
|
||||
const parsedEndAt = new SMDate(event.end_at, {
|
||||
format: "yyyy-MM-dd HH:mm:ss",
|
||||
utc: true,
|
||||
});
|
||||
|
||||
if (
|
||||
(parsedEndAt.isBefore(new SMDate("now")) &&
|
||||
(event.status == "open" ||
|
||||
event.status == "soon" ||
|
||||
event.status == "full")) ||
|
||||
event.status == "closed"
|
||||
) {
|
||||
return {
|
||||
banner: "closed",
|
||||
"bg-class": "bg-purple-800",
|
||||
"font-class": "text-white",
|
||||
};
|
||||
} else if (event.status == "full") {
|
||||
return {
|
||||
banner: "full",
|
||||
"bg-class": "bg-purple-800",
|
||||
"font-class": "text-white",
|
||||
};
|
||||
} else if (event.status == "open") {
|
||||
return {
|
||||
banner: "open",
|
||||
"bg-class": "bg-green-700",
|
||||
"font-class": "text-white",
|
||||
};
|
||||
} else if (event.status == "cancelled") {
|
||||
return {
|
||||
banner: "cancelled",
|
||||
"bg-class": "bg-red-700",
|
||||
"font-class": "text-white",
|
||||
};
|
||||
} else if (event.status == "draft") {
|
||||
return {
|
||||
banner: "draft",
|
||||
"bg-class": "bg-purple-800",
|
||||
"font-class": "text-white",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
banner: "Open Soon",
|
||||
"bg-class": "bg-yellow-400",
|
||||
"font-class": "text-black",
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.event-card {
|
||||
color: inherit !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<a :href="computedUrl" :target="props.target" rel="noopener"
|
||||
><slot></slot
|
||||
></a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: "_self",
|
||||
},
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
/**
|
||||
* Return the URL with a token param attached if the user is logged in and its a api media download request.
|
||||
*/
|
||||
const computedUrl = computed(() => {
|
||||
const url = new URL(props.href);
|
||||
const path = url.pathname;
|
||||
const mediumRegex = /^\/media\/[a-zA-Z0-9]+\/download$/;
|
||||
|
||||
if (mediumRegex.test(path) && userStore.token) {
|
||||
if (url.search) {
|
||||
return `${props.href}&token=${encodeURIComponent(userStore.token)}`;
|
||||
} else {
|
||||
return `${props.href}?token=${encodeURIComponent(userStore.token)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return props.href;
|
||||
});
|
||||
</script>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<form :id="id" @submit.prevent="submit">
|
||||
<slot></slot>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, watch } from "vue";
|
||||
import { generateRandomElementId } from "../helpers/utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["submit", "failedValidation"]);
|
||||
const id = generateRandomElementId();
|
||||
let inputs = [];
|
||||
|
||||
watch(
|
||||
() => props.modelValue.loading(),
|
||||
(status) => {
|
||||
if (!status) {
|
||||
enableFormInputs();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle the user submitting the form.
|
||||
*/
|
||||
const submit = async function () {
|
||||
try {
|
||||
inputs = Array.from(document.querySelectorAll(`#${id} input`));
|
||||
|
||||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||||
const input = inputs[i] as HTMLInputElement;
|
||||
if (!input.disabled) {
|
||||
input.disabled = true;
|
||||
} else {
|
||||
inputs.splice(i, 1);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
if (await props.modelValue.validate()) {
|
||||
emits("submit", () => {
|
||||
enableFormInputs();
|
||||
});
|
||||
} else {
|
||||
emits("failedValidation");
|
||||
enableFormInputs();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reenable form inputs
|
||||
*/
|
||||
const enableFormInputs = () => {
|
||||
for (const input of inputs) {
|
||||
const typedInput = input as HTMLInputElement;
|
||||
typedInput.disabled = false;
|
||||
}
|
||||
|
||||
inputs = [];
|
||||
};
|
||||
|
||||
provide(props.formId, props.modelValue);
|
||||
defineExpose({ submit });
|
||||
</script>
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div class="form-error" v-if="props.modelValue._message.length > 0">
|
||||
<ion-icon class="invalid-icon" name="alert-circle-outline"></ion-icon>
|
||||
<p>{{ props.modelValue._message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form-error {
|
||||
display: flex;
|
||||
color: var(--danger-color);
|
||||
background-color: var(--danger-color-lighter);
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 24px;
|
||||
align-items: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<component :is="computedContent"></component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { computed } from "vue";
|
||||
import { ImportMetaExtras } from "../../../import-meta";
|
||||
// import SMImageGallery from "./SMImageGallery.vue";
|
||||
|
||||
const props = defineProps({
|
||||
html: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the html as a component, relative links as router-link and sanitized.
|
||||
*/
|
||||
const computedContent = computed(() => {
|
||||
let html = "";
|
||||
|
||||
// Sanitize HTML
|
||||
html = DOMPurify.sanitize(props.html);
|
||||
|
||||
// Convert nl to <br>
|
||||
html = html.replaceAll("\n", "<br />");
|
||||
|
||||
// Convert local links to router-links
|
||||
const regexHref = new RegExp(
|
||||
`<a ([^>]*?)href="${
|
||||
(import.meta as ImportMetaExtras).env.APP_URL
|
||||
}(.*?>.*?)</a>`,
|
||||
"ig",
|
||||
);
|
||||
html = html.replace(regexHref, '<router-link $1to="$2</router-link>');
|
||||
|
||||
// Convert image galleries to SMImageGallery component
|
||||
// const regexGallery =
|
||||
// /<div.*?class="tinymce-gallery".*?>\s*((?:<div class="tinymce-gallery-item" style="background-image: url\('.*?'\);">.*?<\/div>\s*)*)<\/div>/gi;
|
||||
|
||||
// const matches = [...html.matchAll(regexGallery)];
|
||||
// for (const match of matches) {
|
||||
// const images = match[1]; // Extract the captured group from the match
|
||||
// const imageSrcs = images
|
||||
// .match(/style="background-image: url\('(.*?)'\)/gi)
|
||||
// .map((m) => m.match(/background-image: url\('(.*?)'\)/i)[1]);
|
||||
// const smImageGallery = `<SMImageGallery :images='${JSON.stringify(
|
||||
// imageSrcs
|
||||
// )}' />`;
|
||||
// html = html.replace(images, smImageGallery);
|
||||
// }
|
||||
|
||||
// Update local images to use at most the large size
|
||||
const regexImg = new RegExp(
|
||||
`<img ([^>]*?)src="${
|
||||
(import.meta as ImportMetaExtras).env.APP_URL
|
||||
}/uploads/([^"]*?)"`,
|
||||
"ig",
|
||||
);
|
||||
html = html.replace(
|
||||
regexImg,
|
||||
`<img $1src="${
|
||||
(import.meta as ImportMetaExtras).env.APP_URL
|
||||
}/uploads/$2?size=large"`,
|
||||
);
|
||||
|
||||
return { template: `<div class="sm-html">${html}</div>` };
|
||||
});
|
||||
</script>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<component
|
||||
:is="`h${props.size}`"
|
||||
:id="id"
|
||||
:class="['sm-header', props.noCopy ? '' : 'cursor-pointer']"
|
||||
@click.prevent="copyAnchor">
|
||||
{{ props.text }}
|
||||
<span v-if="!props.noCopy" class="pl-2 text-sky-5 opacity-75 hidden"
|
||||
>#</span
|
||||
>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useToastStore } from "../store/ToastStore";
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
required: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
noCopy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const computedHeaderId = (text: string): string => {
|
||||
return text.replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase();
|
||||
};
|
||||
|
||||
const id = ref(
|
||||
props.id && props.id.length > 0 ? props.id : computedHeaderId(props.text),
|
||||
);
|
||||
|
||||
const copyAnchor = () => {
|
||||
if (props.noCopy === false) {
|
||||
const currentUrl = window.location.href.replace(/#.*/, "");
|
||||
const newUrl = currentUrl + "#" + id.value;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(newUrl)
|
||||
.then(() => {
|
||||
useToastStore().addToast({
|
||||
title: "Copied to Clipboard",
|
||||
content:
|
||||
"The heading URL has been copied to the clipboard.",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
useToastStore().addToast({
|
||||
title: "Copy to Clipboard",
|
||||
content: "Failed to copy the heading URL to the clipboard.",
|
||||
type: "danger",
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-header:hover span {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div class="image">
|
||||
<SMLoading
|
||||
v-if="props.src != '' && imgLoaded == false && imgError == false" />
|
||||
<img
|
||||
v-if="props.src != '' && imgError == false"
|
||||
:src="src"
|
||||
@load="imgLoaded = true"
|
||||
@error="imgError = true" />
|
||||
<div v-if="imgError == true" class="image-error">
|
||||
<ion-icon name="alert-circle-outline"></ion-icon>
|
||||
<p>Error loading image</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import SMLoading from "./SMLoading.vue";
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const imgLoaded = ref(false);
|
||||
const imgError = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image {
|
||||
display: flex;
|
||||
flex-basis: 300px;
|
||||
|
||||
/* Firefox */
|
||||
justify-content: center;
|
||||
max-height: 300px;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
|
||||
ion-icon {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,364 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="[
|
||||
'flex',
|
||||
'gap-4',
|
||||
'my-4',
|
||||
'select-none',
|
||||
props.showEditor
|
||||
? ['overflow-auto']
|
||||
: ['flex-wrap', 'flex-justify-center'],
|
||||
]">
|
||||
<div
|
||||
v-for="(image, index) in modelValue"
|
||||
class="flex flex-col flex-justify-center relative sm-gallery-item p-1"
|
||||
:key="index">
|
||||
<img
|
||||
:src="mediaGetThumbnail(image as Media, 'small')"
|
||||
class="max-h-40 max-w-40 cursor-pointer"
|
||||
@click="showGalleryModal(index)" />
|
||||
<div
|
||||
v-if="props.showEditor"
|
||||
class="absolute rounded-5 bg-white -top-0.25 -right-0.25 hidden cursor-pointer item-delete"
|
||||
@click="handleRemoveItem((image as Media).id)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 block"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
|
||||
fill="rgba(185,28,28,1)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.showEditor"
|
||||
class="flex flex-col flex-justify-center">
|
||||
<div
|
||||
class="flex flex-col flex-justify-center flex-items-center h-23 w-40 cursor-pointer bg-gray-300 text-gray-800 hover:text-gray-600"
|
||||
@click="handleAddToGallery">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-15 w-15"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Add image</title>
|
||||
<path
|
||||
d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.showEditor == false && showModalImage !== null"
|
||||
:class="[
|
||||
'image-gallery-modal',
|
||||
{ 'image-gallery-modal-buttons': showButtons },
|
||||
]"
|
||||
@click="hideModal"
|
||||
@mousemove="handleModalUpdateButtons"
|
||||
@mouseleave="handleModalUpdateButtons">
|
||||
<img
|
||||
:src="mediaGetVariantUrl(modelValue[showModalImage] as Media)"
|
||||
class="image-gallery-modal-image" />
|
||||
<div
|
||||
class="image-gallery-modal-prev"
|
||||
@click.stop="handleModalPrevImage"></div>
|
||||
<div
|
||||
class="image-gallery-modal-next"
|
||||
@click.stop="handleModalNextImage"></div>
|
||||
<div class="image-gallery-modal-close" @click="hideModal">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Close</title>
|
||||
<path
|
||||
d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
import { mediaGetThumbnail, mediaGetVariantUrl } from "../helpers/media";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
showEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const showModalImage = ref(null);
|
||||
let showButtons = ref(false);
|
||||
let mouseMoveTimeout = null;
|
||||
|
||||
const showGalleryModal = (index) => {
|
||||
showModalImage.value = index;
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
|
||||
const hideModal = () => {
|
||||
showModalImage.value = null;
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
handleModalPrevImage();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
handleModalNextImage();
|
||||
} else if (event.key === "Escape") {
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalUpdateButtons = () => {
|
||||
if (mouseMoveTimeout !== null) {
|
||||
clearTimeout(mouseMoveTimeout);
|
||||
mouseMoveTimeout = null;
|
||||
}
|
||||
|
||||
showButtons.value = true;
|
||||
mouseMoveTimeout = setTimeout(() => {
|
||||
showButtons.value = false;
|
||||
mouseMoveTimeout = null;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handleModalPrevImage = () => {
|
||||
handleModalUpdateButtons();
|
||||
|
||||
if (showModalImage.value !== null) {
|
||||
if (showModalImage.value > 0) {
|
||||
showModalImage.value--;
|
||||
} else {
|
||||
showModalImage.value = props.modelValue.length - 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalNextImage = () => {
|
||||
handleModalUpdateButtons();
|
||||
|
||||
if (showModalImage.value !== null) {
|
||||
if (showModalImage.value < props.modelValue.length - 1) {
|
||||
showModalImage.value++;
|
||||
} else {
|
||||
showModalImage.value = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToGallery = async () => {
|
||||
let result = await openDialog(SMDialogMedia, {
|
||||
allowUpload: true,
|
||||
multiple: true,
|
||||
initial: props.modelValue,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const mediaResult = result as Media[];
|
||||
let newValue = props.modelValue;
|
||||
let galleryIds = new Set(mediaResult.map((item) => (item as Media).id));
|
||||
|
||||
mediaResult.forEach((item) => {
|
||||
if (!galleryIds.has(item.id)) {
|
||||
newValue.push(item);
|
||||
galleryIds.add(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
emits("update:modelValue", mediaResult);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveItem = async (id: string) => {
|
||||
const newList = props.modelValue.filter((item) => item.id !== id);
|
||||
emits("update:modelValue", newList);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// .image-gallery {
|
||||
// display: grid;
|
||||
// grid-template-columns: 1fr 1fr;
|
||||
// gap: 15px;
|
||||
|
||||
// .image-gallery-image {
|
||||
// cursor: pointer;
|
||||
// max-width: 100%;
|
||||
// max-height: 100%;
|
||||
// object-fit: contain;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media (min-width: 768px) {
|
||||
// .image-gallery {
|
||||
// grid-template-columns: 1fr 1fr 1fr;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @media (min-width: 1024px) {
|
||||
// .image-gallery {
|
||||
// grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
// }
|
||||
// }
|
||||
|
||||
.image-gallery-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 5000;
|
||||
|
||||
.image-gallery-modal-image {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.image-gallery-modal-buttons {
|
||||
.image-gallery-modal-prev,
|
||||
.image-gallery-modal-next {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-modal-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 150%;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-modal-prev,
|
||||
.image-gallery-modal-next {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
content: "";
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 75px;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: #999;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-modal-prev {
|
||||
left: 0;
|
||||
|
||||
&::after {
|
||||
border-left: 2px solid black;
|
||||
border-bottom: 2px solid black;
|
||||
transform: rotateZ(45deg) translateX(2px) translateY(-2px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: rotateZ(45deg) translateX(-0.5px) translateY(0.5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-modal-next {
|
||||
right: 0;
|
||||
|
||||
&::after {
|
||||
border-right: 2px solid black;
|
||||
border-top: 2px solid black;
|
||||
transform: rotateZ(45deg) translateX(-2px) translateY(2px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: rotateZ(45deg) translateX(0.5px) translateY(-0.5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sm-gallery-item:hover .item-delete {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
<template>
|
||||
<div class="sm-image-stack-container">
|
||||
<div
|
||||
:class="[
|
||||
'sm-image-stack',
|
||||
{ 'sm-image-stack-hover': frontImage !== -1 },
|
||||
]"
|
||||
:style="{
|
||||
height: 300 + props.src.length * 20 + 'px',
|
||||
width: 533 + props.src.length * 40 + 'px',
|
||||
}"
|
||||
@mouseout="handleHover(-1)">
|
||||
<div
|
||||
v-for="(source, index) in props.src"
|
||||
:key="index"
|
||||
:style="{
|
||||
top: (index + 1) * 20 + 'px',
|
||||
left: index * 40 + 'px',
|
||||
'background-image': `url('${source}')`,
|
||||
}"
|
||||
:class="['image', { hover: frontImage == index }]"
|
||||
@mouseover="handleHover(index)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const frontImage = ref(-1);
|
||||
|
||||
const handleHover = (index) => {
|
||||
frontImage.value = index;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-image-stack-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sm-image-stack {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
&.sm-image-stack-hover {
|
||||
.image {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hover {
|
||||
opacity: 1 !important;
|
||||
z-index: 1;
|
||||
top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
height: 300px;
|
||||
width: 533px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--base-shadow);
|
||||
// box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,882 +0,0 @@
|
||||
<template>
|
||||
<SMControl
|
||||
:class="[
|
||||
'control-type-input',
|
||||
{
|
||||
'input-active': active,
|
||||
'has-prepend': slots.prepend,
|
||||
'has-append': slots.append,
|
||||
},
|
||||
props.size,
|
||||
]"
|
||||
:invalid="feedbackInvalid"
|
||||
:no-help="props.noHelp">
|
||||
<div v-if="slots.prepend" class="input-control-prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<template v-if="props.type == 'checkbox'">
|
||||
<label
|
||||
:class="[
|
||||
'control-label',
|
||||
'control-label-checkbox',
|
||||
{ disabled: disabled },
|
||||
]"
|
||||
v-bind="{ for: id }"
|
||||
><input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
class="checkbox-control"
|
||||
:disabled="disabled"
|
||||
:checked="value"
|
||||
@input="handleCheckbox" />
|
||||
<span class="checkbox-control-box">
|
||||
<span class="checkbox-control-tick"></span> </span
|
||||
>{{ label }}</label
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'range'">
|
||||
<label
|
||||
class="control-label control-label-range"
|
||||
v-bind="{ for: id }"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
type="range"
|
||||
class="range-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
step: props.step,
|
||||
}"
|
||||
:value="value"
|
||||
@input="handleInput" />
|
||||
<span class="range-control-value">{{ value }}</span>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'select'">
|
||||
<label
|
||||
class="control-label control-label-select"
|
||||
v-bind="{ for: id }"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<ion-icon
|
||||
class="select-dropdown-icon"
|
||||
name="caret-down-outline" />
|
||||
<select
|
||||
class="select-input-control"
|
||||
:disabled="disabled"
|
||||
@input="handleInput">
|
||||
<option
|
||||
v-for="option in Object.entries(props.options)"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
:selected="option[0] == value">
|
||||
{{ option[1] }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="control-label" v-bind="{ for: id }">{{
|
||||
label
|
||||
}}</label>
|
||||
<template v-if="props.type == 'static'">
|
||||
<div class="static-input-control" v-bind="{ id: id }">
|
||||
<span class="text">
|
||||
{{ value }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'file'">
|
||||
<input
|
||||
:id="id"
|
||||
type="file"
|
||||
class="file-input-control"
|
||||
:accept="props.accept"
|
||||
:disabled="disabled"
|
||||
@change="handleChange" />
|
||||
<div class="file-input-control-value">
|
||||
{{ value?.name ? value.name : value }}
|
||||
</div>
|
||||
<label
|
||||
:class="[
|
||||
'button',
|
||||
'primary',
|
||||
'file-input-control-button',
|
||||
{ disabled: disabled },
|
||||
]"
|
||||
:for="id"
|
||||
>Select file</label
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'textarea'">
|
||||
<ion-icon
|
||||
class="invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<textarea
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||
v-model="value"
|
||||
rows="5"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup"></textarea>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'media'">
|
||||
<div class="media-input-control">
|
||||
<img
|
||||
v-if="mediaUrl?.length > 0"
|
||||
:src="mediaGetVariantUrl(value, 'medium')" />
|
||||
<ion-icon v-else name="image-outline" />
|
||||
<!-- <SMButton
|
||||
size="medium"
|
||||
:disabled="disabled"
|
||||
@click="handleMediaSelect"
|
||||
label="Select File" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ion-icon
|
||||
class="invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<ion-icon
|
||||
v-if="
|
||||
props.showClear &&
|
||||
value?.length > 0 &&
|
||||
!feedbackInvalid
|
||||
"
|
||||
class="clear-icon"
|
||||
name="close-outline"
|
||||
@click.stop="handleClear"></ion-icon>
|
||||
|
||||
<input
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{
|
||||
id: id,
|
||||
autofocus: props.autofocus,
|
||||
autocomplete:
|
||||
props.type === 'email' ? 'email' : null,
|
||||
spellcheck: props.type === 'email' ? false : null,
|
||||
autocorrect: props.type === 'email' ? 'on' : null,
|
||||
autocapitalize:
|
||||
props.type === 'email' ? 'off' : null,
|
||||
}"
|
||||
v-model="value"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup" />
|
||||
<ul
|
||||
class="autocomplete-list"
|
||||
v-if="computedAutocompleteItems.length > 0 && focused">
|
||||
<li
|
||||
v-for="item in computedAutocompleteItems"
|
||||
:key="item"
|
||||
@mousedown="handleAutocompleteClick(item)">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="slots.append" class="input-control-append">
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
<template v-if="slots.help" #help><slot name="help"></slot></template>
|
||||
</SMControl>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watch, ref, useSlots, computed } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import SMControl from "./SMControl.vue";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
button: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
autocomplete: {
|
||||
type: [Array<string>, Function],
|
||||
default: () => {
|
||||
[];
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: ""
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: ""
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId()
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const active = ref(value.value?.toString().length ?? 0 > 0);
|
||||
const focused = ref(false);
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
if (props.type === "media") {
|
||||
mediaUrl.value = value.value.url ?? "";
|
||||
}
|
||||
|
||||
active.value =
|
||||
newValue.toString().length > 0 ||
|
||||
newValue instanceof File ||
|
||||
focused.value == true;
|
||||
}
|
||||
);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
watch(
|
||||
() => form.loading(),
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mediaUrl = ref(value.value.url ?? "");
|
||||
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
focused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value?.length ?? 0 > 0;
|
||||
focused.value = false;
|
||||
emits("blur");
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckbox = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.checked;
|
||||
emits("update:modelValue", target.checked);
|
||||
|
||||
if (control) {
|
||||
control.value = target.checked;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (control) {
|
||||
control.value = target.value;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (event: Event) => {
|
||||
emits("keyup", event);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
value.value = "";
|
||||
emits("update:modelValue", "");
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
if (control) {
|
||||
control.value = event.target.files[0];
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelect = async () => {
|
||||
let result = await openDialog(SMDialogMedia);
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
mediaUrl.value = mediaResult.url;
|
||||
emits("update:modelValue", mediaResult);
|
||||
|
||||
if (control) {
|
||||
control.value = mediaResult;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const computedAutocompleteItems = computed(() => {
|
||||
let autocompleteList = [];
|
||||
|
||||
if (props.autocomplete) {
|
||||
if (typeof props.autocomplete === "function") {
|
||||
autocompleteList = props.autocomplete(value.value);
|
||||
} else {
|
||||
autocompleteList = props.autocomplete.filter((str) =>
|
||||
str.includes(value.value)
|
||||
);
|
||||
}
|
||||
|
||||
return autocompleteList.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
return autocompleteList;
|
||||
});
|
||||
|
||||
const handleAutocompleteClick = (item) => {
|
||||
value.value = item;
|
||||
emits("update:modelValue", item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.input-control-prepend {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
& + .control-item .input-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
height: 50px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-item {
|
||||
max-width: 100%;
|
||||
align-items: start;
|
||||
|
||||
.control-label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
transform-origin: top left;
|
||||
transform: translate(16px, 16px) scale(1);
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: var(--base-color-darker);
|
||||
pointer-events: none;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.invalid-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 10px;
|
||||
top: 14px;
|
||||
color: var(--danger-color);
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 18px;
|
||||
background-color: var(--input-clear-icon-color);
|
||||
border-radius: 50%;
|
||||
font-size: 80%;
|
||||
padding: 1px 1px 1px 0px;
|
||||
|
||||
&:hover {
|
||||
color: var(--input-clear-icon-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 20px 16px 10px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--base-color-text);
|
||||
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 92%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--primary-color);
|
||||
z-index: 1;
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
margin: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.static-input-control {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control {
|
||||
opacity: 0;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
}
|
||||
|
||||
.file-input-control-value {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control-button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 30px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.control-label-select {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
|
||||
.select-dropdown-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.select-input-control {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding: 20px 16px 8px 14px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
height: 52px;
|
||||
color: var(--base-color-text);
|
||||
}
|
||||
|
||||
.control-label-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0 16px 32px;
|
||||
pointer-events: all;
|
||||
transform: none;
|
||||
color: var(--base-color-text);
|
||||
|
||||
&.disabled {
|
||||
color: var(--base-color-darker);
|
||||
cursor: not-allowed;
|
||||
|
||||
.checkbox-control-box {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + .checkbox-control-box {
|
||||
.checkbox-control-tick {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control-box {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 2px;
|
||||
background-color: var(--base-color-light);
|
||||
|
||||
.checkbox-control-tick {
|
||||
position: absolute;
|
||||
display: none;
|
||||
border-right: 3px solid var(--base-color-text);
|
||||
border-bottom: 3px solid var(--base-color-text);
|
||||
top: 1px;
|
||||
left: 7px;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-input-control {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
img,
|
||||
ion-icon {
|
||||
display: block;
|
||||
margin: 48px auto 8px auto;
|
||||
border-radius: 8px;
|
||||
font-size: 800%;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-label-range {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.range-control {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.range-control-value {
|
||||
margin-top: 22px;
|
||||
padding-left: 16px;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-append .control-item .input-control {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
&.input-active {
|
||||
.control-item {
|
||||
.control-label:not(.control-label-checkbox) {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.control-invalid {
|
||||
.control-row .control-item {
|
||||
.invalid-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-control {
|
||||
border: 2px solid var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
&.input-active {
|
||||
.control-row .control-item .control-label {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.control-row {
|
||||
.control-item {
|
||||
.control-label {
|
||||
transform: translate(16px, 12px) scale(1);
|
||||
}
|
||||
|
||||
.input-control {
|
||||
padding: 16px 8px 4px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
.button {
|
||||
.button-label {
|
||||
ion-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
height: 36px;
|
||||
padding: 3px 24px 13px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.control-item {
|
||||
.input-control {
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,315 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'sm-input',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'flex-1',
|
||||
{ 'sm-input-small': small },
|
||||
]">
|
||||
<div
|
||||
:class="[
|
||||
'relative',
|
||||
'w-full',
|
||||
'flex',
|
||||
{ 'input-active': active || focused },
|
||||
]">
|
||||
<label
|
||||
:for="id"
|
||||
:class="[
|
||||
'absolute',
|
||||
'select-none',
|
||||
'pointer-events-none',
|
||||
'transform-origin-top-left',
|
||||
'text-gray',
|
||||
'block',
|
||||
'scale-100',
|
||||
'transition',
|
||||
small
|
||||
? ['translate-x-4', 'text-sm', '-top-1.5']
|
||||
: ['translate-x-5', 'top-0.5'],
|
||||
]"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<template v-if="!props.textarea">
|
||||
<input
|
||||
:type="props.type"
|
||||
:class="[
|
||||
'w-full',
|
||||
'text-gray-6',
|
||||
'flex-1',
|
||||
small
|
||||
? ['text-sm', 'pt-3', 'px-3']
|
||||
: ['text-lg', 'pt-5', 'px-4'],
|
||||
feedbackInvalid ? 'border-red-6' : 'border-gray',
|
||||
feedbackInvalid ? 'border-2' : 'border-1',
|
||||
{ 'bg-gray-1': disabled },
|
||||
{ 'rounded-l-2': !slots.prepend },
|
||||
{ 'rounded-r-2': !slots.append },
|
||||
]"
|
||||
v-bind="{
|
||||
id: id,
|
||||
autofocus: props.autofocus,
|
||||
autocomplete: props.type === 'email' ? 'email' : null,
|
||||
spellcheck: props.type === 'email' ? false : null,
|
||||
autocorrect: props.type === 'email' ? 'on' : null,
|
||||
autocapitalize: props.type === 'email' ? 'off' : null,
|
||||
}"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup"
|
||||
:value="value"
|
||||
:disabled="disabled" />
|
||||
<template v-if="slots.append"
|
||||
><slot name="append"></slot
|
||||
></template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<textarea
|
||||
:class="[
|
||||
'w-full',
|
||||
'text-gray-6',
|
||||
'flex-1',
|
||||
small
|
||||
? ['text-sm', 'pt-3', 'px-3']
|
||||
: ['text-lg', 'pt-5', 'px-4'],
|
||||
feedbackInvalid ? 'border-red-6' : 'border-gray',
|
||||
feedbackInvalid ? 'border-2' : 'border-1',
|
||||
{ 'bg-gray-1': disabled },
|
||||
{ 'rounded-l-2': !slots.prepend },
|
||||
{ 'rounded-r-2': !slots.append },
|
||||
]"
|
||||
v-bind="{
|
||||
id: id,
|
||||
autofocus: props.autofocus,
|
||||
autocomplete: props.type === 'email' ? 'email' : null,
|
||||
spellcheck: props.type === 'email' ? false : null,
|
||||
autocorrect: props.type === 'email' ? 'on' : null,
|
||||
autocapitalize: props.type === 'email' ? 'off' : null,
|
||||
}"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup"
|
||||
:value="value"
|
||||
:disabled="disabled"></textarea>
|
||||
</template>
|
||||
</div>
|
||||
<p v-if="feedbackInvalid" class="px-2 pt-2 text-xs text-red-6">
|
||||
{{ feedbackInvalid }}
|
||||
</p>
|
||||
<p v-if="slots.default" class="px-2 pt-2 text-xs text-gray-5">
|
||||
<slot></slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, useSlots, inject } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
textarea: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: "",
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: "",
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId(),
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const active = ref(value.value?.toString().length ?? 0 > 0);
|
||||
const focused = ref(false);
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
active.value = newValue.toString().length > 0 || focused.value == true;
|
||||
},
|
||||
);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
focused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value?.length ?? 0 > 0;
|
||||
focused.value = false;
|
||||
emits("blur");
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (control) {
|
||||
control.value = target.value;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (event: Event) => {
|
||||
emits("keyup", event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-input {
|
||||
label {
|
||||
--un-translate-y: 0.85rem;
|
||||
}
|
||||
.input-active label {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
&.sm-input-small .input-active label {
|
||||
transform: translate(12px, 7px) scale(0.7);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,202 +0,0 @@
|
||||
<template>
|
||||
<div class="input-attachments">
|
||||
<ul>
|
||||
<li v-if="mediaItems.length == 0" class="attachments-none">
|
||||
<ion-icon name="sad-outline"></ion-icon>
|
||||
<p>No attachments</p>
|
||||
</li>
|
||||
<li v-for="media of mediaItems" :key="media.id">
|
||||
<div class="attachment-media-icon">
|
||||
<img
|
||||
:src="getFilePreview(media.url)"
|
||||
height="48"
|
||||
width="48" />
|
||||
</div>
|
||||
<div class="attachment-media-name">
|
||||
{{ media.title || media.name }}
|
||||
</div>
|
||||
<div class="attachment-media-size">
|
||||
({{ bytesReadable(media.size) }})
|
||||
</div>
|
||||
<div class="attachment-media-remove">
|
||||
<ion-icon
|
||||
name="close-outline"
|
||||
title="Remove attachment"
|
||||
@click="handleClickRemove(media.id)" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<button type="button" @click="handleClickAdd">Add media</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref, watch } from "vue";
|
||||
import { openDialog } from "../components/SMDialog";
|
||||
import { api } from "../helpers/api";
|
||||
import { Media, MediaResponse } from "../helpers/api.types";
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
import { getFilePreview } from "../helpers/utils";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array<string>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
const value: Ref<string[]> = ref(props.modelValue);
|
||||
const mediaItems: Ref<Media[]> = ref([]);
|
||||
|
||||
/**
|
||||
* Handle the user adding a new media item.
|
||||
*/
|
||||
const handleClickAdd = async () => {
|
||||
openDialog(SMDialogMedia, { mime: "", accepts: "", allowUpload: true })
|
||||
.then((result) => {
|
||||
const media = result as Media;
|
||||
|
||||
mediaItems.value.push(media);
|
||||
value.value.push(media.id);
|
||||
|
||||
emits("update:modelValue", value.value);
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle removing a media item from the attachment array.
|
||||
* @param {string} media_id The media id to remove.
|
||||
*/
|
||||
const handleClickRemove = (media_id: string) => {
|
||||
const index = value.value.indexOf(media_id);
|
||||
if (index !== -1) {
|
||||
value.value.splice(index, 1);
|
||||
}
|
||||
|
||||
const mediaIndex = mediaItems.value.findIndex(
|
||||
(media) => media.id === media_id,
|
||||
);
|
||||
if (mediaIndex !== -1) {
|
||||
mediaItems.value.splice(mediaIndex, 1);
|
||||
}
|
||||
|
||||
emits("update:modelValue", value.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the attachment list
|
||||
*/
|
||||
const handleLoad = () => {
|
||||
mediaItems.value = [];
|
||||
|
||||
if (value.value && typeof value.value.forEach === "function") {
|
||||
value.value.forEach((item) => {
|
||||
api.get({
|
||||
url: `/media/${item}`,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data) {
|
||||
const data = result.data as MediaResponse;
|
||||
|
||||
mediaItems.value.push(data.medium);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
handleLoad();
|
||||
},
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// .input-attachments {
|
||||
// display: block;
|
||||
|
||||
// label {
|
||||
// position: relative;
|
||||
// display: block;
|
||||
// padding: 8px 16px 0 16px;
|
||||
// color: var(--base-color);
|
||||
// }
|
||||
|
||||
// a.button {
|
||||
// display: inline-block;
|
||||
// }
|
||||
|
||||
// ul {
|
||||
// list-style-type: none;
|
||||
// padding: 0;
|
||||
// border: 1px solid var(--base-color-border);
|
||||
|
||||
// li {
|
||||
// background-color: var(--base-color-light);
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// padding: 16px;
|
||||
// margin: 0;
|
||||
|
||||
// &.attachments-none {
|
||||
// justify-content: center;
|
||||
|
||||
// ion-icon {
|
||||
// font-size: 115%;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// margin: 0;
|
||||
// padding-left: #{map-get($spacing, 2)};
|
||||
// }
|
||||
// }
|
||||
|
||||
// .attachment-media-icon {
|
||||
// display: flex;
|
||||
// width: 64px;
|
||||
// justify-content: center;
|
||||
// }
|
||||
|
||||
// .attachment-media-name {
|
||||
// flex: 1;
|
||||
// }
|
||||
|
||||
// .attachment-media-size {
|
||||
// font-size: 75%;
|
||||
// padding-left: #{map-get($spacing, 2)};
|
||||
// color: var(--base-color-dark);
|
||||
// }
|
||||
|
||||
// .attachment-media-remove {
|
||||
// font-size: 115%;
|
||||
// padding-top: #{map-get($spacing, 1)};
|
||||
// margin-left: #{map-get($spacing, 3)};
|
||||
// color: var(--base-color-text);
|
||||
// cursor: pointer;
|
||||
|
||||
// &:hover {
|
||||
// color: var(--danger-color);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
@@ -1,882 +0,0 @@
|
||||
<template>
|
||||
<SMControl
|
||||
:class="[
|
||||
'control-type-input',
|
||||
{
|
||||
'input-active': active,
|
||||
'has-prepend': slots.prepend,
|
||||
'has-append': slots.append,
|
||||
},
|
||||
props.size,
|
||||
]"
|
||||
:invalid="feedbackInvalid"
|
||||
:no-help="props.noHelp">
|
||||
<div v-if="slots.prepend" class="input-control-prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<template v-if="props.type == 'checkbox'">
|
||||
<label
|
||||
:class="[
|
||||
'control-label',
|
||||
'control-label-checkbox',
|
||||
{ disabled: disabled },
|
||||
]"
|
||||
v-bind="{ for: id }"
|
||||
><input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
class="checkbox-control"
|
||||
:disabled="disabled"
|
||||
:checked="value"
|
||||
@input="handleCheckbox" />
|
||||
<span class="checkbox-control-box">
|
||||
<span class="checkbox-control-tick"></span> </span
|
||||
>{{ label }}</label
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'range'">
|
||||
<label
|
||||
class="control-label control-label-range"
|
||||
v-bind="{ for: id }"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
type="range"
|
||||
class="range-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
step: props.step,
|
||||
}"
|
||||
:value="value"
|
||||
@input="handleInput" />
|
||||
<span class="range-control-value">{{ value }}</span>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'select'">
|
||||
<label
|
||||
class="control-label control-label-select"
|
||||
v-bind="{ for: id }"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<ion-icon
|
||||
class="select-dropdown-icon"
|
||||
name="caret-down-outline" />
|
||||
<select
|
||||
class="select-input-control"
|
||||
:disabled="disabled"
|
||||
@input="handleInput">
|
||||
<option
|
||||
v-for="option in Object.entries(props.options)"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
:selected="option[0] == value">
|
||||
{{ option[1] }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="control-label" v-bind="{ for: id }">{{
|
||||
label
|
||||
}}</label>
|
||||
<template v-if="props.type == 'static'">
|
||||
<div class="static-input-control" v-bind="{ id: id }">
|
||||
<span class="text">
|
||||
{{ value }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'file'">
|
||||
<input
|
||||
:id="id"
|
||||
type="file"
|
||||
class="file-input-control"
|
||||
:accept="props.accept"
|
||||
:disabled="disabled"
|
||||
@change="handleChange" />
|
||||
<div class="file-input-control-value">
|
||||
{{ value?.name ? value.name : value }}
|
||||
</div>
|
||||
<label
|
||||
:class="[
|
||||
'button',
|
||||
'primary',
|
||||
'file-input-control-button',
|
||||
{ disabled: disabled },
|
||||
]"
|
||||
:for="id"
|
||||
>Select file</label
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'textarea'">
|
||||
<ion-icon
|
||||
class="invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<textarea
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||
v-model="value"
|
||||
rows="5"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup"></textarea>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'media'">
|
||||
<div class="media-input-control">
|
||||
<img
|
||||
v-if="mediaUrl?.length > 0"
|
||||
:src="mediaGetVariantUrl(value, 'medium')" />
|
||||
<ion-icon v-else name="image-outline" />
|
||||
<!-- <SMButton
|
||||
size="medium"
|
||||
:disabled="disabled"
|
||||
@click="handleMediaSelect"
|
||||
label="Select File" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ion-icon
|
||||
class="invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<ion-icon
|
||||
v-if="
|
||||
props.showClear &&
|
||||
value?.length > 0 &&
|
||||
!feedbackInvalid
|
||||
"
|
||||
class="clear-icon"
|
||||
name="close-outline"
|
||||
@click.stop="handleClear"></ion-icon>
|
||||
|
||||
<input
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{
|
||||
id: id,
|
||||
autofocus: props.autofocus,
|
||||
autocomplete:
|
||||
props.type === 'email' ? 'email' : null,
|
||||
spellcheck: props.type === 'email' ? false : null,
|
||||
autocorrect: props.type === 'email' ? 'on' : null,
|
||||
autocapitalize:
|
||||
props.type === 'email' ? 'off' : null,
|
||||
}"
|
||||
v-model="value"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup" />
|
||||
<ul
|
||||
class="autocomplete-list"
|
||||
v-if="computedAutocompleteItems.length > 0 && focused">
|
||||
<li
|
||||
v-for="item in computedAutocompleteItems"
|
||||
:key="item"
|
||||
@mousedown="handleAutocompleteClick(item)">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="slots.append" class="input-control-append">
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
<template v-if="slots.help" #help><slot name="help"></slot></template>
|
||||
</SMControl>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watch, ref, useSlots, computed } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import SMControl from "./SMControl.vue";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
button: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
autocomplete: {
|
||||
type: [Array<string>, Function],
|
||||
default: () => {
|
||||
[];
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: ""
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: ""
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId()
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const active = ref(value.value?.toString().length ?? 0 > 0);
|
||||
const focused = ref(false);
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
if (props.type === "media") {
|
||||
mediaUrl.value = value.value.url ?? "";
|
||||
}
|
||||
|
||||
active.value =
|
||||
newValue.toString().length > 0 ||
|
||||
newValue instanceof File ||
|
||||
focused.value == true;
|
||||
}
|
||||
);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
watch(
|
||||
() => form.loading(),
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mediaUrl = ref(value.value.url ?? "");
|
||||
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
focused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value?.length ?? 0 > 0;
|
||||
focused.value = false;
|
||||
emits("blur");
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckbox = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.checked;
|
||||
emits("update:modelValue", target.checked);
|
||||
|
||||
if (control) {
|
||||
control.value = target.checked;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (control) {
|
||||
control.value = target.value;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (event: Event) => {
|
||||
emits("keyup", event);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
value.value = "";
|
||||
emits("update:modelValue", "");
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
if (control) {
|
||||
control.value = event.target.files[0];
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelect = async () => {
|
||||
let result = await openDialog(SMDialogMedia);
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
mediaUrl.value = mediaResult.url;
|
||||
emits("update:modelValue", mediaResult);
|
||||
|
||||
if (control) {
|
||||
control.value = mediaResult;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const computedAutocompleteItems = computed(() => {
|
||||
let autocompleteList = [];
|
||||
|
||||
if (props.autocomplete) {
|
||||
if (typeof props.autocomplete === "function") {
|
||||
autocompleteList = props.autocomplete(value.value);
|
||||
} else {
|
||||
autocompleteList = props.autocomplete.filter((str) =>
|
||||
str.includes(value.value)
|
||||
);
|
||||
}
|
||||
|
||||
return autocompleteList.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
return autocompleteList;
|
||||
});
|
||||
|
||||
const handleAutocompleteClick = (item) => {
|
||||
value.value = item;
|
||||
emits("update:modelValue", item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.input-control-prepend {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
& + .control-item .input-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
height: 50px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-item {
|
||||
max-width: 100%;
|
||||
align-items: start;
|
||||
|
||||
.control-label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
transform-origin: top left;
|
||||
transform: translate(16px, 16px) scale(1);
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: var(--base-color-darker);
|
||||
pointer-events: none;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.invalid-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 10px;
|
||||
top: 14px;
|
||||
color: var(--danger-color);
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 18px;
|
||||
background-color: var(--input-clear-icon-color);
|
||||
border-radius: 50%;
|
||||
font-size: 80%;
|
||||
padding: 1px 1px 1px 0px;
|
||||
|
||||
&:hover {
|
||||
color: var(--input-clear-icon-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 20px 16px 10px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--base-color-text);
|
||||
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 92%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--primary-color);
|
||||
z-index: 1;
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
margin: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.static-input-control {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control {
|
||||
opacity: 0;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
}
|
||||
|
||||
.file-input-control-value {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control-button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 30px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.control-label-select {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
|
||||
.select-dropdown-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.select-input-control {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding: 20px 16px 8px 14px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
height: 52px;
|
||||
color: var(--base-color-text);
|
||||
}
|
||||
|
||||
.control-label-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0 16px 32px;
|
||||
pointer-events: all;
|
||||
transform: none;
|
||||
color: var(--base-color-text);
|
||||
|
||||
&.disabled {
|
||||
color: var(--base-color-darker);
|
||||
cursor: not-allowed;
|
||||
|
||||
.checkbox-control-box {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + .checkbox-control-box {
|
||||
.checkbox-control-tick {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control-box {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 2px;
|
||||
background-color: var(--base-color-light);
|
||||
|
||||
.checkbox-control-tick {
|
||||
position: absolute;
|
||||
display: none;
|
||||
border-right: 3px solid var(--base-color-text);
|
||||
border-bottom: 3px solid var(--base-color-text);
|
||||
top: 1px;
|
||||
left: 7px;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-input-control {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
img,
|
||||
ion-icon {
|
||||
display: block;
|
||||
margin: 48px auto 8px auto;
|
||||
border-radius: 8px;
|
||||
font-size: 800%;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-label-range {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.range-control {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.range-control-value {
|
||||
margin-top: 22px;
|
||||
padding-left: 16px;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-append .control-item .input-control {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
&.input-active {
|
||||
.control-item {
|
||||
.control-label:not(.control-label-checkbox) {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.control-invalid {
|
||||
.control-row .control-item {
|
||||
.invalid-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-control {
|
||||
border: 2px solid var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
&.input-active {
|
||||
.control-row .control-item .control-label {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.control-row {
|
||||
.control-item {
|
||||
.control-label {
|
||||
transform: translate(16px, 12px) scale(1);
|
||||
}
|
||||
|
||||
.input-control {
|
||||
padding: 16px 8px 4px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
.button {
|
||||
.button-label {
|
||||
ion-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
height: 36px;
|
||||
padding: 3px 24px 13px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.control-item {
|
||||
.input-control {
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-items-center justify-center">
|
||||
<div :class="['spinner', { small: props.small }]"></div>
|
||||
<div v-if="slots.default" :class="['mt-3', { small: props.small }]">
|
||||
<slot name="default"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const props = defineProps({
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.spinner {
|
||||
width: 12rem;
|
||||
height: 12rem;
|
||||
border: 2rem solid transparent;
|
||||
border-top-color: #00a5f1;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.1);
|
||||
border-left-color: rgba(0, 0, 0, 0.1);
|
||||
border-right-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: spinner-rotation 8s ease-in-out infinite;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.small {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-width: 0.5rem;
|
||||
margin: 0 1.5rem 0 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner-rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
border-top-color: #eb3594;
|
||||
}
|
||||
20% {
|
||||
transform: rotate(360deg);
|
||||
border-top-color: #00a5f1;
|
||||
}
|
||||
40% {
|
||||
transform: rotate(720deg);
|
||||
border-top-color: #39b54a;
|
||||
}
|
||||
60% {
|
||||
transform: rotate(1080deg);
|
||||
border-top-color: #f79e1c;
|
||||
}
|
||||
80% {
|
||||
transform: rotate(1440deg);
|
||||
border-top-color: #e11e26;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(1800deg);
|
||||
border-top-color: #eb3594;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-sky-500 text-white">
|
||||
<div class="max-w-7xl mx-auto flex flex-col pt-10 px-4">
|
||||
<div class="pb-12">
|
||||
<h1 class="text-4xl">{{ title }}</h1>
|
||||
<router-link
|
||||
class="sm-masthead-backlink text-sm"
|
||||
v-if="props.backLink !== null"
|
||||
:to="props.backLink"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="h-3">
|
||||
<path
|
||||
d="M400-80 0-480l400-400 56 57-343 343 343 343-56 57Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
{{ props.backTitle }}</router-link
|
||||
>
|
||||
<p
|
||||
class="sm-masthead-info text-sm max-w-lg pt-2 text-sky-2"
|
||||
v-if="slots.default">
|
||||
<slot></slot>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="tabs().length > 0"
|
||||
class="block text-right overflow-x-auto whitespace-nowrap scroll-smooth scrollbar-width-none"
|
||||
style="scrollbar-width: none">
|
||||
<router-link
|
||||
:to="tab.to"
|
||||
v-for="(tab, idx) in tabs()"
|
||||
:key="idx"
|
||||
class="inline-block decoration-none !text-sky-1 px-6 py-4 font-bold hover:bg-sky-400 rounded-t-2"
|
||||
exact-active-class="!bg-gray-1 !text-sky-500"
|
||||
>{{ tab.title }}</router-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
backLink: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return null;
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
backTitle: {
|
||||
type: String,
|
||||
default: "Back",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const tabGroups = [
|
||||
[
|
||||
{ title: "Contact", to: "/contact" },
|
||||
{ title: "Code of Conduct", to: "/code-of-conduct" },
|
||||
{ title: "Rules", to: "/rules" },
|
||||
{ title: "Terms and Conditions", to: "/terms-and-conditions" },
|
||||
{ title: "Privacy", to: "/privacy" },
|
||||
],
|
||||
[
|
||||
{ title: "Connect", to: "/minecraft" },
|
||||
{ title: "Curve Calculator", to: "/minecraft/curve" },
|
||||
],
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const tabs = () => {
|
||||
const currentTabGroup = tabGroups.find((items) =>
|
||||
items.some((item) => item.to === route.path)
|
||||
);
|
||||
|
||||
return currentTabGroup || [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-masthead-info a,
|
||||
.sm-masthead-backlink {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,717 +0,0 @@
|
||||
<template>
|
||||
<nav id="navbar" :class="{ 'is-open': isNavVisible }">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="max-w-7xl flex flex-row items-center mx-auto px-4 py-2 gap-2">
|
||||
<router-link :to="{ name: 'home' }">
|
||||
<svg
|
||||
id="navbar-logo"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 2762 491"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xml:space="preserve"
|
||||
xmlns:serif="http://www.serif.com/"
|
||||
style="
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 10;
|
||||
"
|
||||
alt="STEMMechanics">
|
||||
<g>
|
||||
<g>
|
||||
<g id="g7146">
|
||||
<g id="g7154"></g>
|
||||
<g id="g7158"></g>
|
||||
<g id="g7162">
|
||||
<g id="g7164">
|
||||
<g id="g7188"></g>
|
||||
<g id="g7192">
|
||||
<path
|
||||
id="path7194"
|
||||
d="M520.706,133.507l0,263.567l32.794,-0l0,-257.505c0,-3.348 -2.714,-6.062 -6.061,-6.062l-26.733,-0Z"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7196">
|
||||
<path
|
||||
id="path7198"
|
||||
d="M56.5,139.568l-0,257.506l32.794,0l-0,-263.568l-26.733,0c-3.348,0 -6.061,2.714 -6.061,6.062"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7200">
|
||||
<path
|
||||
id="path7202"
|
||||
d="M553.5,216.404l-0,-76.836c-0,-3.348 -2.714,-6.061 -6.061,-6.061l-26.733,0l-0,263.567l32.794,0l-0,-145.67"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7204">
|
||||
<path
|
||||
id="path7206"
|
||||
d="M89.294,216.404l-0,-82.897l-26.733,0c-3.348,0 -6.061,2.713 -6.061,6.061l-0,257.506l32.794,0l-0,-145.67"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7208">
|
||||
<path
|
||||
id="path7210"
|
||||
d="M112.715,74.519l55.15,259.463l39.438,-8.383l-55.15,-259.463l-39.438,8.383Z"
|
||||
style="
|
||||
fill: #f89d00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7212">
|
||||
<path
|
||||
id="path7214"
|
||||
d="M112.715,74.519l8.464,39.818l39.437,-8.382l-8.463,-39.819l-39.438,8.383Z"
|
||||
style="
|
||||
fill: #d58700;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7216">
|
||||
<path
|
||||
id="path7218"
|
||||
d="M207.304,325.599l-55.151,-259.463l-39.437,8.383l55.15,259.463"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7220">
|
||||
<path
|
||||
id="path7222"
|
||||
d="M78.998,49.805l-32.558,6.92c-1.252,0.267 -2.484,-0.533 -2.75,-1.786l-1.96,-9.222c-0.291,-1.369 -1.636,-2.242 -3.005,-1.952l-24.19,5.143c-1.369,0.29 -2.242,1.635 -1.951,3.003l11.328,53.293c0.291,1.37 1.636,2.243 3.004,1.952l24.19,-5.142c1.369,-0.291 2.243,-1.636 1.952,-3.005l-1.961,-9.222c-0.266,-1.253 0.534,-2.484 1.786,-2.751l32.558,-6.919l-6.443,-30.312Z"
|
||||
style="
|
||||
fill: #b7cee9;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7224">
|
||||
<path
|
||||
id="path7226"
|
||||
d="M78.998,49.805l-32.558,6.92c-1.252,0.267 -2.484,-0.533 -2.75,-1.786l-1.96,-9.222c-0.291,-1.369 -1.636,-2.242 -3.005,-1.952l-24.19,5.143c-1.369,0.29 -2.242,1.635 -1.951,3.003l11.328,53.293c0.291,1.37 1.636,2.243 3.004,1.952l24.19,-5.142c1.369,-0.291 2.243,-1.636 1.952,-3.005l-1.961,-9.222c-0.266,-1.253 0.534,-2.484 1.786,-2.751l32.558,-6.919l-6.443,-30.312Z"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7228">
|
||||
<path
|
||||
id="path7230"
|
||||
d="M164.359,17.061l-91.655,19.481c-3.116,0.663 -5.105,3.726 -4.443,6.843l9.982,46.963c0.663,3.116 3.726,5.107 6.843,4.443l110.229,-23.429c4.739,-1.008 9.577,1.218 11.896,5.472l8.86,16.253c1.528,2.802 5.038,3.835 7.84,2.308l7.786,-3.384c7.942,-4.329 11.079,-13.3 6.748,-21.242l-14.286,-26.203c-11.658,-21.383 -35.978,-32.568 -59.8,-27.505"
|
||||
style="
|
||||
fill: #d7e3f2;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g72281" serif:id="g7228">
|
||||
<path
|
||||
id="path72301"
|
||||
serif:id="path7230"
|
||||
d="M164.359,17.061l-91.655,19.481c-3.116,0.663 -5.105,3.726 -4.443,6.843l9.982,46.963c0.663,3.116 3.726,5.107 6.843,4.443l110.229,-23.429c4.739,-1.008 9.577,1.218 11.896,5.472l8.86,16.253c1.528,2.802 5.038,3.835 7.84,2.308l7.786,-3.384c7.942,-4.329 11.079,-13.3 6.748,-21.242l-14.286,-26.203c-11.658,-21.383 -35.978,-32.568 -59.8,-27.505"
|
||||
style="
|
||||
fill: #d7e3f2;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7232">
|
||||
<path
|
||||
id="path7234"
|
||||
d="M102.999,30.103l-30.294,6.439c-3.117,0.663 -5.106,3.726 -4.444,6.843l9.983,46.963c0.662,3.116 3.726,5.107 6.842,4.443l110.229,-23.429c4.739,-1.008 9.578,1.218 11.896,5.472l8.86,16.253c1.528,2.802 5.038,3.835 7.84,2.308l7.786,-3.384c7.943,-4.329 11.079,-13.3 6.749,-21.242l-14.287,-26.203c-11.658,-21.383 -35.977,-32.568 -59.8,-27.505l-26.359,5.603"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7240"></g>
|
||||
<g id="g7244">
|
||||
<path
|
||||
id="path7246"
|
||||
d="M553.5,295.445l-497,0l-0,172.631c-0,3.905 3.166,7.07 7.071,7.07l482.858,0c3.905,0 7.071,-3.165 7.071,-7.07l-0,-172.631Z"
|
||||
style="
|
||||
fill: #e00000;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7248">
|
||||
<path
|
||||
id="path7250"
|
||||
d="M546.429,444.992l-482.857,0c-3.906,0 -7.072,-3.166 -7.072,-7.071l-0,30.155c-0,3.905 3.166,7.07 7.072,7.07l482.857,0c3.905,0 7.071,-3.165 7.071,-7.07l-0,-30.155c-0,3.905 -3.166,7.071 -7.071,7.071"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7252">
|
||||
<path
|
||||
id="path7254"
|
||||
d="M553.5,295.445l-497,0l-0,172.631c-0,3.905 3.166,7.07 7.071,7.07l482.858,0c3.905,0 7.071,-3.165 7.071,-7.07l-0,-172.631Z"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7256">
|
||||
<path
|
||||
id="path7258"
|
||||
d="M249.87,347.278l-0,33c-0,2.762 2.239,5 5,5l100.261,0c2.761,0 5,-2.238 5,-5l-0,-33c-0,-2.762 -2.239,-5 -5,-5l-100.261,0c-2.761,0 -5,2.238 -5,5"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7260">
|
||||
<path
|
||||
id="path7262"
|
||||
d="M249.87,347.278l-0,33c-0,2.762 2.239,5 5,5l100.261,0c2.761,0 5,-2.238 5,-5l-0,-33c-0,-2.762 -2.239,-5 -5,-5l-100.261,0c-2.761,0 -5,2.238 -5,5Z"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
id="path7148"
|
||||
x="89.294"
|
||||
y="133.507"
|
||||
width="431.412"
|
||||
height="36.366"
|
||||
style="
|
||||
fill: #7d8c97;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<clipPath id="_clip1">
|
||||
<rect
|
||||
x="48.391"
|
||||
y="1"
|
||||
width="554.191"
|
||||
height="287.599" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<clipPath id="_clip2">
|
||||
<polygon
|
||||
points="122.46,223.269 389.191,141.565 470.894,408.297 204.163,490 122.46,223.269 " />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip2)">
|
||||
<path
|
||||
d="M401.112,309.199l20.426,-6.257c5.753,-1.762 8.987,-7.856 7.222,-13.609l-7.905,-25.756c-1.764,-5.748 -7.853,-8.979 -13.602,-7.218l-20.445,6.262c-5.67,-9.632 -12.768,-18.137 -20.928,-25.318l10.016,-18.862c2.822,-5.314 0.8,-11.909 -4.515,-14.729l-23.799,-12.627c-5.313,-2.819 -11.904,-0.798 -14.724,4.512l-10.021,18.874c-10.513,-2.725 -21.525,-3.831 -32.67,-3.127l-6.26,-20.436c-1.762,-5.751 -7.852,-8.985 -13.603,-7.223l-25.76,7.89c-5.752,1.762 -8.986,7.852 -7.224,13.604l6.26,20.435c-9.628,5.659 -18.132,12.743 -25.314,20.889l-18.874,-10.022c-5.311,-2.82 -11.903,-0.803 -14.725,4.508l-12.644,23.789c-2.824,5.313 -0.805,11.909 4.509,14.731l18.862,10.016c-2.738,10.52 -3.855,21.541 -3.158,32.697l-20.446,6.262c-5.748,1.761 -8.983,7.848 -7.225,13.598l7.876,25.764c1.76,5.755 7.852,8.992 13.605,7.23l20.427,-6.257c5.665,9.653 12.762,18.178 20.925,25.376l-10.022,18.873c-2.821,5.311 -0.803,11.903 4.507,14.725l23.79,12.645c5.314,2.823 11.909,0.805 14.73,-4.509l10.017,-18.862c10.542,2.744 21.588,3.86 32.768,3.153l6.26,20.436c1.762,5.751 7.852,8.985 13.603,7.223l25.76,-7.89c5.752,-1.762 8.986,-7.852 7.224,-13.603l-6.26,-20.436c9.658,-5.677 18.185,-12.788 25.381,-20.965l18.862,10.016c5.314,2.822 11.909,0.8 14.729,-4.515l12.627,-23.8c2.819,-5.312 0.798,-11.903 -4.513,-14.723l-18.873,-10.022c2.731,-10.535 3.837,-21.572 3.124,-32.742Z"
|
||||
style="
|
||||
fill: #ffdb05;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M276.901,251.218c35.634,-10.915 73.425,9.154 84.34,44.787c10.915,35.634 -9.153,73.426 -44.787,84.341c-35.633,10.915 -73.425,-9.153 -84.34,-44.787c-10.915,-35.634 9.153,-73.426 44.787,-84.341Z"
|
||||
style="fill: #b3b6c3" />
|
||||
<path
|
||||
d="M283.918,274.128c22.99,-7.042 47.372,5.905 54.414,28.895c7.042,22.989 -5.906,47.371 -28.895,54.413c-22.99,7.042 -47.371,-5.905 -54.413,-28.895c-7.042,-22.989 5.905,-47.371 28.894,-54.413Z"
|
||||
style="fill: #fff" />
|
||||
<path
|
||||
d="M318.049,385.553c-18.636,5.708 -38.38,3.818 -55.594,-5.323c-17.215,-9.142 -29.839,-24.44 -35.548,-43.076c-5.708,-18.636 -3.818,-38.38 5.324,-55.595c14.472,-27.253 44.692,-42.511 75.198,-37.969c2.974,0.443 5.026,3.213 4.584,6.188c-0.444,2.974 -3.214,5.026 -6.189,4.584c-25.952,-3.865 -51.662,9.118 -63.975,32.305c-7.777,14.645 -9.386,31.442 -4.529,47.298c4.856,15.855 15.597,28.87 30.242,36.646c14.645,7.777 31.442,9.386 47.297,4.529c15.855,-4.857 28.87,-15.597 36.646,-30.242c12.783,-24.071 8.534,-53.276 -10.571,-72.676c-2.111,-2.142 -2.084,-5.59 0.059,-7.701c2.142,-2.111 5.59,-2.084 7.701,0.059c10.813,10.98 17.77,24.873 20.121,40.179c2.397,15.612 -0.262,31.258 -7.69,45.247c-9.141,17.214 -24.439,29.838 -43.076,35.547Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M330.222,261.501c-1.342,0.411 -2.841,0.306 -4.174,-0.411l-0.255,-0.136c-2.656,-1.41 -3.666,-4.707 -2.256,-7.364c1.411,-2.656 4.707,-3.665 7.364,-2.255l0.306,0.164c2.649,1.424 3.641,4.726 2.216,7.375c-0.708,1.315 -1.878,2.221 -3.201,2.627Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M311.144,362.648c-5.021,1.538 -10.346,2.282 -15.812,2.136c-3.006,-0.08 -5.378,-2.583 -5.298,-5.589c0.081,-3.007 2.583,-5.38 5.589,-5.298c14.58,0.389 27.884,-7.364 34.72,-20.237c4.776,-8.992 5.764,-19.306 2.782,-29.042c-2.983,-9.735 -9.577,-17.726 -18.57,-22.502c-8.993,-4.775 -19.306,-5.763 -29.042,-2.781c-9.736,2.982 -17.727,9.577 -22.502,18.57c-6.772,12.752 -5.815,28.016 2.496,39.837c1.73,2.46 1.138,5.857 -1.322,7.586c-2.46,1.731 -5.856,1.139 -7.586,-1.322c-10.683,-15.194 -11.912,-34.816 -3.206,-51.209c6.139,-11.563 16.413,-20.041 28.931,-23.875c12.517,-3.835 25.777,-2.565 37.34,3.575c11.562,6.14 20.041,16.414 23.875,28.931c3.834,12.518 2.564,25.778 -3.576,37.341c-6.225,11.722 -16.623,20.143 -28.819,23.879Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M278.051,359.577c-1.325,0.405 -2.803,0.309 -4.126,-0.386l-0.233,-0.123c-2.656,-1.411 -3.666,-4.707 -2.256,-7.364c1.411,-2.656 4.707,-3.666 7.364,-2.256l0.188,0.1c2.663,1.398 3.688,4.689 2.29,7.352c-0.703,1.34 -1.886,2.266 -3.227,2.677Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M350.408,445.197l-25.761,7.89c-8.613,2.639 -17.767,-2.222 -20.405,-10.835l-5.026,-16.409c-8.725,0.207 -17.424,-0.631 -25.964,-2.499l-8.039,15.139c-2.047,3.854 -5.472,6.68 -9.644,7.958c-0.001,0.001 -0.001,0.001 -0.002,0.001c-4.174,1.278 -8.595,0.853 -12.449,-1.195l-23.79,-12.645c-7.951,-4.226 -10.985,-14.134 -6.761,-22.088l8.047,-15.154c-6.317,-6.026 -11.871,-12.761 -16.576,-20.1l-16.397,5.022c-4.174,1.279 -8.595,0.855 -12.45,-1.194c-3.855,-2.048 -6.681,-5.475 -7.958,-9.65l-7.876,-25.765c-2.632,-8.61 2.23,-17.759 10.838,-20.396l16.421,-5.03c-0.2,-8.7 0.637,-17.376 2.5,-25.89l-15.139,-8.039c-3.854,-2.048 -6.681,-5.474 -7.959,-9.647c-1.278,-4.174 -0.853,-8.594 1.195,-12.449l12.645,-23.79c4.225,-7.952 14.134,-10.985 22.087,-6.761l15.154,8.047c6.009,-6.299 12.724,-11.84 20.039,-16.537l-5.026,-16.407c-2.638,-8.613 2.223,-17.767 10.836,-20.406l25.761,-7.89c8.613,-2.639 17.767,2.222 20.405,10.835l5.026,16.409c8.691,-0.205 17.357,0.624 25.863,2.476l8.047,-15.154c4.224,-7.953 14.131,-10.99 22.085,-6.769l23.8,12.627c3.856,2.046 6.684,5.47 7.962,9.644c1.279,4.173 0.857,8.594 -1.19,12.449l-8.04,15.139c6.312,6.009 11.865,12.728 16.572,20.048l16.421,-5.03c8.608,-2.637 17.761,2.221 20.402,10.827l7.905,25.756c1.281,4.173 0.86,8.595 -1.187,12.451c-2.046,3.856 -5.472,6.684 -9.646,7.963l-16.396,5.022c0.212,8.715 -0.617,17.405 -2.475,25.936l15.154,8.047c7.954,4.224 10.99,14.131 6.769,22.085l-12.626,23.8c-2.046,3.856 -5.471,6.684 -9.644,7.962c-0,0.001 -0.002,0.001 -0.003,0.001c-4.172,1.278 -8.592,0.855 -12.446,-1.192l-15.139,-8.039c-6.027,6.33 -12.766,11.897 -20.11,16.612l5.027,16.408c2.636,8.613 -2.224,17.767 -10.837,20.406Zm-81.581,-33.338c0.94,-0.287 1.962,-0.323 2.965,-0.062c10.156,2.643 20.604,3.648 31.054,2.988c2.515,-0.159 4.812,1.43 5.55,3.84l6.26,20.436c0.879,2.871 3.931,4.492 6.802,3.612l25.76,-7.891c2.871,-0.879 4.492,-3.931 3.612,-6.802l-6.259,-20.435c-0.739,-2.41 0.274,-5.012 2.447,-6.29c9.026,-5.306 17.118,-11.991 24.052,-19.869c1.666,-1.893 4.415,-2.394 6.642,-1.212l18.862,10.016c1.285,0.682 2.759,0.823 4.15,0.397c1.391,-0.426 2.532,-1.369 3.214,-2.654l12.627,-23.8c1.407,-2.651 0.395,-5.953 -2.256,-7.361l-18.874,-10.023c-2.225,-1.181 -3.349,-3.736 -2.717,-6.175c2.631,-10.149 3.627,-20.588 2.96,-31.029c-0.161,-2.517 1.428,-4.816 3.84,-5.555l20.426,-6.256c1.392,-0.427 2.533,-1.369 3.215,-2.654c0.683,-1.286 0.823,-2.76 0.396,-4.15l-7.905,-25.757c-0.88,-2.869 -3.931,-4.487 -6.801,-3.608l-20.445,6.262c-2.408,0.738 -5.009,-0.273 -6.288,-2.444c-5.301,-9.004 -11.973,-17.076 -19.834,-23.993c-1.893,-1.667 -2.394,-4.415 -1.212,-6.642l10.016,-18.862c0.683,-1.285 0.823,-2.759 0.397,-4.15c-0.426,-1.391 -1.369,-2.532 -2.654,-3.214l-23.799,-12.627c-2.652,-1.407 -5.954,-0.396 -7.362,2.256l-10.022,18.875c-1.181,2.225 -3.736,3.35 -6.176,2.717c-10.125,-2.625 -20.541,-3.622 -30.96,-2.964c-2.516,0.159 -4.812,-1.429 -5.551,-3.84l-6.26,-20.436c-0.879,-2.871 -3.931,-4.491 -6.802,-3.612l-25.76,7.891c-2.871,0.88 -4.491,3.931 -3.612,6.802l6.26,20.436c0.738,2.41 -0.274,5.012 -2.448,6.29c-8.999,5.29 -17.071,11.95 -23.989,19.795c-1.667,1.89 -4.413,2.389 -6.638,1.208l-18.873,-10.022c-2.652,-1.408 -5.954,-0.397 -7.363,2.253l-12.645,23.791c-0.683,1.285 -0.824,2.758 -0.398,4.149c0.426,1.391 1.368,2.534 2.653,3.216l18.862,10.016c2.227,1.183 3.351,3.74 2.716,6.181c-2.638,10.134 -3.645,20.558 -2.993,30.986c0.157,2.514 -1.431,4.808 -3.841,5.546l-20.443,6.262c-2.869,0.879 -4.49,3.929 -3.613,6.8l7.877,25.764c0.426,1.391 1.367,2.534 2.652,3.216c1.285,0.683 2.759,0.824 4.15,0.398l20.427,-6.257c2.411,-0.738 5.014,0.276 6.291,2.451c5.295,9.023 11.968,17.114 19.831,24.048c1.89,1.667 2.39,4.413 1.208,6.638l-10.022,18.874c-1.408,2.651 -0.397,5.954 2.253,7.362l23.791,12.645c1.285,0.683 2.758,0.824 4.149,0.398c0,-0 0.001,-0 0.001,-0c1.391,-0.426 2.532,-1.368 3.215,-2.653l10.016,-18.862c0.696,-1.312 1.87,-2.241 3.216,-2.654Z"
|
||||
style="fill-rule: nonzero" />
|
||||
</g>
|
||||
<path
|
||||
d="M331.028,101.547l46.069,15.854c-0,-0 31.781,-6.692 34.148,-47.156c2.41,-40.449 -35.739,-58.435 -35.739,-58.435c-0.711,18.016 -5.801,18.692 -20.98,31.049c-15.178,12.356 -33.75,37.677 -23.498,58.688Z"
|
||||
style="
|
||||
fill: #7c5748;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M359.686,111.409l17.412,5.992c0,0 31.782,-6.691 34.148,-47.156c2.411,-40.449 -35.738,-58.434 -35.738,-58.434c32.273,36.893 -5.32,85.875 -15.822,99.598Z"
|
||||
style="
|
||||
fill: #5f4c44;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M407.419,88.985c1.997,-5.238 3.393,-11.41 3.827,-18.74c2.41,-40.449 -35.739,-58.434 -35.739,-58.434c-0.712,18.015 -5.801,18.691 -20.98,31.048c-2.503,2.052 -5.126,4.452 -7.65,7.129c2.17,2.446 3.834,6.078 3.708,11.522c0,0 14.139,-2.418 26.644,1.885c2.997,1.032 5.203,2.665 7.143,4.644c6.184,6.304 9.196,16.179 23.047,20.946Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M407.419,88.985c1.997,-5.238 3.393,-11.41 3.827,-18.74c2.41,-40.449 -35.739,-58.434 -35.739,-58.434c15.513,17.722 14.878,38.192 8.865,56.228c6.184,6.304 9.196,16.179 23.047,20.946Z"
|
||||
style="
|
||||
fill: #0094d8;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M321.254,134.516l47.508,16.35c3.765,1.295 7.868,-0.707 9.163,-4.472l6.488,-18.85c1.296,-3.766 -0.706,-7.869 -4.472,-9.164l-47.508,-16.349c-3.765,-1.296 -7.868,0.706 -9.163,4.471l-6.488,18.851c-1.296,3.765 0.706,7.867 4.472,9.163Z"
|
||||
style="
|
||||
fill: #c0c9d2;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M365.308,121.077l-6.42,18.657c-1.314,3.819 -5.476,5.849 -9.295,4.535l19.072,6.563c3.819,1.314 7.98,-0.716 9.294,-4.535l6.421,-18.657c1.314,-3.818 -0.716,-7.979 -4.535,-9.294l-19.072,-6.563c3.819,1.314 5.849,5.475 4.535,9.294Z"
|
||||
style="
|
||||
fill: #a6aeba;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M324.347,135.58c-11.322,7.729 -23.596,27.224 -38.115,69.412c-25.168,73.133 -35.058,150.447 -16.621,156.791c18.437,6.345 58.219,-60.681 83.387,-133.814c14.518,-42.188 16.842,-65.108 12.674,-78.168"
|
||||
style="
|
||||
fill: #f89e00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M336.293,222.22c14.518,-42.187 19.332,-64.251 19.042,-75.976l10.336,3.557c4.169,13.06 1.845,35.979 -12.674,78.168c-25.168,73.133 -64.949,140.158 -83.387,133.814c9.214,3.17 41.515,-66.43 66.683,-139.563Z"
|
||||
style="
|
||||
fill: #d38302;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M268.259,365.699c7.978,2.746 17.565,-3.002 29.311,-17.572c9.288,-11.519 19.504,-28.127 29.666,-48.198c1.039,-2.053 0.203,-4.563 -1.858,-5.584c-2.039,-1.01 -4.511,-0.181 -5.539,1.851c-24.764,48.965 -42.73,63.785 -48.883,61.667c-4.802,-1.652 -9.061,-15.466 -5.268,-48.963c3.376,-29.812 12.292,-67.194 24.461,-102.559c12.241,-35.571 23.68,-57.245 34.909,-66.134l37.205,12.804c3.381,13.915 -0.94,38.038 -13.182,73.611c-6.071,17.64 -13.273,35.637 -20.923,52.308c-0.947,2.063 -0.045,4.504 2.011,5.467l0.001,0c2.086,0.978 4.566,0.064 5.528,-2.031c7.76,-16.91 15.063,-35.162 21.219,-53.047c11.784,-34.244 16.388,-58.069 14.209,-73.928c4.703,-0.013 9.103,-2.957 10.718,-7.648l6.487,-18.852c1.289,-3.745 0.493,-7.707 -1.76,-10.633c2.175,-1.078 4.612,-2.487 7.124,-4.31c1.975,-1.432 2.296,-4.251 0.69,-6.087l-0.001,-0c-1.42,-1.623 -3.833,-1.867 -5.58,-0.603c-4.971,3.597 -9.556,5.235 -11.518,5.826l-43.209,-14.869c-5.449,-13.415 2.155,-29.18 11.956,-41.016c0.302,1.235 0.446,2.589 0.409,4.068c-0.018,0.708 0.09,1.422 0.406,2.055c0.858,1.719 2.675,2.571 4.43,2.271c0.131,-0.023 13.226,-2.195 24.597,1.719c4.422,1.521 6.85,4.886 9.924,9.144c3.522,4.882 7.796,10.799 16.226,14.799c-0.487,1.023 -1.008,2.019 -1.562,2.987c-1.058,1.851 -0.504,4.206 1.246,5.424l0.003,0.002c1.999,1.39 4.759,0.766 5.968,-1.348c4.508,-7.88 7.105,-17.207 7.732,-27.794c1.046,-17.73 -5.222,-34.16 -18.13,-47.513c-9.647,-9.982 -19.558,-14.717 -19.974,-14.914c-1.143,-0.538 -2.461,-0.524 -3.585,0.024c-0.112,0.055 -0.223,0.115 -0.332,0.181c-1.188,0.72 -1.936,1.988 -1.992,3.377c-0.529,13.196 -2.91,15.039 -12.929,22.801c-1.925,1.491 -4.106,3.181 -6.546,5.17c-9.257,7.544 -17.414,17.555 -22.381,27.468c-5.619,11.215 -7.016,21.958 -4.146,31.399c-0.096,0.044 -0.195,0.079 -0.291,0.125c-2.726,1.33 -4.77,3.642 -5.757,6.51l-6.488,18.851c-1.614,4.692 0.043,9.722 3.743,12.625c-11.479,11.161 -22.51,32.773 -34.294,67.016c-12.357,35.912 -21.417,73.936 -24.859,104.322c-3.904,34.496 -0.269,53.919 10.808,57.731Zm88.902,-319.624c2.36,-1.924 4.499,-3.58 6.386,-5.042c9.052,-7.013 13.907,-10.773 15.56,-22.321c9.841,6.455 29.701,23.083 28.033,51.325c-0.283,4.8 -1.019,9.281 -2.195,13.428c-6.063,-3.044 -9.17,-7.346 -12.426,-11.858c-3.461,-4.796 -7.039,-9.755 -13.947,-12.133c-9.004,-3.098 -18.626,-2.998 -24.131,-2.572c-0.373,-2.353 -1.104,-4.505 -2.184,-6.446c1.651,-1.604 3.301,-3.074 4.904,-4.381Zm-36.46,80.626l6.487,-18.852c0.267,-0.775 0.818,-1.399 1.555,-1.758c0.736,-0.36 1.567,-0.411 2.343,-0.143l47.508,16.348c1.6,0.551 2.453,2.3 1.903,3.899l-6.488,18.851c-0.275,0.8 -0.849,1.413 -1.555,1.758c-0.706,0.344 -1.543,0.42 -2.343,0.144l-47.509,-16.35c-1.599,-0.55 -2.452,-2.298 -1.901,-3.897Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<clipPath id="_clip3">
|
||||
<polygon
|
||||
points="338.743,110.391 329.8,355.224 84.967,346.281 93.91,101.448 338.743,110.391 " />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip3)">
|
||||
<path
|
||||
d="M199.991,236.632l21.148,-19.657c2.063,-1.919 2.181,-5.148 0.263,-7.213l-67.366,-72.473l0.295,-8.09c0.071,-1.93 -0.954,-3.735 -2.649,-4.663l-40.059,-21.893c-1.928,-1.052 -4.313,-0.752 -5.921,0.744l-10.574,9.828c-1.61,1.495 -2.082,3.852 -1.174,5.851l18.912,41.551c0.802,1.757 2.527,2.911 4.458,2.982l8.089,0.295l67.365,72.475c1.919,2.064 5.148,2.182 7.213,0.263Z"
|
||||
style="
|
||||
fill: #cfd8dc;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M315.572,345.996l6.207,-5.769c11.646,-11.351 12.322,-29.849 1.535,-42.019l-61.852,-66.563c-0.923,-0.99 -2.203,-1.574 -3.557,-1.621c-8.451,-0.309 -15.051,-7.41 -14.743,-15.861c0.052,-1.354 -0.436,-2.672 -1.357,-3.666l-9.829,-10.573c-1.919,-2.064 -5.147,-2.182 -7.212,-0.264l-42.296,39.315c-2.064,1.919 -2.182,5.147 -0.263,7.212l9.828,10.574c0.92,0.992 2.195,1.579 3.547,1.631c8.451,0.309 15.051,7.41 14.743,15.861c-0.052,1.354 0.436,2.673 1.357,3.666l61.862,66.542c11.183,12.019 29.99,12.706 42.02,1.535l0.01,0Z"
|
||||
style="
|
||||
fill: #ff02ad;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<g>
|
||||
<path
|
||||
d="M241.858,249.868c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.443c1.955,2.032 1.893,5.262 -0.138,7.218c-2.031,1.954 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="
|
||||
fill: #c62828;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M225.997,264.611c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.444c1.955,2.031 1.893,5.262 -0.138,7.217c-2.031,1.955 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="
|
||||
fill: #c62828;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<path
|
||||
d="M180.837,242.532c0.047,-1.354 0.63,-2.634 1.621,-3.557l42.296,-39.315c2.064,-1.918 5.293,-1.8 7.212,0.264l9.829,10.574c0.924,0.991 1.416,2.31 1.367,3.666c-0.308,8.451 6.292,15.552 14.743,15.86c1.354,0.048 2.634,0.631 3.557,1.622l61.852,66.552c10.627,12.227 9.956,30.599 -1.535,42.019l-6.217,5.779c-12.03,11.171 -30.837,10.484 -42.02,-1.535l-61.861,-66.552c-0.919,-0.991 -1.407,-2.305 -1.358,-3.656c0.308,-8.451 -6.292,-15.552 -14.743,-15.861c-1.354,-0.047 -2.634,-0.63 -3.557,-1.621l-9.829,-10.574c-0.921,-0.993 -1.409,-2.312 -1.357,-3.665Zm47.133,-31.917l-34.821,32.366l5.013,5.393c12.293,1.528 21.714,11.663 22.341,24.035l60.515,65.104c7.345,7.891 19.693,8.342 27.595,1.008l6.217,-5.78c7.536,-7.503 7.976,-19.561 1.008,-27.594l-60.515,-65.104c-12.293,-1.527 -21.714,-11.663 -22.341,-24.035l-5.012,-5.393Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M93.496,116.762c0.047,-1.354 0.63,-2.633 1.622,-3.557l10.573,-9.829c1.608,-1.496 3.993,-1.795 5.921,-0.743l40.06,21.893c1.702,0.927 2.732,2.737 2.659,4.673l-0.295,8.09l67.366,72.474c1.883,2.097 1.709,5.324 -0.389,7.208c-2.046,1.836 -5.18,1.722 -7.086,-0.259l-68.802,-74.018c-0.924,-0.992 -1.416,-2.311 -1.367,-3.666l0.257,-7.049l-34.051,-18.61l-5.181,4.816l16.076,35.318l7.039,0.257c1.353,0.047 2.633,0.63 3.557,1.622l68.8,74.017c1.883,2.098 1.709,5.325 -0.389,7.208c-2.046,1.837 -5.18,1.722 -7.086,-0.259l-67.368,-72.453l-8.089,-0.295c-1.93,-0.071 -3.656,-1.225 -4.457,-2.982l-18.912,-41.551c-0.33,-0.722 -0.487,-1.511 -0.458,-2.305Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M241.858,249.868c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.443c1.955,2.032 1.893,5.262 -0.138,7.218c-2.031,1.954 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M225.997,264.611c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.444c1.955,2.031 1.893,5.262 -0.138,7.217c-2.031,1.955 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="fill-rule: nonzero" />
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M562.573,120.232l-87.881,207.058c-10.861,25.591 -40.411,37.532 -66.002,26.671c-25.591,-10.862 -37.532,-40.412 -26.671,-66.003l87.881,-207.058l92.673,39.332Z"
|
||||
style="
|
||||
fill: #dfe9f4;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M419.861,198.798l-37.842,89.16c-10.861,25.591 1.08,55.141 26.671,66.002c25.59,10.861 55.14,-1.079 66.002,-26.67l53.493,-126.037l-108.324,-2.455Z"
|
||||
style="
|
||||
fill: #24c100;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M528.18,201.254l-53.499,126.05c-10.864,25.596 -40.385,37.525 -65.981,26.662c-16.865,-7.158 -27.793,-22.426 -30.186,-39.277c4.202,3.899 9.15,7.192 14.756,9.571c25.596,10.864 55.163,-1.045 66.027,-26.642l41.172,-97.008l27.711,0.644Z"
|
||||
style="
|
||||
fill: #21af00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M578.717,120.131l12.276,-28.925c1.381,-3.254 -0.137,-7.011 -3.391,-8.392l-108.177,-45.912c-3.253,-1.381 -7.01,0.137 -8.391,3.39l-12.277,28.926c-1.381,3.254 0.137,7.011 3.391,8.392l108.177,45.913c3.253,1.38 7.01,-0.138 8.392,-3.392Z"
|
||||
style="
|
||||
fill: #2dcef6;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M590.997,91.198l-12.288,28.95c-1.365,3.217 -5.116,4.771 -8.378,3.386l-108.175,-45.912c-3.263,-1.385 -4.751,-5.162 -3.385,-8.378l3.12,-7.353l100.179,42.518c3.263,1.385 7.013,-0.169 8.398,-3.431l9.147,-21.552l7.996,3.393c3.263,1.384 4.77,5.116 3.386,8.379Z"
|
||||
style="
|
||||
fill: #1ec5e0;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M523.331,129.908c7.056,2.995 10.353,11.155 7.359,18.21c-2.995,7.056 -11.155,10.353 -18.21,7.359c-7.056,-2.995 -10.353,-11.155 -7.359,-18.21c2.995,-7.056 11.155,-10.353 18.21,-7.359Z"
|
||||
style="
|
||||
fill: #21af00;
|
||||
" />
|
||||
<path
|
||||
d="M470.558,233.05c7.624,3.236 11.187,12.053 7.951,19.677c-3.235,7.624 -12.052,11.186 -19.676,7.951c-7.624,-3.236 -11.187,-12.053 -7.951,-19.677c3.236,-7.624 12.053,-11.186 19.676,-7.951Z"
|
||||
style="
|
||||
fill: #dfe9f4;
|
||||
" />
|
||||
<path
|
||||
d="M428.819,292.984c6.059,2.571 8.891,9.579 6.319,15.638c-2.571,6.059 -9.578,8.89 -15.638,6.318c-6.059,-2.571 -8.89,-9.578 -6.318,-15.637c2.571,-6.059 9.578,-8.891 15.637,-6.319Z"
|
||||
style="
|
||||
fill: #dfe9f4;
|
||||
" />
|
||||
<g>
|
||||
<path
|
||||
d="M473.695,150.963c5.26,2.232 7.718,8.315 5.485,13.576c-2.232,5.26 -8.315,7.718 -13.575,5.485c-5.261,-2.232 -7.719,-8.315 -5.486,-13.576c2.232,-5.26 8.316,-7.718 13.576,-5.485Z"
|
||||
style="
|
||||
fill: #21af00;
|
||||
" />
|
||||
<g>
|
||||
<path
|
||||
d="M589.555,78.219l-18.628,-7.906c-2.54,-1.078 -5.468,0.105 -6.546,2.645c-1.078,2.539 0.105,5.467 2.645,6.545l18.628,7.907c0.714,0.302 1.045,1.132 0.742,1.845l-12.277,28.927c-0.303,0.714 -1.128,1.047 -1.841,0.744c-48.948,-20.774 -58.962,-25.024 -108.18,-45.913c-0.713,-0.303 -1.051,-1.13 -0.748,-1.843l12.278,-28.928c0.302,-0.713 1.133,-1.049 1.847,-0.746l72.345,30.705c2.54,1.078 5.468,-0.105 6.546,-2.645c1.078,-2.54 -0.105,-5.468 -2.645,-6.546l-72.345,-30.705c-5.785,-2.455 -12.484,0.252 -14.939,6.036l-12.277,28.928c-2.454,5.78 0.253,12.479 6.038,14.934l3.159,1.341l-11.156,26.284c-1.078,2.54 0.106,5.468 2.645,6.546c2.54,1.078 5.468,-0.105 6.546,-2.645l11.156,-26.284l83.479,35.43l-31.114,73.309l-36.489,-0.826c-2.754,-0.062 -5.041,2.119 -5.1,4.879c-0.067,2.753 2.097,5.042 4.879,5.101l32.511,0.738l-50.618,119.264c-9.769,23.017 -36.438,33.794 -59.455,24.025c-23.017,-9.769 -33.794,-36.438 -24.025,-59.455l36.518,-86.042l44.574,1.007c2.754,0.062 5.041,-2.119 5.1,-4.879c0.067,-2.752 -2.103,-5.039 -4.879,-5.1l-40.596,-0.92l25.271,-59.541c1.078,-2.54 -0.105,-5.468 -2.645,-6.546c-2.54,-1.077 -5.468,0.106 -6.546,2.645l-65.987,155.475c-11.919,28.084 1.231,60.627 29.315,72.546c28.083,11.92 60.627,-1.23 72.546,-29.314c19.018,-44.809 66.682,-157.112 85.931,-202.465l3.159,1.341c5.78,2.453 12.479,-0.253 14.933,-6.034l12.277,-28.927c2.455,-5.785 -0.252,-12.484 -6.032,-14.937Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M510.531,160.069c9.585,4.068 20.688,-0.419 24.757,-10.004c4.066,-9.581 -0.42,-20.685 -10.006,-24.753c-9.585,-4.068 -20.689,0.418 -24.755,10c-4.069,9.586 0.418,20.688 10.004,24.757Zm10.85,-25.566c4.515,1.916 6.632,7.147 4.716,11.662c-1.916,4.515 -7.151,6.63 -11.666,4.714c-4.514,-1.916 -6.63,-7.152 -4.714,-11.666c1.916,-4.515 7.15,-6.626 11.664,-4.71Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M483.107,254.679c4.309,-10.151 -0.447,-21.919 -10.598,-26.228c-10.15,-4.308 -21.915,0.449 -26.223,10.6c-4.308,10.151 0.444,21.913 10.596,26.222c10.151,4.308 21.917,-0.443 26.225,-10.594Zm-22.325,1.403c-5.084,-2.158 -7.463,-8.045 -5.305,-13.13c2.158,-5.085 8.047,-7.468 13.132,-5.31c5.084,2.158 7.465,8.052 5.308,13.136c-2.158,5.084 -8.05,7.462 -13.135,5.304Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M430.768,288.391c-8.585,-3.644 -18.538,0.378 -22.181,8.963c-3.644,8.585 0.378,18.538 8.963,22.182c8.585,3.643 18.537,-0.378 22.181,-8.963c3.644,-8.585 -0.378,-18.538 -8.963,-22.182Zm-9.318,21.954c-3.518,-1.493 -5.166,-5.571 -3.673,-9.09c1.494,-3.519 5.572,-5.167 9.09,-3.673c3.519,1.493 5.167,5.571 3.673,9.09c-1.493,3.518 -5.571,5.166 -9.09,3.673Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M483.773,166.486c3.305,-7.786 -0.343,-16.812 -8.128,-20.116c-7.791,-3.307 -16.817,0.341 -20.121,8.126c-3.305,7.786 0.342,16.812 8.133,20.119c7.786,3.304 16.812,-0.343 20.116,-8.129Zm-19.059,-8.089c1.155,-2.719 4.306,-3.993 7.03,-2.836c2.719,1.154 3.993,4.305 2.838,7.024c-1.154,2.72 -4.305,3.993 -7.024,2.839c-2.725,-1.156 -3.998,-4.307 -2.844,-7.027Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M1338.77,177.49l-14.203,208.687l45.422,0.422l7.453,-101.11l23.063,94.782c5.531,6.187 11.859,7.5 18.984,3.937l43.031,-97.875l-5.765,99.844l45.703,-0.422l12.797,-208.547c-12.563,-3.281 -25.594,-3.187 -39.094,0.282l-57.797,118.406l-32.766,-118.125c-19.5,-5.344 -35.109,-5.438 -46.828,-0.281Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M1536.29,178.474c-3.375,1.594 -5.578,4.219 -6.609,7.875l-12.657,192.797c0.938,5.156 3.422,8.109 7.454,8.859l131.484,-5.203c9.938,-14.531 11.016,-30.094 3.234,-46.687l-94.359,2.812l3.797,-43.594l76.922,-3.234c8.156,-12.188 8.718,-25.125 1.687,-38.813l-75.515,1.547l2.531,-36l91.969,-2.953c6.187,-12.844 6.984,-26.625 2.39,-41.344l-132.328,3.938Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M1829.72,195.068c3.281,16.875 -1.125,30.375 -13.219,40.5c-8.156,-5.25 -20.953,-9.422 -38.39,-12.516c-15.282,-1.5 -27.75,2.93 -37.407,13.289c-9.656,10.359 -15.562,23.789 -17.718,40.289c-3.188,10.406 -1.219,23.766 5.906,40.078c7.125,14.813 17.976,22.641 32.554,23.485c14.579,0.843 32.04,-2.016 52.383,-8.578c7.313,14.437 8.016,28.593 2.11,42.468c-24.094,11.063 -43.641,16.313 -58.641,15.75c-27.656,-3.469 -47.812,-13.5 -60.469,-30.094c-16.875,-22.218 -22.593,-53.578 -17.156,-94.078c4.313,-32.062 14.484,-56.297 30.516,-72.703c20.906,-16.969 46.265,-23.906 76.078,-20.812c20.719,1.312 35.203,8.953 43.453,22.922Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M1845.63,183.958l-12.093,197.578c10.312,7.407 25.546,8.532 45.703,3.375l5.062,-85.781l65.953,-2.812l-4.078,85.922c12.938,7.968 28.078,7.593 45.422,-1.125l9.563,-200.11c-13.313,-7.219 -27.188,-7.312 -41.625,-0.281l-5.344,73.266l-66.797,3.375l4.078,-78.61c-16.875,-6.469 -32.156,-4.734 -45.844,5.203Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2073.74,175.099l-73.688,205.453c10.219,8.719 25.782,11.672 46.688,8.859l12.938,-37.968l52.171,-6.469l10.125,37.687c20.532,3.938 37.219,1.688 50.063,-6.75l-56.25,-202.5c-13.219,-8.156 -27.235,-7.593 -42.047,1.688Zm30.234,130.5l-34.312,4.078l22.078,-69.187l12.234,65.109Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2185.9,183.958l-12.093,197.578c10.312,7.407 25.547,8.532 45.703,3.375l7.875,-115.031l62.859,114.75c16.5,6.563 29.532,5.531 39.094,-3.094l12.094,-200.531c-13.313,-7.219 -27.188,-7.312 -41.625,-0.281l-6.75,110.812l-66.938,-113.484c-20.156,-5.25 -33.562,-3.281 -40.219,5.906Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2340.33,383.083c13.218,6.844 29.39,7.547 48.515,2.11l16.594,-206.86c-16.5,-4.781 -31.875,-5.484 -46.125,-2.109l-18.984,206.859Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2562.53,195.068c3.281,16.875 -1.125,30.375 -13.219,40.5c-8.156,-5.25 -20.953,-9.422 -38.39,-12.516c-15.282,-1.5 -27.75,2.93 -37.407,13.289c-9.656,10.359 -15.562,23.789 -17.718,40.289c-3.188,10.406 -1.219,23.766 5.906,40.078c7.125,14.813 17.976,22.641 32.555,23.485c14.578,0.843 32.039,-2.016 52.382,-8.578c7.313,14.437 8.016,28.593 2.11,42.468c-24.094,11.063 -43.641,16.313 -58.641,15.75c-27.656,-3.469 -47.812,-13.5 -60.469,-30.094c-16.875,-22.218 -22.593,-53.578 -17.156,-94.078c4.313,-32.062 14.484,-56.297 30.516,-72.703c20.906,-16.969 46.265,-23.906 76.078,-20.812c20.719,1.312 35.203,8.953 43.453,22.922Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2572.61,363.536c-6.093,-21.281 -2.531,-37.781 10.688,-49.5c24.094,14.157 41.578,22.875 52.453,26.157c20.25,-0.188 31.359,-5.25 33.328,-15.188c0.469,-7.969 -5.719,-15.656 -18.562,-23.062l-36.141,-17.578c-22.594,-12 -34.313,-28.922 -35.156,-50.766c0.843,-12.563 5.179,-23.859 13.008,-33.891c7.828,-10.031 16.64,-16.617 26.437,-19.758c9.797,-3.14 21.445,-4.289 34.945,-3.445c24.75,1.5 46.453,8.531 65.11,21.094c0.75,17.719 -4.782,33.141 -16.594,46.266c-22.781,-17.063 -41.016,-25.032 -54.703,-23.907c-12.844,-1.5 -19.641,3.094 -20.391,13.782c-0.469,6.093 8.578,13.968 27.141,23.625c21.281,8.625 37.055,18.515 47.32,29.671c10.266,11.157 14.836,24.704 13.711,40.641c-1.5,21.563 -11.531,38.109 -30.094,49.641c-11.812,8.25 -28.875,11.671 -51.187,10.265c-21.281,-2.719 -41.719,-10.734 -61.313,-24.047Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M644.827,363.536c-6.094,-21.281 -2.531,-37.781 10.688,-49.5c24.093,14.157 41.578,22.875 52.453,26.157c20.25,-0.188 31.359,-5.25 33.328,-15.188c0.469,-7.969 -5.719,-15.656 -18.563,-23.062l-36.14,-17.578c-22.594,-12 -34.313,-28.922 -35.157,-50.766c0.844,-12.563 5.18,-23.859 13.008,-33.891c7.828,-10.031 16.641,-16.617 26.438,-19.758c9.797,-3.14 21.445,-4.289 34.945,-3.445c24.75,1.5 46.453,8.531 65.109,21.094c0.75,17.719 -4.781,33.141 -16.593,46.266c-22.782,-17.063 -41.016,-25.032 -54.704,-23.907c-12.843,-1.5 -19.64,3.094 -20.39,13.782c-0.469,6.093 8.578,13.968 27.14,23.625c21.282,8.625 37.055,18.515 47.321,29.671c10.265,11.157 14.836,24.704 13.711,40.641c-1.5,21.563 -11.532,38.109 -30.094,49.641c-11.813,8.25 -28.875,11.671 -51.188,10.265c-21.281,-2.719 -41.718,-10.734 -61.312,-24.047Z"
|
||||
fill="currentColor"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M805.297,178.896c-9.563,12.281 -9.141,27.187 1.266,44.719l51.047,-1.125l-9.141,161.437c16.969,6.281 32.578,6.375 46.828,0.281l7.594,-163.547l51.89,-0.703c7.219,-15.094 7.594,-30.14 1.125,-45.14l-150.609,4.078Z"
|
||||
fill="currentColor"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M983.767,178.474c-3.375,1.594 -5.578,4.219 -6.609,7.875l-12.657,192.797c0.938,5.156 3.422,8.109 7.454,8.859l131.484,-5.203c9.937,-14.531 11.016,-30.094 3.234,-46.687l-94.359,2.812l3.797,-43.594l76.922,-3.234c8.156,-12.188 8.718,-25.125 1.687,-38.813l-75.515,1.547l2.531,-36l91.969,-2.953c6.187,-12.844 6.984,-26.625 2.39,-41.344l-132.328,3.938Z"
|
||||
fill="currentColor"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M1136.08,177.49l-14.203,208.687l45.421,0.422l7.454,-101.11l23.062,94.782c5.531,6.187 11.86,7.5 18.985,3.937l43.031,-97.875l-5.766,99.844l45.703,-0.422l12.797,-208.547c-12.562,-3.281 -25.594,-3.187 -39.094,0.282l-57.796,118.406l-32.766,-118.125c-19.5,-5.344 -35.109,-5.438 -46.828,-0.281Z"
|
||||
fill="currentColor"
|
||||
style="fill-rule: nonzero" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</router-link>
|
||||
<div class="flex flex-items-center flex-justify-end w-full">
|
||||
<router-link
|
||||
:to="{ name: 'workshops' }"
|
||||
role="button"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white hidden sm:block">
|
||||
Find Workshops
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
title="Toggle nav"
|
||||
id="navbar-toggle"
|
||||
class="leading-0 ml-4 mr-2 bg-transparent cursor-pointer"
|
||||
@click.stop="toggleNav">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1zm0 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1zm0 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1z"
|
||||
clip-rule="evenodd"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- <router-link
|
||||
:to="{ name: 'cart' }"
|
||||
id="navbar-cart"
|
||||
class="block cursor-pointer select-none relative"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="w-6 h-6 pointer-events-none">
|
||||
<path
|
||||
d="M220-80q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h110v-10q0-63 43.5-106.5T480-880q63 0 106.5 43.5T630-730v10h110q24 0 42 18t18 42v520q0 24-18 42t-42 18H220Zm0-60h520v-520H630v90q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T570-570v-90H390v90q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T330-570v-90H220v520Zm170-580h180v-10q0-38-26-64t-64-26q-38 0-64 26t-26 64v10ZM220-140v-520 520Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<div
|
||||
class="absolute flex items-center flex-justify-center -top-2 -right-4 bg-red text-3 p-1 w-5 h-5 rounded-9 text-white">
|
||||
14
|
||||
</div>
|
||||
</router-link> -->
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="navbar-draw"
|
||||
class="absolute top-full left-0 right-0 px-4 pt-4 pb-6 shadow-xl w-full bg-white dark:bg-dark-9 z-1"
|
||||
v-show="isNavVisible">
|
||||
<div class="max-w-7xl mx-auto flex flex-col">
|
||||
<router-link
|
||||
:to="{ name: 'blog' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center">
|
||||
Blog
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'workshops' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center">
|
||||
Workshops
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'community' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center">
|
||||
Community
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'contact' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center">
|
||||
Contact
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'register' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center"
|
||||
v-show="!userStore.id">
|
||||
Register
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'login' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center"
|
||||
v-show="!userStore.id">
|
||||
Log in
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'dashboard' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center"
|
||||
v-show="userStore.id">
|
||||
Dashboard
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'logout' }"
|
||||
class="hover:text-sky-600 hover:bg-gray-1 dark:hover:text-sky-400 text-sm transition-colors px-2.5 py-2 inline-flex items-center"
|
||||
v-show="userStore.id">
|
||||
Log out
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isNavVisible = ref(false);
|
||||
const isNavTransparent = ref(false);
|
||||
|
||||
/**
|
||||
* Toggle the navbar visiblity
|
||||
*/
|
||||
const toggleNav = () => {
|
||||
isNavVisible.value = !isNavVisible.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the navbar
|
||||
*/
|
||||
const hideNav = () => {
|
||||
isNavVisible.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle user scrolling
|
||||
*/
|
||||
const handleScroll = () => {
|
||||
isNavTransparent.value = window.scrollY >= 60;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.body.addEventListener("click", hideNav);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener("click", hideNav);
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#navbar {
|
||||
&.is-open {
|
||||
background-color: #fff;
|
||||
|
||||
#navbar-logo,
|
||||
#navbar-toggle,
|
||||
#navbar-cart {
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#navbar-logo,
|
||||
#navbar-toggle,
|
||||
#navbar-cart {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#navbar-draw a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-home {
|
||||
#navbar {
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
#navbar-logo,
|
||||
#navbar-toggle,
|
||||
#navbar-cart {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,645 +0,0 @@
|
||||
<template>
|
||||
<footer class="bg-dark-800 py-12 mt-36">
|
||||
<div class="max-w-7xl m-auto px-4">
|
||||
<div
|
||||
class="grid gap-10 grid-cols-1 sm:grid-cols-2 md:grid-cols-4 text-sm text-white text-center md:text-left">
|
||||
<div class="sm:col-span-2 flex items-center">
|
||||
<p class="mt-4 md:mr-12 text-gray-400">
|
||||
STEMMechanics Australia acknowledges the Traditional
|
||||
Owners of Country throughout Australia and the
|
||||
continuing connection to land, cultures and communities.
|
||||
We pay our respect to Aboriginal and Torres Strait
|
||||
Islander cultures; and to Elders both past, present and
|
||||
emerging.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-lg text-gray-4"
|
||||
>Community</span
|
||||
>
|
||||
<ul class="mt-4 leading-5 space-y-2">
|
||||
<li><a href="/community">Our Community</a></li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/STEMMechanics"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://discord.gg/yNzk4x7mpD"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://facebook.com/stemmechanics"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Facebook
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'minecraft' }">
|
||||
Minecraft
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://twitter.com/stemmechanics"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://youtube.com/@STEMMechanics"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
YouTube
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-lg text-gray-4"
|
||||
>STEMMechanics</span
|
||||
>
|
||||
<ul class="mt-4 leading-5 space-y-2">
|
||||
<li>
|
||||
<router-link :to="{ name: 'contact' }">
|
||||
Contact Us
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'code-of-conduct' }">
|
||||
Code of Conduct
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'terms-and-conditions' }">
|
||||
Terms & Conditions
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'privacy' }">
|
||||
Privacy Policy
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col md:flex-row items-center gap-2 border-t border-gray-600/50 mt-8 pt-10">
|
||||
<router-link :to="{ name: 'home' }">
|
||||
<svg
|
||||
id="navbar-logo"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 2762 491"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xml:space="preserve"
|
||||
xmlns:serif="http://www.serif.com/"
|
||||
style="
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 10;
|
||||
"
|
||||
alt="STEMMechanics">
|
||||
<g>
|
||||
<g>
|
||||
<g id="g7146">
|
||||
<g id="g7154"></g>
|
||||
<g id="g7158"></g>
|
||||
<g id="g7162">
|
||||
<g id="g7164">
|
||||
<g id="g7188"></g>
|
||||
<g id="g7192">
|
||||
<path
|
||||
id="path7194"
|
||||
d="M520.706,133.507l0,263.567l32.794,-0l0,-257.505c0,-3.348 -2.714,-6.062 -6.061,-6.062l-26.733,-0Z"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7196">
|
||||
<path
|
||||
id="path7198"
|
||||
d="M56.5,139.568l-0,257.506l32.794,0l-0,-263.568l-26.733,0c-3.348,0 -6.061,2.714 -6.061,6.062"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7200">
|
||||
<path
|
||||
id="path7202"
|
||||
d="M553.5,216.404l-0,-76.836c-0,-3.348 -2.714,-6.061 -6.061,-6.061l-26.733,0l-0,263.567l32.794,0l-0,-145.67"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7204">
|
||||
<path
|
||||
id="path7206"
|
||||
d="M89.294,216.404l-0,-82.897l-26.733,0c-3.348,0 -6.061,2.713 -6.061,6.061l-0,257.506l32.794,0l-0,-145.67"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7208">
|
||||
<path
|
||||
id="path7210"
|
||||
d="M112.715,74.519l55.15,259.463l39.438,-8.383l-55.15,-259.463l-39.438,8.383Z"
|
||||
style="
|
||||
fill: #f89d00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7212">
|
||||
<path
|
||||
id="path7214"
|
||||
d="M112.715,74.519l8.464,39.818l39.437,-8.382l-8.463,-39.819l-39.438,8.383Z"
|
||||
style="
|
||||
fill: #d58700;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7216">
|
||||
<path
|
||||
id="path7218"
|
||||
d="M207.304,325.599l-55.151,-259.463l-39.437,8.383l55.15,259.463"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7220">
|
||||
<path
|
||||
id="path7222"
|
||||
d="M78.998,49.805l-32.558,6.92c-1.252,0.267 -2.484,-0.533 -2.75,-1.786l-1.96,-9.222c-0.291,-1.369 -1.636,-2.242 -3.005,-1.952l-24.19,5.143c-1.369,0.29 -2.242,1.635 -1.951,3.003l11.328,53.293c0.291,1.37 1.636,2.243 3.004,1.952l24.19,-5.142c1.369,-0.291 2.243,-1.636 1.952,-3.005l-1.961,-9.222c-0.266,-1.253 0.534,-2.484 1.786,-2.751l32.558,-6.919l-6.443,-30.312Z"
|
||||
style="
|
||||
fill: #b7cee9;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7224">
|
||||
<path
|
||||
id="path7226"
|
||||
d="M78.998,49.805l-32.558,6.92c-1.252,0.267 -2.484,-0.533 -2.75,-1.786l-1.96,-9.222c-0.291,-1.369 -1.636,-2.242 -3.005,-1.952l-24.19,5.143c-1.369,0.29 -2.242,1.635 -1.951,3.003l11.328,53.293c0.291,1.37 1.636,2.243 3.004,1.952l24.19,-5.142c1.369,-0.291 2.243,-1.636 1.952,-3.005l-1.961,-9.222c-0.266,-1.253 0.534,-2.484 1.786,-2.751l32.558,-6.919l-6.443,-30.312Z"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7228">
|
||||
<path
|
||||
id="path7230"
|
||||
d="M164.359,17.061l-91.655,19.481c-3.116,0.663 -5.105,3.726 -4.443,6.843l9.982,46.963c0.663,3.116 3.726,5.107 6.843,4.443l110.229,-23.429c4.739,-1.008 9.577,1.218 11.896,5.472l8.86,16.253c1.528,2.802 5.038,3.835 7.84,2.308l7.786,-3.384c7.942,-4.329 11.079,-13.3 6.748,-21.242l-14.286,-26.203c-11.658,-21.383 -35.978,-32.568 -59.8,-27.505"
|
||||
style="
|
||||
fill: #d7e3f2;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g72281" serif:id="g7228">
|
||||
<path
|
||||
id="path72301"
|
||||
serif:id="path7230"
|
||||
d="M164.359,17.061l-91.655,19.481c-3.116,0.663 -5.105,3.726 -4.443,6.843l9.982,46.963c0.663,3.116 3.726,5.107 6.843,4.443l110.229,-23.429c4.739,-1.008 9.577,1.218 11.896,5.472l8.86,16.253c1.528,2.802 5.038,3.835 7.84,2.308l7.786,-3.384c7.942,-4.329 11.079,-13.3 6.748,-21.242l-14.286,-26.203c-11.658,-21.383 -35.978,-32.568 -59.8,-27.505"
|
||||
style="
|
||||
fill: #d7e3f2;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7232">
|
||||
<path
|
||||
id="path7234"
|
||||
d="M102.999,30.103l-30.294,6.439c-3.117,0.663 -5.106,3.726 -4.444,6.843l9.983,46.963c0.662,3.116 3.726,5.107 6.842,4.443l110.229,-23.429c4.739,-1.008 9.578,1.218 11.896,5.472l8.86,16.253c1.528,2.802 5.038,3.835 7.84,2.308l7.786,-3.384c7.943,-4.329 11.079,-13.3 6.749,-21.242l-14.287,-26.203c-11.658,-21.383 -35.977,-32.568 -59.8,-27.505l-26.359,5.603"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7240"></g>
|
||||
<g id="g7244">
|
||||
<path
|
||||
id="path7246"
|
||||
d="M553.5,295.445l-497,0l-0,172.631c-0,3.905 3.166,7.07 7.071,7.07l482.858,0c3.905,0 7.071,-3.165 7.071,-7.07l-0,-172.631Z"
|
||||
style="
|
||||
fill: #e00000;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7248">
|
||||
<path
|
||||
id="path7250"
|
||||
d="M546.429,444.992l-482.857,0c-3.906,0 -7.072,-3.166 -7.072,-7.071l-0,30.155c-0,3.905 3.166,7.07 7.072,7.07l482.857,0c3.905,0 7.071,-3.165 7.071,-7.07l-0,-30.155c-0,3.905 -3.166,7.071 -7.071,7.071"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7252">
|
||||
<path
|
||||
id="path7254"
|
||||
d="M553.5,295.445l-497,0l-0,172.631c-0,3.905 3.166,7.07 7.071,7.07l482.858,0c3.905,0 7.071,-3.165 7.071,-7.07l-0,-172.631Z"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7256">
|
||||
<path
|
||||
id="path7258"
|
||||
d="M249.87,347.278l-0,33c-0,2.762 2.239,5 5,5l100.261,0c2.761,0 5,-2.238 5,-5l-0,-33c-0,-2.762 -2.239,-5 -5,-5l-100.261,0c-2.761,0 -5,2.238 -5,5"
|
||||
style="
|
||||
fill: #b00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g id="g7260">
|
||||
<path
|
||||
id="path7262"
|
||||
d="M249.87,347.278l-0,33c-0,2.762 2.239,5 5,5l100.261,0c2.761,0 5,-2.238 5,-5l-0,-33c-0,-2.762 -2.239,-5 -5,-5l-100.261,0c-2.761,0 -5,2.238 -5,5Z"
|
||||
style="
|
||||
fill: none;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
id="path7148"
|
||||
x="89.294"
|
||||
y="133.507"
|
||||
width="431.412"
|
||||
height="36.366"
|
||||
style="
|
||||
fill: #7d8c97;
|
||||
fill-rule: nonzero;
|
||||
stroke: #000;
|
||||
stroke-width: 15px;
|
||||
" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<clipPath id="_clip1">
|
||||
<rect
|
||||
x="48.391"
|
||||
y="1"
|
||||
width="554.191"
|
||||
height="287.599" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<clipPath id="_clip2">
|
||||
<polygon
|
||||
points="122.46,223.269 389.191,141.565 470.894,408.297 204.163,490 122.46,223.269 " />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip2)">
|
||||
<path
|
||||
d="M401.112,309.199l20.426,-6.257c5.753,-1.762 8.987,-7.856 7.222,-13.609l-7.905,-25.756c-1.764,-5.748 -7.853,-8.979 -13.602,-7.218l-20.445,6.262c-5.67,-9.632 -12.768,-18.137 -20.928,-25.318l10.016,-18.862c2.822,-5.314 0.8,-11.909 -4.515,-14.729l-23.799,-12.627c-5.313,-2.819 -11.904,-0.798 -14.724,4.512l-10.021,18.874c-10.513,-2.725 -21.525,-3.831 -32.67,-3.127l-6.26,-20.436c-1.762,-5.751 -7.852,-8.985 -13.603,-7.223l-25.76,7.89c-5.752,1.762 -8.986,7.852 -7.224,13.604l6.26,20.435c-9.628,5.659 -18.132,12.743 -25.314,20.889l-18.874,-10.022c-5.311,-2.82 -11.903,-0.803 -14.725,4.508l-12.644,23.789c-2.824,5.313 -0.805,11.909 4.509,14.731l18.862,10.016c-2.738,10.52 -3.855,21.541 -3.158,32.697l-20.446,6.262c-5.748,1.761 -8.983,7.848 -7.225,13.598l7.876,25.764c1.76,5.755 7.852,8.992 13.605,7.23l20.427,-6.257c5.665,9.653 12.762,18.178 20.925,25.376l-10.022,18.873c-2.821,5.311 -0.803,11.903 4.507,14.725l23.79,12.645c5.314,2.823 11.909,0.805 14.73,-4.509l10.017,-18.862c10.542,2.744 21.588,3.86 32.768,3.153l6.26,20.436c1.762,5.751 7.852,8.985 13.603,7.223l25.76,-7.89c5.752,-1.762 8.986,-7.852 7.224,-13.603l-6.26,-20.436c9.658,-5.677 18.185,-12.788 25.381,-20.965l18.862,10.016c5.314,2.822 11.909,0.8 14.729,-4.515l12.627,-23.8c2.819,-5.312 0.798,-11.903 -4.513,-14.723l-18.873,-10.022c2.731,-10.535 3.837,-21.572 3.124,-32.742Z"
|
||||
style="
|
||||
fill: #ffdb05;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M276.901,251.218c35.634,-10.915 73.425,9.154 84.34,44.787c10.915,35.634 -9.153,73.426 -44.787,84.341c-35.633,10.915 -73.425,-9.153 -84.34,-44.787c-10.915,-35.634 9.153,-73.426 44.787,-84.341Z"
|
||||
style="fill: #b3b6c3" />
|
||||
<path
|
||||
d="M283.918,274.128c22.99,-7.042 47.372,5.905 54.414,28.895c7.042,22.989 -5.906,47.371 -28.895,54.413c-22.99,7.042 -47.371,-5.905 -54.413,-28.895c-7.042,-22.989 5.905,-47.371 28.894,-54.413Z"
|
||||
style="fill: #fff" />
|
||||
<path
|
||||
d="M318.049,385.553c-18.636,5.708 -38.38,3.818 -55.594,-5.323c-17.215,-9.142 -29.839,-24.44 -35.548,-43.076c-5.708,-18.636 -3.818,-38.38 5.324,-55.595c14.472,-27.253 44.692,-42.511 75.198,-37.969c2.974,0.443 5.026,3.213 4.584,6.188c-0.444,2.974 -3.214,5.026 -6.189,4.584c-25.952,-3.865 -51.662,9.118 -63.975,32.305c-7.777,14.645 -9.386,31.442 -4.529,47.298c4.856,15.855 15.597,28.87 30.242,36.646c14.645,7.777 31.442,9.386 47.297,4.529c15.855,-4.857 28.87,-15.597 36.646,-30.242c12.783,-24.071 8.534,-53.276 -10.571,-72.676c-2.111,-2.142 -2.084,-5.59 0.059,-7.701c2.142,-2.111 5.59,-2.084 7.701,0.059c10.813,10.98 17.77,24.873 20.121,40.179c2.397,15.612 -0.262,31.258 -7.69,45.247c-9.141,17.214 -24.439,29.838 -43.076,35.547Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M330.222,261.501c-1.342,0.411 -2.841,0.306 -4.174,-0.411l-0.255,-0.136c-2.656,-1.41 -3.666,-4.707 -2.256,-7.364c1.411,-2.656 4.707,-3.665 7.364,-2.255l0.306,0.164c2.649,1.424 3.641,4.726 2.216,7.375c-0.708,1.315 -1.878,2.221 -3.201,2.627Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M311.144,362.648c-5.021,1.538 -10.346,2.282 -15.812,2.136c-3.006,-0.08 -5.378,-2.583 -5.298,-5.589c0.081,-3.007 2.583,-5.38 5.589,-5.298c14.58,0.389 27.884,-7.364 34.72,-20.237c4.776,-8.992 5.764,-19.306 2.782,-29.042c-2.983,-9.735 -9.577,-17.726 -18.57,-22.502c-8.993,-4.775 -19.306,-5.763 -29.042,-2.781c-9.736,2.982 -17.727,9.577 -22.502,18.57c-6.772,12.752 -5.815,28.016 2.496,39.837c1.73,2.46 1.138,5.857 -1.322,7.586c-2.46,1.731 -5.856,1.139 -7.586,-1.322c-10.683,-15.194 -11.912,-34.816 -3.206,-51.209c6.139,-11.563 16.413,-20.041 28.931,-23.875c12.517,-3.835 25.777,-2.565 37.34,3.575c11.562,6.14 20.041,16.414 23.875,28.931c3.834,12.518 2.564,25.778 -3.576,37.341c-6.225,11.722 -16.623,20.143 -28.819,23.879Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M278.051,359.577c-1.325,0.405 -2.803,0.309 -4.126,-0.386l-0.233,-0.123c-2.656,-1.411 -3.666,-4.707 -2.256,-7.364c1.411,-2.656 4.707,-3.666 7.364,-2.256l0.188,0.1c2.663,1.398 3.688,4.689 2.29,7.352c-0.703,1.34 -1.886,2.266 -3.227,2.677Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M350.408,445.197l-25.761,7.89c-8.613,2.639 -17.767,-2.222 -20.405,-10.835l-5.026,-16.409c-8.725,0.207 -17.424,-0.631 -25.964,-2.499l-8.039,15.139c-2.047,3.854 -5.472,6.68 -9.644,7.958c-0.001,0.001 -0.001,0.001 -0.002,0.001c-4.174,1.278 -8.595,0.853 -12.449,-1.195l-23.79,-12.645c-7.951,-4.226 -10.985,-14.134 -6.761,-22.088l8.047,-15.154c-6.317,-6.026 -11.871,-12.761 -16.576,-20.1l-16.397,5.022c-4.174,1.279 -8.595,0.855 -12.45,-1.194c-3.855,-2.048 -6.681,-5.475 -7.958,-9.65l-7.876,-25.765c-2.632,-8.61 2.23,-17.759 10.838,-20.396l16.421,-5.03c-0.2,-8.7 0.637,-17.376 2.5,-25.89l-15.139,-8.039c-3.854,-2.048 -6.681,-5.474 -7.959,-9.647c-1.278,-4.174 -0.853,-8.594 1.195,-12.449l12.645,-23.79c4.225,-7.952 14.134,-10.985 22.087,-6.761l15.154,8.047c6.009,-6.299 12.724,-11.84 20.039,-16.537l-5.026,-16.407c-2.638,-8.613 2.223,-17.767 10.836,-20.406l25.761,-7.89c8.613,-2.639 17.767,2.222 20.405,10.835l5.026,16.409c8.691,-0.205 17.357,0.624 25.863,2.476l8.047,-15.154c4.224,-7.953 14.131,-10.99 22.085,-6.769l23.8,12.627c3.856,2.046 6.684,5.47 7.962,9.644c1.279,4.173 0.857,8.594 -1.19,12.449l-8.04,15.139c6.312,6.009 11.865,12.728 16.572,20.048l16.421,-5.03c8.608,-2.637 17.761,2.221 20.402,10.827l7.905,25.756c1.281,4.173 0.86,8.595 -1.187,12.451c-2.046,3.856 -5.472,6.684 -9.646,7.963l-16.396,5.022c0.212,8.715 -0.617,17.405 -2.475,25.936l15.154,8.047c7.954,4.224 10.99,14.131 6.769,22.085l-12.626,23.8c-2.046,3.856 -5.471,6.684 -9.644,7.962c-0,0.001 -0.002,0.001 -0.003,0.001c-4.172,1.278 -8.592,0.855 -12.446,-1.192l-15.139,-8.039c-6.027,6.33 -12.766,11.897 -20.11,16.612l5.027,16.408c2.636,8.613 -2.224,17.767 -10.837,20.406Zm-81.581,-33.338c0.94,-0.287 1.962,-0.323 2.965,-0.062c10.156,2.643 20.604,3.648 31.054,2.988c2.515,-0.159 4.812,1.43 5.55,3.84l6.26,20.436c0.879,2.871 3.931,4.492 6.802,3.612l25.76,-7.891c2.871,-0.879 4.492,-3.931 3.612,-6.802l-6.259,-20.435c-0.739,-2.41 0.274,-5.012 2.447,-6.29c9.026,-5.306 17.118,-11.991 24.052,-19.869c1.666,-1.893 4.415,-2.394 6.642,-1.212l18.862,10.016c1.285,0.682 2.759,0.823 4.15,0.397c1.391,-0.426 2.532,-1.369 3.214,-2.654l12.627,-23.8c1.407,-2.651 0.395,-5.953 -2.256,-7.361l-18.874,-10.023c-2.225,-1.181 -3.349,-3.736 -2.717,-6.175c2.631,-10.149 3.627,-20.588 2.96,-31.029c-0.161,-2.517 1.428,-4.816 3.84,-5.555l20.426,-6.256c1.392,-0.427 2.533,-1.369 3.215,-2.654c0.683,-1.286 0.823,-2.76 0.396,-4.15l-7.905,-25.757c-0.88,-2.869 -3.931,-4.487 -6.801,-3.608l-20.445,6.262c-2.408,0.738 -5.009,-0.273 -6.288,-2.444c-5.301,-9.004 -11.973,-17.076 -19.834,-23.993c-1.893,-1.667 -2.394,-4.415 -1.212,-6.642l10.016,-18.862c0.683,-1.285 0.823,-2.759 0.397,-4.15c-0.426,-1.391 -1.369,-2.532 -2.654,-3.214l-23.799,-12.627c-2.652,-1.407 -5.954,-0.396 -7.362,2.256l-10.022,18.875c-1.181,2.225 -3.736,3.35 -6.176,2.717c-10.125,-2.625 -20.541,-3.622 -30.96,-2.964c-2.516,0.159 -4.812,-1.429 -5.551,-3.84l-6.26,-20.436c-0.879,-2.871 -3.931,-4.491 -6.802,-3.612l-25.76,7.891c-2.871,0.88 -4.491,3.931 -3.612,6.802l6.26,20.436c0.738,2.41 -0.274,5.012 -2.448,6.29c-8.999,5.29 -17.071,11.95 -23.989,19.795c-1.667,1.89 -4.413,2.389 -6.638,1.208l-18.873,-10.022c-2.652,-1.408 -5.954,-0.397 -7.363,2.253l-12.645,23.791c-0.683,1.285 -0.824,2.758 -0.398,4.149c0.426,1.391 1.368,2.534 2.653,3.216l18.862,10.016c2.227,1.183 3.351,3.74 2.716,6.181c-2.638,10.134 -3.645,20.558 -2.993,30.986c0.157,2.514 -1.431,4.808 -3.841,5.546l-20.443,6.262c-2.869,0.879 -4.49,3.929 -3.613,6.8l7.877,25.764c0.426,1.391 1.367,2.534 2.652,3.216c1.285,0.683 2.759,0.824 4.15,0.398l20.427,-6.257c2.411,-0.738 5.014,0.276 6.291,2.451c5.295,9.023 11.968,17.114 19.831,24.048c1.89,1.667 2.39,4.413 1.208,6.638l-10.022,18.874c-1.408,2.651 -0.397,5.954 2.253,7.362l23.791,12.645c1.285,0.683 2.758,0.824 4.149,0.398c0,-0 0.001,-0 0.001,-0c1.391,-0.426 2.532,-1.368 3.215,-2.653l10.016,-18.862c0.696,-1.312 1.87,-2.241 3.216,-2.654Z"
|
||||
style="fill-rule: nonzero" />
|
||||
</g>
|
||||
<path
|
||||
d="M331.028,101.547l46.069,15.854c-0,-0 31.781,-6.692 34.148,-47.156c2.41,-40.449 -35.739,-58.435 -35.739,-58.435c-0.711,18.016 -5.801,18.692 -20.98,31.049c-15.178,12.356 -33.75,37.677 -23.498,58.688Z"
|
||||
style="
|
||||
fill: #7c5748;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M359.686,111.409l17.412,5.992c0,0 31.782,-6.691 34.148,-47.156c2.411,-40.449 -35.738,-58.434 -35.738,-58.434c32.273,36.893 -5.32,85.875 -15.822,99.598Z"
|
||||
style="
|
||||
fill: #5f4c44;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M407.419,88.985c1.997,-5.238 3.393,-11.41 3.827,-18.74c2.41,-40.449 -35.739,-58.434 -35.739,-58.434c-0.712,18.015 -5.801,18.691 -20.98,31.048c-2.503,2.052 -5.126,4.452 -7.65,7.129c2.17,2.446 3.834,6.078 3.708,11.522c0,0 14.139,-2.418 26.644,1.885c2.997,1.032 5.203,2.665 7.143,4.644c6.184,6.304 9.196,16.179 23.047,20.946Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M407.419,88.985c1.997,-5.238 3.393,-11.41 3.827,-18.74c2.41,-40.449 -35.739,-58.434 -35.739,-58.434c15.513,17.722 14.878,38.192 8.865,56.228c6.184,6.304 9.196,16.179 23.047,20.946Z"
|
||||
style="
|
||||
fill: #0094d8;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M321.254,134.516l47.508,16.35c3.765,1.295 7.868,-0.707 9.163,-4.472l6.488,-18.85c1.296,-3.766 -0.706,-7.869 -4.472,-9.164l-47.508,-16.349c-3.765,-1.296 -7.868,0.706 -9.163,4.471l-6.488,18.851c-1.296,3.765 0.706,7.867 4.472,9.163Z"
|
||||
style="
|
||||
fill: #c0c9d2;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M365.308,121.077l-6.42,18.657c-1.314,3.819 -5.476,5.849 -9.295,4.535l19.072,6.563c3.819,1.314 7.98,-0.716 9.294,-4.535l6.421,-18.657c1.314,-3.818 -0.716,-7.979 -4.535,-9.294l-19.072,-6.563c3.819,1.314 5.849,5.475 4.535,9.294Z"
|
||||
style="
|
||||
fill: #a6aeba;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M324.347,135.58c-11.322,7.729 -23.596,27.224 -38.115,69.412c-25.168,73.133 -35.058,150.447 -16.621,156.791c18.437,6.345 58.219,-60.681 83.387,-133.814c14.518,-42.188 16.842,-65.108 12.674,-78.168"
|
||||
style="
|
||||
fill: #f89e00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M336.293,222.22c14.518,-42.187 19.332,-64.251 19.042,-75.976l10.336,3.557c4.169,13.06 1.845,35.979 -12.674,78.168c-25.168,73.133 -64.949,140.158 -83.387,133.814c9.214,3.17 41.515,-66.43 66.683,-139.563Z"
|
||||
style="
|
||||
fill: #d38302;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M268.259,365.699c7.978,2.746 17.565,-3.002 29.311,-17.572c9.288,-11.519 19.504,-28.127 29.666,-48.198c1.039,-2.053 0.203,-4.563 -1.858,-5.584c-2.039,-1.01 -4.511,-0.181 -5.539,1.851c-24.764,48.965 -42.73,63.785 -48.883,61.667c-4.802,-1.652 -9.061,-15.466 -5.268,-48.963c3.376,-29.812 12.292,-67.194 24.461,-102.559c12.241,-35.571 23.68,-57.245 34.909,-66.134l37.205,12.804c3.381,13.915 -0.94,38.038 -13.182,73.611c-6.071,17.64 -13.273,35.637 -20.923,52.308c-0.947,2.063 -0.045,4.504 2.011,5.467l0.001,0c2.086,0.978 4.566,0.064 5.528,-2.031c7.76,-16.91 15.063,-35.162 21.219,-53.047c11.784,-34.244 16.388,-58.069 14.209,-73.928c4.703,-0.013 9.103,-2.957 10.718,-7.648l6.487,-18.852c1.289,-3.745 0.493,-7.707 -1.76,-10.633c2.175,-1.078 4.612,-2.487 7.124,-4.31c1.975,-1.432 2.296,-4.251 0.69,-6.087l-0.001,-0c-1.42,-1.623 -3.833,-1.867 -5.58,-0.603c-4.971,3.597 -9.556,5.235 -11.518,5.826l-43.209,-14.869c-5.449,-13.415 2.155,-29.18 11.956,-41.016c0.302,1.235 0.446,2.589 0.409,4.068c-0.018,0.708 0.09,1.422 0.406,2.055c0.858,1.719 2.675,2.571 4.43,2.271c0.131,-0.023 13.226,-2.195 24.597,1.719c4.422,1.521 6.85,4.886 9.924,9.144c3.522,4.882 7.796,10.799 16.226,14.799c-0.487,1.023 -1.008,2.019 -1.562,2.987c-1.058,1.851 -0.504,4.206 1.246,5.424l0.003,0.002c1.999,1.39 4.759,0.766 5.968,-1.348c4.508,-7.88 7.105,-17.207 7.732,-27.794c1.046,-17.73 -5.222,-34.16 -18.13,-47.513c-9.647,-9.982 -19.558,-14.717 -19.974,-14.914c-1.143,-0.538 -2.461,-0.524 -3.585,0.024c-0.112,0.055 -0.223,0.115 -0.332,0.181c-1.188,0.72 -1.936,1.988 -1.992,3.377c-0.529,13.196 -2.91,15.039 -12.929,22.801c-1.925,1.491 -4.106,3.181 -6.546,5.17c-9.257,7.544 -17.414,17.555 -22.381,27.468c-5.619,11.215 -7.016,21.958 -4.146,31.399c-0.096,0.044 -0.195,0.079 -0.291,0.125c-2.726,1.33 -4.77,3.642 -5.757,6.51l-6.488,18.851c-1.614,4.692 0.043,9.722 3.743,12.625c-11.479,11.161 -22.51,32.773 -34.294,67.016c-12.357,35.912 -21.417,73.936 -24.859,104.322c-3.904,34.496 -0.269,53.919 10.808,57.731Zm88.902,-319.624c2.36,-1.924 4.499,-3.58 6.386,-5.042c9.052,-7.013 13.907,-10.773 15.56,-22.321c9.841,6.455 29.701,23.083 28.033,51.325c-0.283,4.8 -1.019,9.281 -2.195,13.428c-6.063,-3.044 -9.17,-7.346 -12.426,-11.858c-3.461,-4.796 -7.039,-9.755 -13.947,-12.133c-9.004,-3.098 -18.626,-2.998 -24.131,-2.572c-0.373,-2.353 -1.104,-4.505 -2.184,-6.446c1.651,-1.604 3.301,-3.074 4.904,-4.381Zm-36.46,80.626l6.487,-18.852c0.267,-0.775 0.818,-1.399 1.555,-1.758c0.736,-0.36 1.567,-0.411 2.343,-0.143l47.508,16.348c1.6,0.551 2.453,2.3 1.903,3.899l-6.488,18.851c-0.275,0.8 -0.849,1.413 -1.555,1.758c-0.706,0.344 -1.543,0.42 -2.343,0.144l-47.509,-16.35c-1.599,-0.55 -2.452,-2.298 -1.901,-3.897Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<clipPath id="_clip3">
|
||||
<polygon
|
||||
points="338.743,110.391 329.8,355.224 84.967,346.281 93.91,101.448 338.743,110.391 " />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip3)">
|
||||
<path
|
||||
d="M199.991,236.632l21.148,-19.657c2.063,-1.919 2.181,-5.148 0.263,-7.213l-67.366,-72.473l0.295,-8.09c0.071,-1.93 -0.954,-3.735 -2.649,-4.663l-40.059,-21.893c-1.928,-1.052 -4.313,-0.752 -5.921,0.744l-10.574,9.828c-1.61,1.495 -2.082,3.852 -1.174,5.851l18.912,41.551c0.802,1.757 2.527,2.911 4.458,2.982l8.089,0.295l67.365,72.475c1.919,2.064 5.148,2.182 7.213,0.263Z"
|
||||
style="
|
||||
fill: #cfd8dc;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M315.572,345.996l6.207,-5.769c11.646,-11.351 12.322,-29.849 1.535,-42.019l-61.852,-66.563c-0.923,-0.99 -2.203,-1.574 -3.557,-1.621c-8.451,-0.309 -15.051,-7.41 -14.743,-15.861c0.052,-1.354 -0.436,-2.672 -1.357,-3.666l-9.829,-10.573c-1.919,-2.064 -5.147,-2.182 -7.212,-0.264l-42.296,39.315c-2.064,1.919 -2.182,5.147 -0.263,7.212l9.828,10.574c0.92,0.992 2.195,1.579 3.547,1.631c8.451,0.309 15.051,7.41 14.743,15.861c-0.052,1.354 0.436,2.673 1.357,3.666l61.862,66.542c11.183,12.019 29.99,12.706 42.02,1.535l0.01,0Z"
|
||||
style="
|
||||
fill: #ff02ad;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<g>
|
||||
<path
|
||||
d="M241.858,249.868c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.443c1.955,2.032 1.893,5.262 -0.138,7.218c-2.031,1.954 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="
|
||||
fill: #c62828;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M225.997,264.611c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.444c1.955,2.031 1.893,5.262 -0.138,7.217c-2.031,1.955 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="
|
||||
fill: #c62828;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<path
|
||||
d="M180.837,242.532c0.047,-1.354 0.63,-2.634 1.621,-3.557l42.296,-39.315c2.064,-1.918 5.293,-1.8 7.212,0.264l9.829,10.574c0.924,0.991 1.416,2.31 1.367,3.666c-0.308,8.451 6.292,15.552 14.743,15.86c1.354,0.048 2.634,0.631 3.557,1.622l61.852,66.552c10.627,12.227 9.956,30.599 -1.535,42.019l-6.217,5.779c-12.03,11.171 -30.837,10.484 -42.02,-1.535l-61.861,-66.552c-0.919,-0.991 -1.407,-2.305 -1.358,-3.656c0.308,-8.451 -6.292,-15.552 -14.743,-15.861c-1.354,-0.047 -2.634,-0.63 -3.557,-1.621l-9.829,-10.574c-0.921,-0.993 -1.409,-2.312 -1.357,-3.665Zm47.133,-31.917l-34.821,32.366l5.013,5.393c12.293,1.528 21.714,11.663 22.341,24.035l60.515,65.104c7.345,7.891 19.693,8.342 27.595,1.008l6.217,-5.78c7.536,-7.503 7.976,-19.561 1.008,-27.594l-60.515,-65.104c-12.293,-1.527 -21.714,-11.663 -22.341,-24.035l-5.012,-5.393Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M93.496,116.762c0.047,-1.354 0.63,-2.633 1.622,-3.557l10.573,-9.829c1.608,-1.496 3.993,-1.795 5.921,-0.743l40.06,21.893c1.702,0.927 2.732,2.737 2.659,4.673l-0.295,8.09l67.366,72.474c1.883,2.097 1.709,5.324 -0.389,7.208c-2.046,1.836 -5.18,1.722 -7.086,-0.259l-68.802,-74.018c-0.924,-0.992 -1.416,-2.311 -1.367,-3.666l0.257,-7.049l-34.051,-18.61l-5.181,4.816l16.076,35.318l7.039,0.257c1.353,0.047 2.633,0.63 3.557,1.622l68.8,74.017c1.883,2.098 1.709,5.325 -0.389,7.208c-2.046,1.837 -5.18,1.722 -7.086,-0.259l-67.368,-72.453l-8.089,-0.295c-1.93,-0.071 -3.656,-1.225 -4.457,-2.982l-18.912,-41.551c-0.33,-0.722 -0.487,-1.511 -0.458,-2.305Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M241.858,249.868c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.443c1.955,2.032 1.893,5.262 -0.138,7.218c-2.031,1.954 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M225.997,264.611c0.098,-2.817 2.461,-5.022 5.279,-4.924c1.358,0.047 2.641,0.634 3.565,1.631l58.972,63.444c1.955,2.031 1.893,5.262 -0.138,7.217c-2.031,1.955 -5.262,1.892 -7.217,-0.139c-0.041,-0.042 -0.081,-0.085 -0.121,-0.129l-58.972,-63.444c-0.922,-0.99 -1.414,-2.304 -1.368,-3.656Z"
|
||||
style="fill-rule: nonzero" />
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M562.573,120.232l-87.881,207.058c-10.861,25.591 -40.411,37.532 -66.002,26.671c-25.591,-10.862 -37.532,-40.412 -26.671,-66.003l87.881,-207.058l92.673,39.332Z"
|
||||
style="
|
||||
fill: #dfe9f4;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M419.861,198.798l-37.842,89.16c-10.861,25.591 1.08,55.141 26.671,66.002c25.59,10.861 55.14,-1.079 66.002,-26.67l53.493,-126.037l-108.324,-2.455Z"
|
||||
style="
|
||||
fill: #24c100;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M528.18,201.254l-53.499,126.05c-10.864,25.596 -40.385,37.525 -65.981,26.662c-16.865,-7.158 -27.793,-22.426 -30.186,-39.277c4.202,3.899 9.15,7.192 14.756,9.571c25.596,10.864 55.163,-1.045 66.027,-26.642l41.172,-97.008l27.711,0.644Z"
|
||||
style="
|
||||
fill: #21af00;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M578.717,120.131l12.276,-28.925c1.381,-3.254 -0.137,-7.011 -3.391,-8.392l-108.177,-45.912c-3.253,-1.381 -7.01,0.137 -8.391,3.39l-12.277,28.926c-1.381,3.254 0.137,7.011 3.391,8.392l108.177,45.913c3.253,1.38 7.01,-0.138 8.392,-3.392Z"
|
||||
style="
|
||||
fill: #2dcef6;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M590.997,91.198l-12.288,28.95c-1.365,3.217 -5.116,4.771 -8.378,3.386l-108.175,-45.912c-3.263,-1.385 -4.751,-5.162 -3.385,-8.378l3.12,-7.353l100.179,42.518c3.263,1.385 7.013,-0.169 8.398,-3.431l9.147,-21.552l7.996,3.393c3.263,1.384 4.77,5.116 3.386,8.379Z"
|
||||
style="
|
||||
fill: #1ec5e0;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M523.331,129.908c7.056,2.995 10.353,11.155 7.359,18.21c-2.995,7.056 -11.155,10.353 -18.21,7.359c-7.056,-2.995 -10.353,-11.155 -7.359,-18.21c2.995,-7.056 11.155,-10.353 18.21,-7.359Z"
|
||||
style="
|
||||
fill: #21af00;
|
||||
" />
|
||||
<path
|
||||
d="M470.558,233.05c7.624,3.236 11.187,12.053 7.951,19.677c-3.235,7.624 -12.052,11.186 -19.676,7.951c-7.624,-3.236 -11.187,-12.053 -7.951,-19.677c3.236,-7.624 12.053,-11.186 19.676,-7.951Z"
|
||||
style="
|
||||
fill: #dfe9f4;
|
||||
" />
|
||||
<path
|
||||
d="M428.819,292.984c6.059,2.571 8.891,9.579 6.319,15.638c-2.571,6.059 -9.578,8.89 -15.638,6.318c-6.059,-2.571 -8.89,-9.578 -6.318,-15.637c2.571,-6.059 9.578,-8.891 15.637,-6.319Z"
|
||||
style="
|
||||
fill: #dfe9f4;
|
||||
" />
|
||||
<g>
|
||||
<path
|
||||
d="M473.695,150.963c5.26,2.232 7.718,8.315 5.485,13.576c-2.232,5.26 -8.315,7.718 -13.575,5.485c-5.261,-2.232 -7.719,-8.315 -5.486,-13.576c2.232,-5.26 8.316,-7.718 13.576,-5.485Z"
|
||||
style="
|
||||
fill: #21af00;
|
||||
" />
|
||||
<g>
|
||||
<path
|
||||
d="M589.555,78.219l-18.628,-7.906c-2.54,-1.078 -5.468,0.105 -6.546,2.645c-1.078,2.539 0.105,5.467 2.645,6.545l18.628,7.907c0.714,0.302 1.045,1.132 0.742,1.845l-12.277,28.927c-0.303,0.714 -1.128,1.047 -1.841,0.744c-48.948,-20.774 -58.962,-25.024 -108.18,-45.913c-0.713,-0.303 -1.051,-1.13 -0.748,-1.843l12.278,-28.928c0.302,-0.713 1.133,-1.049 1.847,-0.746l72.345,30.705c2.54,1.078 5.468,-0.105 6.546,-2.645c1.078,-2.54 -0.105,-5.468 -2.645,-6.546l-72.345,-30.705c-5.785,-2.455 -12.484,0.252 -14.939,6.036l-12.277,28.928c-2.454,5.78 0.253,12.479 6.038,14.934l3.159,1.341l-11.156,26.284c-1.078,2.54 0.106,5.468 2.645,6.546c2.54,1.078 5.468,-0.105 6.546,-2.645l11.156,-26.284l83.479,35.43l-31.114,73.309l-36.489,-0.826c-2.754,-0.062 -5.041,2.119 -5.1,4.879c-0.067,2.753 2.097,5.042 4.879,5.101l32.511,0.738l-50.618,119.264c-9.769,23.017 -36.438,33.794 -59.455,24.025c-23.017,-9.769 -33.794,-36.438 -24.025,-59.455l36.518,-86.042l44.574,1.007c2.754,0.062 5.041,-2.119 5.1,-4.879c0.067,-2.752 -2.103,-5.039 -4.879,-5.1l-40.596,-0.92l25.271,-59.541c1.078,-2.54 -0.105,-5.468 -2.645,-6.546c-2.54,-1.077 -5.468,0.106 -6.546,2.645l-65.987,155.475c-11.919,28.084 1.231,60.627 29.315,72.546c28.083,11.92 60.627,-1.23 72.546,-29.314c19.018,-44.809 66.682,-157.112 85.931,-202.465l3.159,1.341c5.78,2.453 12.479,-0.253 14.933,-6.034l12.277,-28.927c2.455,-5.785 -0.252,-12.484 -6.032,-14.937Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M510.531,160.069c9.585,4.068 20.688,-0.419 24.757,-10.004c4.066,-9.581 -0.42,-20.685 -10.006,-24.753c-9.585,-4.068 -20.689,0.418 -24.755,10c-4.069,9.586 0.418,20.688 10.004,24.757Zm10.85,-25.566c4.515,1.916 6.632,7.147 4.716,11.662c-1.916,4.515 -7.151,6.63 -11.666,4.714c-4.514,-1.916 -6.63,-7.152 -4.714,-11.666c1.916,-4.515 7.15,-6.626 11.664,-4.71Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M483.107,254.679c4.309,-10.151 -0.447,-21.919 -10.598,-26.228c-10.15,-4.308 -21.915,0.449 -26.223,10.6c-4.308,10.151 0.444,21.913 10.596,26.222c10.151,4.308 21.917,-0.443 26.225,-10.594Zm-22.325,1.403c-5.084,-2.158 -7.463,-8.045 -5.305,-13.13c2.158,-5.085 8.047,-7.468 13.132,-5.31c5.084,2.158 7.465,8.052 5.308,13.136c-2.158,5.084 -8.05,7.462 -13.135,5.304Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M430.768,288.391c-8.585,-3.644 -18.538,0.378 -22.181,8.963c-3.644,8.585 0.378,18.538 8.963,22.182c8.585,3.643 18.537,-0.378 22.181,-8.963c3.644,-8.585 -0.378,-18.538 -8.963,-22.182Zm-9.318,21.954c-3.518,-1.493 -5.166,-5.571 -3.673,-9.09c1.494,-3.519 5.572,-5.167 9.09,-3.673c3.519,1.493 5.167,5.571 3.673,9.09c-1.493,3.518 -5.571,5.166 -9.09,3.673Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M483.773,166.486c3.305,-7.786 -0.343,-16.812 -8.128,-20.116c-7.791,-3.307 -16.817,0.341 -20.121,8.126c-3.305,7.786 0.342,16.812 8.133,20.119c7.786,3.304 16.812,-0.343 20.116,-8.129Zm-19.059,-8.089c1.155,-2.719 4.306,-3.993 7.03,-2.836c2.719,1.154 3.993,4.305 2.838,7.024c-1.154,2.72 -4.305,3.993 -7.024,2.839c-2.725,-1.156 -3.998,-4.307 -2.844,-7.027Z"
|
||||
style="
|
||||
fill: #003;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M1338.77,177.49l-14.203,208.687l45.422,0.422l7.453,-101.11l23.063,94.782c5.531,6.187 11.859,7.5 18.984,3.937l43.031,-97.875l-5.765,99.844l45.703,-0.422l12.797,-208.547c-12.563,-3.281 -25.594,-3.187 -39.094,0.282l-57.797,118.406l-32.766,-118.125c-19.5,-5.344 -35.109,-5.438 -46.828,-0.281Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M1536.29,178.474c-3.375,1.594 -5.578,4.219 -6.609,7.875l-12.657,192.797c0.938,5.156 3.422,8.109 7.454,8.859l131.484,-5.203c9.938,-14.531 11.016,-30.094 3.234,-46.687l-94.359,2.812l3.797,-43.594l76.922,-3.234c8.156,-12.188 8.718,-25.125 1.687,-38.813l-75.515,1.547l2.531,-36l91.969,-2.953c6.187,-12.844 6.984,-26.625 2.39,-41.344l-132.328,3.938Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M1829.72,195.068c3.281,16.875 -1.125,30.375 -13.219,40.5c-8.156,-5.25 -20.953,-9.422 -38.39,-12.516c-15.282,-1.5 -27.75,2.93 -37.407,13.289c-9.656,10.359 -15.562,23.789 -17.718,40.289c-3.188,10.406 -1.219,23.766 5.906,40.078c7.125,14.813 17.976,22.641 32.554,23.485c14.579,0.843 32.04,-2.016 52.383,-8.578c7.313,14.437 8.016,28.593 2.11,42.468c-24.094,11.063 -43.641,16.313 -58.641,15.75c-27.656,-3.469 -47.812,-13.5 -60.469,-30.094c-16.875,-22.218 -22.593,-53.578 -17.156,-94.078c4.313,-32.062 14.484,-56.297 30.516,-72.703c20.906,-16.969 46.265,-23.906 76.078,-20.812c20.719,1.312 35.203,8.953 43.453,22.922Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M1845.63,183.958l-12.093,197.578c10.312,7.407 25.546,8.532 45.703,3.375l5.062,-85.781l65.953,-2.812l-4.078,85.922c12.938,7.968 28.078,7.593 45.422,-1.125l9.563,-200.11c-13.313,-7.219 -27.188,-7.312 -41.625,-0.281l-5.344,73.266l-66.797,3.375l4.078,-78.61c-16.875,-6.469 -32.156,-4.734 -45.844,5.203Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2073.74,175.099l-73.688,205.453c10.219,8.719 25.782,11.672 46.688,8.859l12.938,-37.968l52.171,-6.469l10.125,37.687c20.532,3.938 37.219,1.688 50.063,-6.75l-56.25,-202.5c-13.219,-8.156 -27.235,-7.593 -42.047,1.688Zm30.234,130.5l-34.312,4.078l22.078,-69.187l12.234,65.109Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2185.9,183.958l-12.093,197.578c10.312,7.407 25.547,8.532 45.703,3.375l7.875,-115.031l62.859,114.75c16.5,6.563 29.532,5.531 39.094,-3.094l12.094,-200.531c-13.313,-7.219 -27.188,-7.312 -41.625,-0.281l-6.75,110.812l-66.938,-113.484c-20.156,-5.25 -33.562,-3.281 -40.219,5.906Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2340.33,383.083c13.218,6.844 29.39,7.547 48.515,2.11l16.594,-206.86c-16.5,-4.781 -31.875,-5.484 -46.125,-2.109l-18.984,206.859Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2562.53,195.068c3.281,16.875 -1.125,30.375 -13.219,40.5c-8.156,-5.25 -20.953,-9.422 -38.39,-12.516c-15.282,-1.5 -27.75,2.93 -37.407,13.289c-9.656,10.359 -15.562,23.789 -17.718,40.289c-3.188,10.406 -1.219,23.766 5.906,40.078c7.125,14.813 17.976,22.641 32.555,23.485c14.578,0.843 32.039,-2.016 52.382,-8.578c7.313,14.437 8.016,28.593 2.11,42.468c-24.094,11.063 -43.641,16.313 -58.641,15.75c-27.656,-3.469 -47.812,-13.5 -60.469,-30.094c-16.875,-22.218 -22.593,-53.578 -17.156,-94.078c4.313,-32.062 14.484,-56.297 30.516,-72.703c20.906,-16.969 46.265,-23.906 76.078,-20.812c20.719,1.312 35.203,8.953 43.453,22.922Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
<path
|
||||
d="M2572.61,363.536c-6.093,-21.281 -2.531,-37.781 10.688,-49.5c24.094,14.157 41.578,22.875 52.453,26.157c20.25,-0.188 31.359,-5.25 33.328,-15.188c0.469,-7.969 -5.719,-15.656 -18.562,-23.062l-36.141,-17.578c-22.594,-12 -34.313,-28.922 -35.156,-50.766c0.843,-12.563 5.179,-23.859 13.008,-33.891c7.828,-10.031 16.64,-16.617 26.437,-19.758c9.797,-3.14 21.445,-4.289 34.945,-3.445c24.75,1.5 46.453,8.531 65.11,21.094c0.75,17.719 -4.782,33.141 -16.594,46.266c-22.781,-17.063 -41.016,-25.032 -54.703,-23.907c-12.844,-1.5 -19.641,3.094 -20.391,13.782c-0.469,6.093 8.578,13.968 27.141,23.625c21.281,8.625 37.055,18.515 47.32,29.671c10.266,11.157 14.836,24.704 13.711,40.641c-1.5,21.563 -11.531,38.109 -30.094,49.641c-11.812,8.25 -28.875,11.671 -51.187,10.265c-21.281,-2.719 -41.719,-10.734 -61.313,-24.047Z"
|
||||
style="
|
||||
fill: #00a5f1;
|
||||
fill-rule: nonzero;
|
||||
" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="M644.827,363.536c-6.094,-21.281 -2.531,-37.781 10.688,-49.5c24.093,14.157 41.578,22.875 52.453,26.157c20.25,-0.188 31.359,-5.25 33.328,-15.188c0.469,-7.969 -5.719,-15.656 -18.563,-23.062l-36.14,-17.578c-22.594,-12 -34.313,-28.922 -35.157,-50.766c0.844,-12.563 5.18,-23.859 13.008,-33.891c7.828,-10.031 16.641,-16.617 26.438,-19.758c9.797,-3.14 21.445,-4.289 34.945,-3.445c24.75,1.5 46.453,8.531 65.109,21.094c0.75,17.719 -4.781,33.141 -16.593,46.266c-22.782,-17.063 -41.016,-25.032 -54.704,-23.907c-12.843,-1.5 -19.64,3.094 -20.39,13.782c-0.469,6.093 8.578,13.968 27.14,23.625c21.282,8.625 37.055,18.515 47.321,29.671c10.265,11.157 14.836,24.704 13.711,40.641c-1.5,21.563 -11.532,38.109 -30.094,49.641c-11.813,8.25 -28.875,11.671 -51.188,10.265c-21.281,-2.719 -41.718,-10.734 -61.312,-24.047Z"
|
||||
fill="#fff"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M805.297,178.896c-9.563,12.281 -9.141,27.187 1.266,44.719l51.047,-1.125l-9.141,161.437c16.969,6.281 32.578,6.375 46.828,0.281l7.594,-163.547l51.89,-0.703c7.219,-15.094 7.594,-30.14 1.125,-45.14l-150.609,4.078Z"
|
||||
fill="#fff"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M983.767,178.474c-3.375,1.594 -5.578,4.219 -6.609,7.875l-12.657,192.797c0.938,5.156 3.422,8.109 7.454,8.859l131.484,-5.203c9.937,-14.531 11.016,-30.094 3.234,-46.687l-94.359,2.812l3.797,-43.594l76.922,-3.234c8.156,-12.188 8.718,-25.125 1.687,-38.813l-75.515,1.547l2.531,-36l91.969,-2.953c6.187,-12.844 6.984,-26.625 2.39,-41.344l-132.328,3.938Z"
|
||||
fill="#fff"
|
||||
style="fill-rule: nonzero" />
|
||||
<path
|
||||
d="M1136.08,177.49l-14.203,208.687l45.421,0.422l7.454,-101.11l23.062,94.782c5.531,6.187 11.86,7.5 18.985,3.937l43.031,-97.875l-5.766,99.844l45.703,-0.422l12.797,-208.547c-12.562,-3.281 -25.594,-3.187 -39.094,0.282l-57.796,118.406l-32.766,-118.125c-19.5,-5.344 -35.109,-5.438 -46.828,-0.281Z"
|
||||
fill="#fff"
|
||||
style="fill-rule: nonzero" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</router-link>
|
||||
<div class="flex-1"></div>
|
||||
<span class="text-gray-300 text-sm">
|
||||
Made with ❤️ © 2023 STEMMechanics
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
footer ul li a:not([role="button"]) {
|
||||
color: rgba(156, 163, 175);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-items-center justify-center px-8 py-48 text-gray-8">
|
||||
<h2 class="border-r border-gray pr-3 mr-3 font-500">
|
||||
{{ props.status }}
|
||||
</h2>
|
||||
<p>{{ statusText }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const statusText = ref("");
|
||||
|
||||
switch (props.status) {
|
||||
case 403:
|
||||
statusText.value = "You are not permitted to view this page";
|
||||
break;
|
||||
case 404:
|
||||
statusText.value = "This page was not found";
|
||||
break;
|
||||
case 503:
|
||||
statusText.value = "The server is currently under maintenance";
|
||||
break;
|
||||
default:
|
||||
statusText.value = "An unknown error occurred";
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
@@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-justify-center">
|
||||
<div
|
||||
:class="[
|
||||
'flex',
|
||||
'items-center',
|
||||
'border-y-1',
|
||||
'border-l-1',
|
||||
'rounded-l-2',
|
||||
'transition',
|
||||
small
|
||||
? ['text-sm', 'px-2', 'py-1']
|
||||
: ['text-lg', 'px-4', 'py-2'],
|
||||
computedDisablePrevButton
|
||||
? [
|
||||
'bg-gray-2',
|
||||
'text-gray-4',
|
||||
'border-gray-3',
|
||||
'cursor-not-allowed',
|
||||
]
|
||||
: [
|
||||
'hover:bg-sky-200',
|
||||
'cursor-pointer',
|
||||
'bg-white',
|
||||
'border-gray',
|
||||
],
|
||||
]"
|
||||
@click="handleClickPrev">
|
||||
<svg
|
||||
viewBox="0 0 960 960"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="[small ? 'h-4' : 'h-6']">
|
||||
<path
|
||||
d="M648,78l56,57l-343,343l343,343l-56,57l-400,-400l400,-400Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline-block">Prev</span>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'flex',
|
||||
'items-center',
|
||||
'border-y-1',
|
||||
'border-l-1',
|
||||
'border-gray',
|
||||
'transition',
|
||||
small
|
||||
? ['text-sm', 'px-2', 'py-1']
|
||||
: ['text-lg', 'px-4', 'py-2'],
|
||||
page == props.modelValue
|
||||
? ['bg-sky-600', 'text-white']
|
||||
: ['hover:bg-sky-200', 'cursor-pointer', 'bg-white'],
|
||||
]"
|
||||
v-for="(page, idx) of computedPages"
|
||||
:key="idx"
|
||||
@click="handleClickPage(page)">
|
||||
{{ page }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'flex',
|
||||
'items-center',
|
||||
'border-1',
|
||||
'rounded-r-2',
|
||||
'transition',
|
||||
small
|
||||
? ['text-sm', 'px-2', 'py-1']
|
||||
: ['text-lg', 'px-4', 'py-2'],
|
||||
computedDisableNextButton
|
||||
? [
|
||||
'bg-gray-2',
|
||||
'text-gray-4',
|
||||
'border-gray-3',
|
||||
'cursor-not-allowed',
|
||||
]
|
||||
: [
|
||||
'hover:bg-sky-200',
|
||||
'cursor-pointer',
|
||||
'bg-white',
|
||||
'border-gray',
|
||||
],
|
||||
,
|
||||
]"
|
||||
@click="handleClickNext">
|
||||
<span class="hidden sm:inline-block">Next</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
:class="[small ? 'h-4' : 'h-6']">
|
||||
<path
|
||||
d="m304-82-56-57 343-343-343-343 56-57 400 400L304-82Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
perPage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
/**
|
||||
* Returns the pagination info
|
||||
*/
|
||||
const computedPages = computed(() => {
|
||||
let pages = [];
|
||||
|
||||
let pagesRemaining =
|
||||
Math.ceil(props.total / props.perPage) - props.modelValue;
|
||||
let pagesBefore = Math.max(0, props.modelValue - 1);
|
||||
|
||||
if (pagesRemaining + pagesBefore > 4) {
|
||||
if (pagesRemaining < 2) {
|
||||
pagesBefore = Math.min(pagesBefore, 4 - pagesRemaining);
|
||||
} else if (pagesBefore < 2) {
|
||||
pagesRemaining = Math.min(pagesRemaining, 4 - pagesBefore);
|
||||
} else {
|
||||
pagesRemaining = 2;
|
||||
pagesBefore = 2;
|
||||
}
|
||||
}
|
||||
|
||||
for (; pagesBefore > 0; pagesBefore--) {
|
||||
pages.push(props.modelValue - pagesBefore);
|
||||
}
|
||||
pages.push(props.modelValue);
|
||||
for (let i = 1; i <= pagesRemaining; i++) {
|
||||
pages.push(props.modelValue + i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the total number of pages.
|
||||
*/
|
||||
const computedTotalPages = computed(() => {
|
||||
return Math.ceil(props.total / props.perPage);
|
||||
});
|
||||
|
||||
/**
|
||||
* Return if the previous button should be disabled.
|
||||
*/
|
||||
const computedDisablePrevButton = computed(() => {
|
||||
return props.modelValue <= 1;
|
||||
});
|
||||
|
||||
/**
|
||||
* Return if the next button should be disabled.
|
||||
*/
|
||||
const computedDisableNextButton = computed(() => {
|
||||
return props.modelValue >= computedTotalPages.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle click on previous button
|
||||
*/
|
||||
const handleClickPrev = (): void => {
|
||||
if (computedDisablePrevButton.value == false) {
|
||||
emits("update:modelValue", props.modelValue - 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle click on next button
|
||||
*/
|
||||
const handleClickNext = (): void => {
|
||||
if (computedDisableNextButton.value == false) {
|
||||
emits("update:modelValue", props.modelValue + 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle click on page button
|
||||
* @param {number} page The page number to display.
|
||||
*/
|
||||
const handleClickPage = (page: number): void => {
|
||||
emits("update:modelValue", page);
|
||||
};
|
||||
|
||||
const totalPages = computedTotalPages.value;
|
||||
if (props.modelValue < 1 || totalPages < 1) {
|
||||
emits("update:modelValue", 1);
|
||||
} else {
|
||||
if (totalPages < props.modelValue) {
|
||||
emits("update:modelValue", totalPages);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,876 +0,0 @@
|
||||
<template>
|
||||
<SMControl
|
||||
:class="[
|
||||
'control-type-input',
|
||||
{
|
||||
'input-active': active,
|
||||
'has-prepend': slots.prepend,
|
||||
'has-append': slots.append,
|
||||
},
|
||||
props.size,
|
||||
]"
|
||||
:invalid="feedbackInvalid"
|
||||
:no-help="props.noHelp">
|
||||
<div v-if="slots.prepend" class="input-control-prepend">
|
||||
<slot name="prepend"></slot>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<template v-if="props.type == 'checkbox'">
|
||||
<label
|
||||
:class="[
|
||||
'control-label',
|
||||
'control-label-checkbox',
|
||||
{ disabled: disabled },
|
||||
]"
|
||||
v-bind="{ for: id }"
|
||||
><input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
class="checkbox-control"
|
||||
:disabled="disabled"
|
||||
:checked="value"
|
||||
@input="handleCheckbox" />
|
||||
<span class="checkbox-control-box">
|
||||
<span class="checkbox-control-tick"></span> </span
|
||||
>{{ label }}</label
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'range'">
|
||||
<label
|
||||
class="control-label control-label-range"
|
||||
v-bind="{ for: id }"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<input
|
||||
:id="id"
|
||||
type="range"
|
||||
class="range-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
step: props.step,
|
||||
}"
|
||||
:value="value"
|
||||
@input="handleInput" />
|
||||
<span class="range-control-value">{{ value }}</span>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'select'">
|
||||
<label
|
||||
class="control-label control-label-select"
|
||||
v-bind="{ for: id }"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<ion-icon
|
||||
class="select-dropdown-icon"
|
||||
name="caret-down-outline" />
|
||||
<select
|
||||
class="select-input-control"
|
||||
:disabled="disabled"
|
||||
@input="handleInput">
|
||||
<option
|
||||
v-for="option in Object.entries(props.options)"
|
||||
:key="option[0]"
|
||||
:value="option[0]"
|
||||
:selected="option[0] == value">
|
||||
{{ option[1] }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="control-label" v-bind="{ for: id }">{{
|
||||
label
|
||||
}}</label>
|
||||
<template v-if="props.type == 'static'">
|
||||
<div class="static-input-control" v-bind="{ id: id }">
|
||||
<span class="text">
|
||||
{{ value }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'file'">
|
||||
<input
|
||||
:id="id"
|
||||
type="file"
|
||||
class="file-input-control"
|
||||
:accept="props.accept"
|
||||
:disabled="disabled"
|
||||
@change="handleChange" />
|
||||
<div class="file-input-control-value">
|
||||
{{ value?.name ? value.name : value }}
|
||||
</div>
|
||||
<label
|
||||
:class="[
|
||||
'button',
|
||||
'primary',
|
||||
'file-input-control-button',
|
||||
{ disabled: disabled },
|
||||
]"
|
||||
:for="id"
|
||||
>Select file</label
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'textarea'">
|
||||
<ion-icon
|
||||
class="invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<textarea
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||
v-model="value"
|
||||
rows="5"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup"></textarea>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'media'">
|
||||
<div class="media-input-control">
|
||||
<img
|
||||
v-if="mediaUrl?.length > 0"
|
||||
:src="mediaGetVariantUrl(value, 'medium')" />
|
||||
<ion-icon v-else name="image-outline" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ion-icon
|
||||
class="invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<ion-icon
|
||||
v-if="
|
||||
props.showClear &&
|
||||
value?.length > 0 &&
|
||||
!feedbackInvalid
|
||||
"
|
||||
class="clear-icon"
|
||||
name="close-outline"
|
||||
@click.stop="handleClear"></ion-icon>
|
||||
|
||||
<input
|
||||
:type="props.type"
|
||||
class="input-control"
|
||||
:disabled="disabled"
|
||||
v-bind="{
|
||||
id: id,
|
||||
autofocus: props.autofocus,
|
||||
autocomplete:
|
||||
props.type === 'email' ? 'email' : null,
|
||||
spellcheck: props.type === 'email' ? false : null,
|
||||
autocorrect: props.type === 'email' ? 'on' : null,
|
||||
autocapitalize:
|
||||
props.type === 'email' ? 'off' : null,
|
||||
}"
|
||||
v-model="value"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup" />
|
||||
<ul
|
||||
class="autocomplete-list"
|
||||
v-if="computedAutocompleteItems.length > 0 && focused">
|
||||
<li
|
||||
v-for="item in computedAutocompleteItems"
|
||||
:key="item"
|
||||
@mousedown="handleAutocompleteClick(item)">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="slots.append" class="input-control-append">
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
<template v-if="slots.help" #help><slot name="help"></slot></template>
|
||||
</SMControl>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watch, ref, useSlots, computed } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import SMControl from "./SMControl.vue";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
button: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
autocomplete: {
|
||||
type: [Array<string>, Function],
|
||||
default: () => {
|
||||
[];
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: ""
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: ""
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId()
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const active = ref(value.value?.toString().length ?? 0 > 0);
|
||||
const focused = ref(false);
|
||||
const disabled = ref(props.disabled);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
if (props.type === "media") {
|
||||
mediaUrl.value = value.value.url ?? "";
|
||||
}
|
||||
|
||||
active.value =
|
||||
newValue.toString().length > 0 ||
|
||||
newValue instanceof File ||
|
||||
focused.value == true;
|
||||
}
|
||||
);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
watch(
|
||||
() => form.loading(),
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mediaUrl = ref(value.value.url ?? "");
|
||||
|
||||
const handleFocus = () => {
|
||||
active.value = true;
|
||||
focused.value = true;
|
||||
};
|
||||
|
||||
const handleBlur = async () => {
|
||||
active.value = value.value?.length ?? 0 > 0;
|
||||
focused.value = false;
|
||||
emits("blur");
|
||||
|
||||
if (control) {
|
||||
await control.validate();
|
||||
control.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckbox = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.checked;
|
||||
emits("update:modelValue", target.checked);
|
||||
|
||||
if (control) {
|
||||
control.value = target.checked;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (control) {
|
||||
control.value = target.value;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyup = (event: Event) => {
|
||||
emits("keyup", event);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
value.value = "";
|
||||
emits("update:modelValue", "");
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
if (control) {
|
||||
control.value = event.target.files[0];
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelect = async () => {
|
||||
let result = await openDialog(SMDialogMedia);
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
mediaUrl.value = mediaResult.url;
|
||||
emits("update:modelValue", mediaResult);
|
||||
|
||||
if (control) {
|
||||
control.value = mediaResult;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const computedAutocompleteItems = computed(() => {
|
||||
let autocompleteList = [];
|
||||
|
||||
if (props.autocomplete) {
|
||||
if (typeof props.autocomplete === "function") {
|
||||
autocompleteList = props.autocomplete(value.value);
|
||||
} else {
|
||||
autocompleteList = props.autocomplete.filter((str) =>
|
||||
str.includes(value.value)
|
||||
);
|
||||
}
|
||||
|
||||
return autocompleteList.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
return autocompleteList;
|
||||
});
|
||||
|
||||
const handleAutocompleteClick = (item) => {
|
||||
value.value = item;
|
||||
emits("update:modelValue", item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.input-control-prepend {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
& + .control-item .input-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
height: 50px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-item {
|
||||
max-width: 100%;
|
||||
align-items: start;
|
||||
|
||||
.control-label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
transform-origin: top left;
|
||||
transform: translate(16px, 16px) scale(1);
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: var(--base-color-darker);
|
||||
pointer-events: none;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.invalid-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 10px;
|
||||
top: 14px;
|
||||
color: var(--danger-color);
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 18px;
|
||||
background-color: var(--input-clear-icon-color);
|
||||
border-radius: 50%;
|
||||
font-size: 80%;
|
||||
padding: 1px 1px 1px 0px;
|
||||
|
||||
&:hover {
|
||||
color: var(--input-clear-icon-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 20px 16px 10px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--base-color-text);
|
||||
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 92%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--primary-color);
|
||||
z-index: 1;
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
margin: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.static-input-control {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control {
|
||||
opacity: 0;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
}
|
||||
|
||||
.file-input-control-value {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control-button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 30px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.control-label-select {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
|
||||
.select-dropdown-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.select-input-control {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding: 20px 16px 8px 14px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
height: 52px;
|
||||
color: var(--base-color-text);
|
||||
}
|
||||
|
||||
.control-label-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0 16px 32px;
|
||||
pointer-events: all;
|
||||
transform: none;
|
||||
color: var(--base-color-text);
|
||||
|
||||
&.disabled {
|
||||
color: var(--base-color-darker);
|
||||
cursor: not-allowed;
|
||||
|
||||
.checkbox-control-box {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + .checkbox-control-box {
|
||||
.checkbox-control-tick {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control-box {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 2px;
|
||||
background-color: var(--base-color-light);
|
||||
|
||||
.checkbox-control-tick {
|
||||
position: absolute;
|
||||
display: none;
|
||||
border-right: 3px solid var(--base-color-text);
|
||||
border-bottom: 3px solid var(--base-color-text);
|
||||
top: 1px;
|
||||
left: 7px;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-input-control {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
img,
|
||||
ion-icon {
|
||||
display: block;
|
||||
margin: 48px auto 8px auto;
|
||||
border-radius: 8px;
|
||||
font-size: 800%;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-label-range {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.range-control {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.range-control-value {
|
||||
margin-top: 22px;
|
||||
padding-left: 16px;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-append .control-item .input-control {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
&.input-active {
|
||||
.control-item {
|
||||
.control-label:not(.control-label-checkbox) {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.control-invalid {
|
||||
.control-row .control-item {
|
||||
.invalid-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-control {
|
||||
border: 2px solid var(--danger-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
&.input-active {
|
||||
.control-row .control-item .control-label {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.control-row {
|
||||
.control-item {
|
||||
.control-label {
|
||||
transform: translate(16px, 12px) scale(1);
|
||||
}
|
||||
|
||||
.input-control {
|
||||
padding: 16px 8px 4px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
.button {
|
||||
.button-label {
|
||||
ion-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
height: 36px;
|
||||
padding: 3px 24px 13px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.control-group.control-type-input {
|
||||
.control-row {
|
||||
.control-item {
|
||||
.input-control {
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,653 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 flex-align-center">
|
||||
<label class="control-label" v-bind="{ for: id }">{{ label }}</label>
|
||||
<div v-if="value" class="flex flex-justify-center mb-4">
|
||||
<SMLoading v-if="!imgError && !imgLoaded" class="w-48 h-48" small />
|
||||
<svg
|
||||
v-if="imgError && imgLoaded"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-48 text-gray">
|
||||
<path
|
||||
d="M20 17H22V15H20V17M20 7V13H22V7M6 16H11V18H6M6 12H14V14H6M4 2C2.89 2 2 2.89 2 4V20C2 21.11 2.89 22 4 22H16C17.11 22 18 21.11 18 20V8L12 2M4 4H11V9H16V20H4Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<img
|
||||
:class="[
|
||||
'max-w-48',
|
||||
'max-h-48',
|
||||
'p-2',
|
||||
'w-full',
|
||||
'h-full',
|
||||
{
|
||||
'border-red-6': feedbackInvalid,
|
||||
'border-2': feedbackInvalid,
|
||||
},
|
||||
]"
|
||||
@load="handleImageLoaded"
|
||||
@error="handleImageError"
|
||||
:style="{ display: image == '' ? 'none' : 'block' }"
|
||||
:src="image" />
|
||||
</div>
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="h-48 text-gray">
|
||||
<path
|
||||
d="M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm56-97h489L578-473 446-302l-93-127-117 152Zm-56 97v-600 600Zm160.118-390Q361-570 375.5-584.618q14.5-14.617 14.5-35.5Q390-641 375.382-655.5q-14.617-14.5-35.5-14.5Q319-670 304.5-655.382q-14.5 14.617-14.5 35.5Q290-599 304.618-584.5q14.617 14.5 35.5 14.5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<div class="text-center">
|
||||
<p
|
||||
v-if="feedbackInvalid"
|
||||
class="px-2 -mt-2 pb-2 text-xs text-red-6">
|
||||
{{ feedbackInvalid }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
:disabled="disabled"
|
||||
@click="handleMediaSelect">
|
||||
Select File
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="slots.help"><slot name="help"></slot></template>
|
||||
<input
|
||||
id="file"
|
||||
ref="refUploadInput"
|
||||
type="file"
|
||||
style="display: none"
|
||||
:accept="props.accepts"
|
||||
@change="handleChangeSelectFile" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watch, ref, useSlots, onMounted } from "vue";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { mediaGetThumbnail } from "../helpers/media";
|
||||
import { openDialog } from "./SMDialog";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { Media } from "../helpers/api.types";
|
||||
import SMLoading from "./SMLoading.vue";
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
control: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
button: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
accepts: {
|
||||
type: String,
|
||||
default: "image/*",
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false,
|
||||
},
|
||||
autocomplete: {
|
||||
type: [Array<string>, Function],
|
||||
default: () => {
|
||||
[];
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
allowUpload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
uploadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
const refUploadInput = ref(null);
|
||||
const image = ref("");
|
||||
|
||||
const form = inject(props.formId, props.form);
|
||||
const control =
|
||||
typeof props.control === "object"
|
||||
? props.control
|
||||
: form &&
|
||||
!isEmpty(form) &&
|
||||
typeof props.control === "string" &&
|
||||
props.control !== "" &&
|
||||
Object.prototype.hasOwnProperty.call(form.controls, props.control)
|
||||
? form.controls[props.control]
|
||||
: null;
|
||||
|
||||
const label = ref(
|
||||
props.label != undefined
|
||||
? props.label
|
||||
: typeof props.control == "string"
|
||||
? toTitleCase(props.control)
|
||||
: "",
|
||||
);
|
||||
const value = ref(
|
||||
props.modelValue != undefined
|
||||
? props.modelValue
|
||||
: control != null
|
||||
? control.value
|
||||
: "",
|
||||
);
|
||||
const id = ref(
|
||||
props.id != undefined
|
||||
? props.id
|
||||
: typeof props.control == "string" && props.control.length > 0
|
||||
? props.control
|
||||
: generateRandomElementId(),
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const disabled = ref(props.disabled);
|
||||
const imgLoaded = ref(false);
|
||||
const imgError = ref(false);
|
||||
|
||||
if (props.modelValue != undefined) {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
imgLoaded.value = false;
|
||||
imgError.value = false;
|
||||
value.value = newValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
(newValue) => {
|
||||
mediaGetThumbnail(newValue, "medium", (e) => {
|
||||
image.value = e;
|
||||
imgLoaded.value = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (typeof control === "object" && control !== null) {
|
||||
watch(
|
||||
() => control.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: control.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => control.value,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
watch(
|
||||
() => form.loading(),
|
||||
(newValue) => {
|
||||
disabled.value = newValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const handleMediaSelect = async () => {
|
||||
let result = null;
|
||||
|
||||
if (props.uploadOnly == false) {
|
||||
result = await openDialog(SMDialogMedia, {
|
||||
allowUpload: props.allowUpload,
|
||||
accepts: props.accepts,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const mediaResult = result as Media;
|
||||
emits("update:modelValue", mediaResult);
|
||||
if (control) {
|
||||
control.value = mediaResult;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (refUploadInput.value != null) {
|
||||
refUploadInput.value.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeSelectFile = async () => {
|
||||
if (refUploadInput.value != null && refUploadInput.value.files != null) {
|
||||
imgLoaded.value = false;
|
||||
imgError.value = false;
|
||||
|
||||
const fileList = Array.from(refUploadInput.value.files);
|
||||
|
||||
let file = fileList.length > 0 ? fileList[0] : null;
|
||||
|
||||
emits("update:modelValue", file);
|
||||
if (control) {
|
||||
control.value = file;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.setTimeout(() => {
|
||||
mediaGetThumbnail(value.value, "medium", (e) => {
|
||||
image.value = e;
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
const handleImageLoaded = () => {
|
||||
imgLoaded.value = true;
|
||||
imgError.value = false;
|
||||
};
|
||||
|
||||
const handleImageError = () => {
|
||||
if (image.value !== "") {
|
||||
imgLoaded.value = true;
|
||||
imgError.value = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.input-control-prepend {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
& + .control-item .input-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-control-append {
|
||||
p {
|
||||
display: block;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-dark);
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
height: 50px;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-item {
|
||||
max-width: 100%;
|
||||
align-items: start;
|
||||
|
||||
.control-label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
transform-origin: top left;
|
||||
transform: translate(16px, 16px) scale(1);
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: var(--base-color-darker);
|
||||
pointer-events: none;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.invalid-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 10px;
|
||||
top: 14px;
|
||||
color: var(--danger-color);
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 18px;
|
||||
background-color: var(--input-clear-icon-color);
|
||||
border-radius: 50%;
|
||||
font-size: 80%;
|
||||
padding: 1px 1px 1px 0px;
|
||||
|
||||
&:hover {
|
||||
color: var(--input-clear-icon-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.input-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 20px 16px 10px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--base-color-text);
|
||||
|
||||
&:disabled {
|
||||
background-color: hsl(0, 0%, 92%);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
background-color: var(--base-color-light);
|
||||
color: var(--primary-color);
|
||||
z-index: 1;
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
margin: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.static-input-control {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control {
|
||||
opacity: 0;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
}
|
||||
|
||||
.file-input-control-value {
|
||||
width: 100%;
|
||||
padding: 22px 16px 8px 16px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px 0 0 8px;
|
||||
background-color: var(--base-color);
|
||||
height: 52px;
|
||||
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input-control-button {
|
||||
border-width: 1px 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--base-color-darker);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 16px 30px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.control-label-select {
|
||||
transform: translate(16px, 6px) scale(0.7);
|
||||
}
|
||||
|
||||
.select-dropdown-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.select-input-control {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
padding: 20px 16px 8px 14px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 8px;
|
||||
background-color: var(--base-color-light);
|
||||
height: 52px;
|
||||
color: var(--base-color-text);
|
||||
}
|
||||
|
||||
.control-label-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0 16px 32px;
|
||||
pointer-events: all;
|
||||
transform: none;
|
||||
color: var(--base-color-text);
|
||||
|
||||
&.disabled {
|
||||
color: var(--base-color-darker);
|
||||
cursor: not-allowed;
|
||||
|
||||
.checkbox-control-box {
|
||||
background-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + .checkbox-control-box {
|
||||
.checkbox-control-tick {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-control-box {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--base-color-darker);
|
||||
border-radius: 2px;
|
||||
background-color: var(--base-color-light);
|
||||
|
||||
.checkbox-control-tick {
|
||||
position: absolute;
|
||||
display: none;
|
||||
border-right: 3px solid var(--base-color-text);
|
||||
border-bottom: 3px solid var(--base-color-text);
|
||||
top: 1px;
|
||||
left: 7px;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-input-control {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
img,
|
||||
ion-icon {
|
||||
display: block;
|
||||
margin: 48px auto 8px auto;
|
||||
border-radius: 8px;
|
||||
font-size: 800%;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-label-range {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.range-control {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.range-control-value {
|
||||
margin-top: 22px;
|
||||
padding-left: 16px;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<ul class="social-icons">
|
||||
<li>
|
||||
<a href="https://facebook.com/stemmechanics"
|
||||
><ion-icon name="logo-facebook"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://mastodon.au/@stemmechanics"
|
||||
><ion-icon name="logo-mastodon"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/@stemmechanics"
|
||||
><ion-icon name="logo-youtube"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/stemmechanics"
|
||||
><ion-icon name="logo-twitter"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/stemmechanics"
|
||||
><ion-icon name="logo-github"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/yNzk4x7mpD"
|
||||
><ion-icon name="logo-discord"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.linkedin.com/company/stemmechanics"
|
||||
><ion-icon name="logo-linkedin"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.social-icons {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
font-size: 200%;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.social-icons {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="id == selectedTab"
|
||||
class="border-1 border-gray rounded-b-2 rounded-tr-2 p-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedTab = inject("selectedTab");
|
||||
</script>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<ul class="flex relative">
|
||||
<li
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'text-center',
|
||||
'px-4',
|
||||
'py-2',
|
||||
'-mb-1px',
|
||||
'border-1',
|
||||
'rounded-t-2',
|
||||
'border-gray',
|
||||
selectedTab == tab.id
|
||||
? ['border-b-white']
|
||||
: [
|
||||
'border-x-white',
|
||||
'border-t-white',
|
||||
'hover:border-x-gray-3',
|
||||
'hover:border-t-gray-3',
|
||||
],
|
||||
]"
|
||||
@click="selectedTab = tab.id">
|
||||
{{ tab.label }}
|
||||
</li>
|
||||
</ul>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, ref, useSlots, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["tabChanged", "update:modelValue"]);
|
||||
const slots = useSlots();
|
||||
|
||||
const tabs = ref(
|
||||
slots
|
||||
.default()
|
||||
.map((tab) => {
|
||||
const { label, id, hide } = tab.props;
|
||||
if (hide !== true) {
|
||||
return {
|
||||
label,
|
||||
id,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
const selectedTab = ref(
|
||||
props.modelValue.length == 0 ? tabs.value[0].id : props.modelValue,
|
||||
);
|
||||
|
||||
if (props.modelValue.length == 0) {
|
||||
emits("update:modelValue", selectedTab.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedTab.value,
|
||||
(newValue) => {
|
||||
emits("tabChanged", newValue);
|
||||
emits("update:modelValue", newValue);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
selectedTab.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
provide("selectedTab", selectedTab);
|
||||
</script>
|
||||
@@ -1,193 +0,0 @@
|
||||
<template>
|
||||
<table class="sm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="header in headers" :key="header['value']">
|
||||
{{ header["text"] }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in items"
|
||||
:key="`item-row-${index}`"
|
||||
@click="handleRowClick(item)">
|
||||
<td
|
||||
v-for="header in headers"
|
||||
:data-title="header['text']"
|
||||
:key="`item-row-${index}-${header['value']}`">
|
||||
<template v-if="slots[`item-${header['value']}`]">
|
||||
<slot :name="`item-${header['value']}`" v-bind="item">
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ getItemValue(item, header["value"]) }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from "vue";
|
||||
|
||||
defineProps({
|
||||
headers: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["rowClick"]);
|
||||
const slots = useSlots();
|
||||
|
||||
const handleRowClick = (item) => {
|
||||
emits("rowClick", item);
|
||||
};
|
||||
|
||||
const getItemValue = (data: unknown, key: string): string => {
|
||||
if (typeof data === "object" && data !== null) {
|
||||
return key.split(".").reduce((item, key) => item[key], data);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const hasClassLong = (text: unknown): boolean => {
|
||||
if (typeof text == "string") {
|
||||
return text.length >= 35;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-table {
|
||||
border-spacing: 0;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-radius: 0.75rem;
|
||||
border-color: rgba(209, 213, 219);
|
||||
width: 100%;
|
||||
|
||||
thead th {
|
||||
background-color: rgba(229, 231, 235, 0.75);
|
||||
border-top-width: 1px;
|
||||
text-align: left;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 0.75rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: rgba(55, 65, 81);
|
||||
border-bottom-width: 1px;
|
||||
border-color: rgba(209, 213, 219);
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr:nth-child(even) td {
|
||||
background-color: rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
tr:last-child td:first-child {
|
||||
border-bottom-left-radius: 0.75rem;
|
||||
}
|
||||
|
||||
tr:last-child td:last-child {
|
||||
border-bottom-right-radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.sm-table {
|
||||
display: block;
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
th,
|
||||
td,
|
||||
tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid rgba(209, 213, 219);
|
||||
|
||||
&:first-child {
|
||||
td:first-child {
|
||||
border-top: 1px solid rgba(209, 213, 219);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
|
||||
td {
|
||||
&:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid rgba(209, 213, 219);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 0;
|
||||
position: relative;
|
||||
padding: 8px 12px 8px 140px;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 125px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
content: attr(data-title);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(even) td {
|
||||
background-color: rgba(250, 250, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="toast"
|
||||
class="border-1 border-gray-2 bg-white rounded-md p-4 mt-4 mb-4 pointer-events-auto"
|
||||
:style="styles">
|
||||
<div :class="['max-w-48', 'border-l-5', 'pl-4', 'relative', colour]">
|
||||
<svg
|
||||
v-if="!props.loader"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="h-4 absolute right-0 hover:text-red-7 cursor-pointer"
|
||||
@click="handleClickClose">
|
||||
<path
|
||||
d="m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<h5 class="mt-0 mb-2 pr-6" v-if="title && title.length > 0">
|
||||
{{ title }}
|
||||
</h5>
|
||||
<div class="flex">
|
||||
<svg
|
||||
v-if="props.loader"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="spin h-4 color-gray mr-2 flex-align-middle">
|
||||
<path
|
||||
d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<p class="text-xs">
|
||||
{{ content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useToastStore } from "../store/ToastStore";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
required: false,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
loader: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const toastStore = useToastStore();
|
||||
const toast = ref(null);
|
||||
let height = 40;
|
||||
let hideTimeoutID: number | null = null;
|
||||
|
||||
const styles = ref({
|
||||
transition: "opacity 0.2s ease-in, margin 0.2s ease-in",
|
||||
opacity: 0,
|
||||
marginTop: "40px",
|
||||
});
|
||||
|
||||
let colour = computed(() => {
|
||||
switch (props.type) {
|
||||
case "danger":
|
||||
return "border-red-7";
|
||||
case "success":
|
||||
return "border-green-7";
|
||||
case "warning":
|
||||
return "border-yellow-4";
|
||||
}
|
||||
|
||||
return "border-sky-5";
|
||||
});
|
||||
|
||||
const handleClickClose = () => {
|
||||
if (hideTimeoutID != null) {
|
||||
window.clearTimeout(hideTimeoutID);
|
||||
hideTimeoutID = null;
|
||||
}
|
||||
removeToast();
|
||||
};
|
||||
|
||||
const removeToast = () => {
|
||||
styles.value.opacity = 0;
|
||||
styles.value.marginTop = `-${height}px`;
|
||||
window.setTimeout(() => {
|
||||
toastStore.clearToast(props.id);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const cancelRemoveCountdown = () => {
|
||||
if (hideTimeoutID != null) {
|
||||
window.clearTimeout(hideTimeoutID);
|
||||
hideTimeoutID = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startRemoveCountdown = () => {
|
||||
if (hideTimeoutID == null) {
|
||||
hideTimeoutID = window.setTimeout(() => {
|
||||
hideTimeoutID = null;
|
||||
removeToast();
|
||||
}, 8000);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.setTimeout(() => {
|
||||
styles.value.opacity = 1;
|
||||
styles.value.marginTop = "0";
|
||||
|
||||
if (toast.value != null) {
|
||||
const styles = window.getComputedStyle(toast.value);
|
||||
const marginBottom = parseFloat(styles.marginBottom);
|
||||
height = toast.value.offsetHeight + marginBottom || 0;
|
||||
}
|
||||
|
||||
if (!props.loader) {
|
||||
startRemoveCountdown();
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.loader,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
cancelRemoveCountdown();
|
||||
} else {
|
||||
startRemoveCountdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-10 right-10 z-10 overflow-hidden pointer-events-none"
|
||||
v-if="toastStore.toasts">
|
||||
<SMToast
|
||||
v-for="toast of toastStore.toasts"
|
||||
:id="toast.id"
|
||||
:key="toast.id"
|
||||
:type="toast.type"
|
||||
:title="toast.title"
|
||||
:content="toast.content"
|
||||
:loader="toast.loader" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useToastStore } from "../store/ToastStore";
|
||||
import SMToast from "./SMToast.vue";
|
||||
|
||||
const toastStore = useToastStore();
|
||||
</script>
|
||||
@@ -1,121 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full bg-black bg-op-20 backdrop-blur"></div>
|
||||
<div class="fixed top-0 left-0 w-full flex-justify-center flex pt-36">
|
||||
<div
|
||||
class="max-w-2xl mx-auto border-1 bg-white rounded-xl mt-7xl text-gray-5 px-12 py-8">
|
||||
<SMForm :model-value="form" @submit="handleSubmit">
|
||||
<h3 class="mb-2">Change Password</h3>
|
||||
<p class="mb-4">Enter your new password below</p>
|
||||
<SMInput
|
||||
control="password"
|
||||
type="password"
|
||||
label="New Password"
|
||||
autofocus />
|
||||
<div class="flex flex-justify-between pt-4">
|
||||
<button
|
||||
class="font-medium block w-full md:inline-block md:w-auto px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
type="button"
|
||||
@click="handleClickCancel">
|
||||
Cancel
|
||||
</button>
|
||||
<input
|
||||
class="font-medium block w-full md:inline-block md:w-auto px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
role="button"
|
||||
type="submit"
|
||||
value="Update" />
|
||||
</div>
|
||||
</SMForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { closeDialog } from "../SMDialog";
|
||||
import { api } from "../../helpers/api";
|
||||
import { Form, FormControl, FormObject } from "../../helpers/form";
|
||||
import { And, Password, Required } from "../../helpers/validate";
|
||||
import { useApplicationStore } from "../../store/ApplicationStore";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import SMForm from "../SMForm.vue";
|
||||
import SMInput from "../SMInput.vue";
|
||||
|
||||
const form: FormObject = reactive(
|
||||
Form({
|
||||
password: FormControl("", And([Required(), Password()])),
|
||||
}),
|
||||
);
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
const userStore = useUserStore();
|
||||
const dialogLoading = ref(false);
|
||||
|
||||
/**
|
||||
* User clicks cancel button to close dialog
|
||||
*/
|
||||
const handleClickCancel = () => {
|
||||
closeDialog(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* User clicks form submit button
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
dialogLoading.value = true;
|
||||
|
||||
await api.put({
|
||||
url: "/users/{id}",
|
||||
params: {
|
||||
id: userStore.id,
|
||||
},
|
||||
body: {
|
||||
password: form.controls.password.value,
|
||||
},
|
||||
});
|
||||
|
||||
const toastStore = useToastStore();
|
||||
|
||||
toastStore.addToast({
|
||||
title: "Password Reset",
|
||||
content: "Your password has been reset",
|
||||
type: "success",
|
||||
});
|
||||
closeDialog(false);
|
||||
} catch (error) {
|
||||
form.apiErrors(error, (message) => {
|
||||
useToastStore().addToast({
|
||||
title: "An error occurred",
|
||||
content: message,
|
||||
type: "danger",
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
dialogLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a keyboard event in this component.
|
||||
* @param {KeyboardEvent} event The keyboard event.
|
||||
* @returns {boolean} If the event was handled.
|
||||
*/
|
||||
const eventKeyUp = (event: KeyboardEvent): boolean => {
|
||||
if (event.key === "Escape") {
|
||||
handleClickCancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
applicationStore.addKeyUpListener(eventKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
applicationStore.removeKeyUpListener(eventKeyUp);
|
||||
});
|
||||
</script>
|
||||
@@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full bg-black bg-op-20 backdrop-blur"></div>
|
||||
<div class="fixed top-0 left-0 w-full flex-justify-center flex pt-36">
|
||||
<div
|
||||
class="max-w-2xl mx-auto border-1 bg-white rounded-xl mt-7xl text-gray-5 px-12 py-8">
|
||||
<h1 class="mb-4">{{ props.title }}</h1>
|
||||
<p class="mb-4" v-html="props.text"></p>
|
||||
<div class="flex flex-justify-between pt-4">
|
||||
<button
|
||||
type="button"
|
||||
:class="buttonClass(props.cancel.type)"
|
||||
@click="handleClickCancel()">
|
||||
{{ props.cancel.label }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="buttonClass(props.confirm.type)"
|
||||
@click="handleClickConfirm()">
|
||||
{{ props.confirm.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
import { closeDialog } from "../SMDialog";
|
||||
import { useApplicationStore } from "../../store/ApplicationStore";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cancel: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
type: "secondary",
|
||||
label: "No",
|
||||
};
|
||||
},
|
||||
},
|
||||
confirm: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
type: "primary",
|
||||
label: "Yes",
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* Handle the user clicking the cancel button.
|
||||
*/
|
||||
const handleClickCancel = () => {
|
||||
closeDialog(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the user clicking the confirm button.
|
||||
*/
|
||||
const handleClickConfirm = () => {
|
||||
closeDialog(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a keyboard event in this component.
|
||||
* @param {KeyboardEvent} event The keyboard event.
|
||||
* @returns {boolean} If the event was handled.
|
||||
*/
|
||||
const eventKeyUp = (event: KeyboardEvent): boolean => {
|
||||
if (event.key === "Escape") {
|
||||
handleClickCancel();
|
||||
return true;
|
||||
} else if (event.key === "Enter") {
|
||||
handleClickConfirm();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const buttonClass = (type: string): Array<string> => {
|
||||
let baseClasses = [
|
||||
"font-medium",
|
||||
"px-6",
|
||||
"py-1.5",
|
||||
"rounded-md",
|
||||
"hover:shadow-md",
|
||||
"transition",
|
||||
"text-sm",
|
||||
"text-white",
|
||||
"cursor-pointer",
|
||||
];
|
||||
|
||||
if (type === "secondary") {
|
||||
baseClasses = baseClasses.concat(["bg-gray-400", "hover:bg-gray-300"]);
|
||||
} else if (type === "danger") {
|
||||
baseClasses = baseClasses.concat(["bg-red-600", "hover:bg-red-500"]);
|
||||
} else if (type === "success") {
|
||||
baseClasses = baseClasses.concat([
|
||||
"bg-green-600",
|
||||
"hover:bg-green-500",
|
||||
]);
|
||||
} else {
|
||||
baseClasses = baseClasses.concat(["bg-sky-600", "hover:bg-sky-500"]);
|
||||
}
|
||||
|
||||
return baseClasses;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
applicationStore.addKeyUpListener(eventKeyUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
applicationStore.removeKeyUpListener(eventKeyUp);
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-full bg-black bg-op-20 backdrop-blur"></div>
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 bottom-0 flex-justify-center flex-items-center flex">
|
||||
<div
|
||||
class="flex flex-col m-4 border-1 bg-white rounded-xl text-gray-5 px-4 md:px-12 py-4 md:py-8 max-w-200 w-full overflow-hidden">
|
||||
<h2 class="mb-2">{{ props.title }}</h2>
|
||||
<div
|
||||
v-for="(row, index) in props.rows"
|
||||
class="flex flex-col text-xs my-4"
|
||||
:key="index">
|
||||
<div class="w-full bg-gray-3 h-3 mb-2 rounded-2">
|
||||
<div
|
||||
class="bg-sky-600 h-3 rounded-2"
|
||||
:style="{
|
||||
width: `${props.progress[index]}%`,
|
||||
}"></div>
|
||||
</div>
|
||||
<p class="m-0"></p>
|
||||
{{ row }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
rows: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
progress: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
73
resources/js/editor/Box.js
Normal file
73
resources/js/editor/Box.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import {mergeAttributes, Node} from '@tiptap/core'
|
||||
|
||||
|
||||
export const Box = Node.create({
|
||||
name: 'box',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['info', 'warning', 'danger', 'success', 'bug'],
|
||||
HTMLAttributes: {
|
||||
class: 'box',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: {
|
||||
default: 'info',
|
||||
rendered: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
group: 'block',
|
||||
|
||||
content: 'inline*',
|
||||
|
||||
defining: true,
|
||||
|
||||
parseHTML() {
|
||||
return this.options.types.map((type) => ({
|
||||
tag: 'div',
|
||||
getAttrs: (node) => {
|
||||
// Extract the class attribute and find the type based on the class
|
||||
const classList = node.getAttribute('class')?.split(' ') || [];
|
||||
const boxType = classList.find(cls => this.options.types.includes(cls));
|
||||
return {
|
||||
type: boxType || this.options.types[0], // Default to 'info' if no matching type is found
|
||||
};
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
const hasType = this.options.types.includes(node.attrs.type);
|
||||
const type = hasType
|
||||
? node.attrs.type
|
||||
: this.options.types[0]
|
||||
|
||||
let classes = 'box ' + type;
|
||||
return ['div', mergeAttributes({ class: classes }, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setBox: attributes => ({ commands }) => {
|
||||
if (!this.options.types.includes(attributes.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return commands.setNode(this.name, attributes)
|
||||
},
|
||||
toggleBox: attributes => ({ commands }) => {
|
||||
if (!this.options.types.includes(attributes.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return commands.toggleNode(this.name, 'paragraph', attributes)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
28
resources/js/editor/ColourHighter.js
Normal file
28
resources/js/editor/ColourHighter.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
import findColors from './FindColors.js'
|
||||
|
||||
export const ColorHighlighter = Extension.create({
|
||||
name: 'colorHighlighter',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return findColors(doc)
|
||||
},
|
||||
apply(transaction, oldState) {
|
||||
return transaction.docChanged ? findColors(transaction.doc) : oldState
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state)
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
36
resources/js/editor/ExtraSmall.js
Normal file
36
resources/js/editor/ExtraSmall.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import {mergeAttributes, Node} from '@tiptap/core'
|
||||
|
||||
|
||||
export const ExtraSmall = Node.create({
|
||||
name: 'extraSmall',
|
||||
|
||||
priority: 1000,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'text-xs',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
group: 'block',
|
||||
|
||||
content: 'inline*',
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'p.text-xs' }]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setExtraSmall: () => ({ commands }) => {
|
||||
return commands.setNode(this.name)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
27
resources/js/editor/FindColors.js
Normal file
27
resources/js/editor/FindColors.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view"
|
||||
|
||||
export default function(doc) {
|
||||
const hexColor = /(#[0-9a-f]{3,6})\b/gi
|
||||
const decorations = []
|
||||
|
||||
doc.descendants((node, position) => {
|
||||
if (!node.text) {
|
||||
return
|
||||
}
|
||||
|
||||
Array.from(node.text.matchAll(hexColor)).forEach(match => {
|
||||
const color = match[0]
|
||||
const index = match.index || 0
|
||||
const from = position + index
|
||||
const to = from + color.length
|
||||
const decoration = Decoration.inline(from, to, {
|
||||
class: "color",
|
||||
style: `--color: ${color}`
|
||||
})
|
||||
|
||||
decorations.push(decoration)
|
||||
})
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
38
resources/js/editor/Small.js
Normal file
38
resources/js/editor/Small.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import {mergeAttributes, Node} from '@tiptap/core'
|
||||
|
||||
|
||||
export const Small = Node.create({
|
||||
name: 'small',
|
||||
|
||||
priority: 2000,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'text-sm',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
group: 'block',
|
||||
|
||||
content: 'inline*',
|
||||
|
||||
parseHTML() {
|
||||
return [{
|
||||
tag: 'p.text-sm',
|
||||
}]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSmall: () => ({ commands }) => {
|
||||
return commands.setNode(this.name)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
133
resources/js/editor/SmileyReplacer.js
Normal file
133
resources/js/editor/SmileyReplacer.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Extension, textInputRule } from '@tiptap/core'
|
||||
|
||||
export const SmileyReplacer = Extension.create({
|
||||
name: 'smileyReplacer',
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
textInputRule({ find: /-___- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:'-\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /':-\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':-D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:-\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /-__- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /':-\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /:'-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: />:-\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /O:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;\^\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /:'\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /:-D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /':\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />;\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />=\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /;-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;-\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\^\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /B-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:-\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\^\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:-\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /-_- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:-X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: />.< $/, replace: '😣 ' }),
|
||||
textInputRule({ find: /:-O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /O_O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: />:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: />:P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /X-P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /x-p $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /':\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /'=\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: />:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-. $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:'\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /;-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /#-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: />:\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /0:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O=\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /<\/3 $/, replace: '💔 ' }),
|
||||
textInputRule({ find: /:D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /=D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /;\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;D $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /=\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /d: $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /=\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /;\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /D: $/, replace: '😨 ' }),
|
||||
textInputRule({ find: /:\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /=\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /#\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /:@ $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /<3 $/, replace: '❤️ ' }),
|
||||
textInputRule({ find: /\/shrug $/, replace: '¯\\_(ツ)_/¯' }),
|
||||
]
|
||||
},
|
||||
})
|
||||
159
resources/js/editor/TipTap.js
Normal file
159
resources/js/editor/TipTap.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import Link from "@tiptap/extension-link";
|
||||
import {Editor} from "@tiptap/core";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import Typography from "@tiptap/extension-typography";
|
||||
import {ColorHighlighter} from "./ColourHighter.js";
|
||||
import {SmileyReplacer} from "./SmileyReplacer.js";
|
||||
import {Small} from "./Small.js";
|
||||
import {ExtraSmall} from "./ExtraSmall.js";
|
||||
import {Box} from "./Box.js";
|
||||
|
||||
const editorToggleLink = (editor) => {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
return
|
||||
}
|
||||
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
const CustomLink = Link.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-k': () => editorToggleLink(this.editor)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('editor', (content) => {
|
||||
let editor // Alpine's reactive engine automatically wraps component properties in proxy objects. Attempting to use a proxied editor instance to apply a transaction will cause a "Range Error: Applying a mismatched transaction", so be sure to unwrap it using Alpine.raw(), or simply avoid storing your editor as a component property, as shown in this example.
|
||||
|
||||
return {
|
||||
updatedAt: Date.now(), // force Alpine to rerender on selection change
|
||||
content: SM.decodeHtml(content),
|
||||
init() {
|
||||
const _this = this
|
||||
|
||||
editor = new Editor({
|
||||
element: this.$refs.element,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Highlight,
|
||||
CustomLink.configure({
|
||||
rel: 'noopener noreferrer',
|
||||
openOnClick: 'whenNotEditable',
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph', 'small', 'extraSmall'],
|
||||
}),
|
||||
Typography,
|
||||
ColorHighlighter,
|
||||
SmileyReplacer,
|
||||
Small,
|
||||
ExtraSmall,
|
||||
Box
|
||||
],
|
||||
content: content,
|
||||
onCreate({editor}) {
|
||||
_this.updatedAt = Date.now()
|
||||
},
|
||||
onUpdate({editor}) {
|
||||
_this.updatedAt = Date.now()
|
||||
_this.content = editor.getHTML()
|
||||
},
|
||||
onSelectionUpdate({editor}) {
|
||||
_this.updatedAt = Date.now()
|
||||
}
|
||||
})
|
||||
},
|
||||
isLoaded() {
|
||||
return editor
|
||||
},
|
||||
isActive(type, opts = {}) {
|
||||
return editor.isActive(type, opts)
|
||||
},
|
||||
toggleHeading(opts) {
|
||||
editor.chain().toggleHeading(opts).focus().run()
|
||||
},
|
||||
toggleBold() {
|
||||
editor.chain().toggleBold().focus().run()
|
||||
},
|
||||
toggleItalic() {
|
||||
editor.chain().toggleItalic().focus().run()
|
||||
},
|
||||
toggleUnderline() {
|
||||
editor.chain().toggleUnderline().focus().run()
|
||||
},
|
||||
toggleStrike() {
|
||||
editor.chain().toggleStrike().focus().run()
|
||||
},
|
||||
setParagraph() {
|
||||
editor.chain().setParagraph().focus().run()
|
||||
},
|
||||
toggleCode() {
|
||||
editor.chain().toggleCode().focus().run()
|
||||
},
|
||||
toggleBulletList() {
|
||||
editor.chain().toggleBulletList().focus().run()
|
||||
},
|
||||
toggleOrderedList() {
|
||||
editor.chain().toggleOrderedList().focus().run()
|
||||
},
|
||||
toggleBlockquote() {
|
||||
editor.chain().toggleBlockquote().focus().run()
|
||||
},
|
||||
toggleCodeBlock() {
|
||||
editor.chain().toggleCodeBlock().focus().run()
|
||||
},
|
||||
toggleLink() {
|
||||
editorToggleLink(editor)
|
||||
},
|
||||
clearLink() {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
},
|
||||
toggleHighlight() {
|
||||
editor.chain().toggleHighlight().focus().run()
|
||||
},
|
||||
toggleSubscript() {
|
||||
editor.chain().toggleSubscript().focus().run()
|
||||
},
|
||||
toggleSuperscript() {
|
||||
editor.chain().toggleSuperscript().focus().run()
|
||||
},
|
||||
undo() {
|
||||
editor.chain().undo().focus().run()
|
||||
},
|
||||
redo() {
|
||||
editor.chain().redo().focus().run()
|
||||
},
|
||||
unsetAllMarks() {
|
||||
editor.chain().focus().unsetAllMarks().run()
|
||||
},
|
||||
clearNotes() {
|
||||
editor.chain().focus().clearNodes().run()
|
||||
},
|
||||
setTextAlign(value) {
|
||||
editor.chain().setTextAlign(value).focus().run()
|
||||
},
|
||||
setSmall() {
|
||||
editor.chain().focus().setSmall().run()
|
||||
},
|
||||
setExtraSmall() {
|
||||
editor.chain().focus().setExtraSmall().run()
|
||||
},
|
||||
toggleBox(opts) {
|
||||
editor.chain().toggleBox(opts).focus().run()
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
|
||||
export interface DangerOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
danger: {
|
||||
/**
|
||||
* Toggle a paragraph
|
||||
*/
|
||||
setDanger: () => ReturnType;
|
||||
toggleDanger: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Danger = Node.create<DangerOptions>({
|
||||
name: "danger",
|
||||
|
||||
priority: 1000,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: { class: "danger" },
|
||||
};
|
||||
},
|
||||
|
||||
group: "block",
|
||||
|
||||
content: "inline*",
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "p", class: "danger" }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"p",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setDanger:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name);
|
||||
},
|
||||
toggleDanger:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.toggleNode(this.name, "paragraph");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
|
||||
export interface InfoOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
info: {
|
||||
/**
|
||||
* Toggle a paragraph
|
||||
*/
|
||||
setInfo: () => ReturnType;
|
||||
toggleInfo: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Info = Node.create<InfoOptions>({
|
||||
name: "info",
|
||||
|
||||
priority: 1000,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: { class: "info" },
|
||||
};
|
||||
},
|
||||
|
||||
group: "block",
|
||||
|
||||
content: "inline*",
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "p", class: "info" }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"p",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setInfo:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name);
|
||||
},
|
||||
toggleInfo:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.toggleNode(this.name, "paragraph");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
|
||||
export interface SmallOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
small: {
|
||||
/**
|
||||
* Set a small mark
|
||||
*/
|
||||
setSmall: () => ReturnType;
|
||||
/**
|
||||
* Toggle a small mark
|
||||
*/
|
||||
toggleSmall: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Small = Node.create<SmallOptions>({
|
||||
name: "small",
|
||||
group: "block",
|
||||
content: "inline*",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: { class: "small" },
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "p.small", priority: 100 }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"p",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSmall:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name);
|
||||
},
|
||||
toggleSmall:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.toggleNode(this.name, "paragraph");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
|
||||
export interface SuccessOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
success: {
|
||||
/**
|
||||
* Toggle a paragraph
|
||||
*/
|
||||
setSuccess: () => ReturnType;
|
||||
toggleSuccess: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Success = Node.create<SuccessOptions>({
|
||||
name: "success",
|
||||
|
||||
priority: 1000,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: { class: "success" },
|
||||
};
|
||||
},
|
||||
|
||||
group: "block",
|
||||
|
||||
content: "inline*",
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "p", class: "success" }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"p",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSuccess:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name);
|
||||
},
|
||||
toggleSuccess:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.toggleNode(this.name, "paragraph");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
|
||||
export interface WarningOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
warning: {
|
||||
/**
|
||||
* Toggle a paragraph
|
||||
*/
|
||||
setWarning: () => ReturnType;
|
||||
toggleWarning: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Warning = Node.create<WarningOptions>({
|
||||
name: "warning",
|
||||
|
||||
priority: 1000,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: { class: "warning" },
|
||||
};
|
||||
},
|
||||
|
||||
group: "block",
|
||||
|
||||
content: "inline*",
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "p", class: "warning" }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"p",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setWarning:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name);
|
||||
},
|
||||
toggleWarning:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.toggleNode(this.name, "paragraph");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,476 +0,0 @@
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { useCacheStore } from "../store/CacheStore";
|
||||
import { ImportMetaExtras } from "../../../import-meta";
|
||||
|
||||
interface ApiProgressData {
|
||||
loaded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ApiCallbackData {
|
||||
status: number;
|
||||
statusText: string;
|
||||
url: string;
|
||||
headers: unknown;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
type ApiProgressCallback = (progress: ApiProgressData) => void;
|
||||
type ApiResultCallback = (data: ApiCallbackData) => void;
|
||||
|
||||
export interface ApiOptions {
|
||||
url: string;
|
||||
params?: object;
|
||||
method?: string;
|
||||
headers?: HeadersInit;
|
||||
body?: string | object | FormData | ArrayBuffer | Blob;
|
||||
signal?: AbortSignal | null;
|
||||
progress?: ApiProgressCallback;
|
||||
callback?: ApiResultCallback;
|
||||
chunk?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
data: unknown;
|
||||
json?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const apiDefaultHeaders = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
};
|
||||
|
||||
export const api = {
|
||||
timeout: 8000,
|
||||
baseUrl: (import.meta as ImportMetaExtras).env.APP_URL_API,
|
||||
|
||||
send: function (options: ApiOptions) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let url = this.baseUrl + options.url;
|
||||
|
||||
if (options.params) {
|
||||
let params = "";
|
||||
|
||||
for (const [key, value] of Object.entries(options.params)) {
|
||||
const placeholder = `{${key}}`;
|
||||
if (url.includes(placeholder)) {
|
||||
url = url.replace(
|
||||
placeholder,
|
||||
encodeURIComponent(value),
|
||||
);
|
||||
} else {
|
||||
params += `&${encodeURIComponent(
|
||||
key,
|
||||
)}=${encodeURIComponent(value)}`;
|
||||
}
|
||||
}
|
||||
|
||||
url = url.replace(/{(.*?)}/g, "$1");
|
||||
if (params.length > 0) {
|
||||
url += (url.includes("?") ? "" : "?") + params.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
options.headers = {
|
||||
...apiDefaultHeaders,
|
||||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
const userStore = useUserStore();
|
||||
if (userStore.id) {
|
||||
options.headers["Authorization"] = `Bearer ${userStore.token}`;
|
||||
}
|
||||
|
||||
options.method = options.method.toUpperCase() || "GET";
|
||||
|
||||
if (options.body && typeof options.body === "object") {
|
||||
if (options.body instanceof FormData) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
options.headers,
|
||||
"Content-Type",
|
||||
)
|
||||
) {
|
||||
// remove the "Content-Type" key from the headers object
|
||||
delete options.headers["Content-Type"];
|
||||
}
|
||||
|
||||
if (options.method != "POST") {
|
||||
options.body.append("_method", options.method);
|
||||
options.method = "POST";
|
||||
}
|
||||
} else if (
|
||||
options.body instanceof Blob ||
|
||||
options.body instanceof ArrayBuffer
|
||||
) {
|
||||
// do nothing, let XHR handle these types of bodies without a Content-Type header
|
||||
} else {
|
||||
options.body = JSON.stringify(options.body);
|
||||
options.headers["Content-Type"] = "application/json";
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(options.method == "POST" ||
|
||||
options.method == "PUT" ||
|
||||
options.method == "PATCH") &&
|
||||
options.progress
|
||||
) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
options.progress({
|
||||
loaded: event.loaded,
|
||||
total: event.total,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open(options.method, url);
|
||||
for (const header in options.headers) {
|
||||
xhr.setRequestHeader(header, options.headers[header]);
|
||||
}
|
||||
xhr.onload = function () {
|
||||
const result = {
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
url: url,
|
||||
headers: {},
|
||||
data: "",
|
||||
};
|
||||
|
||||
const headersString = xhr.getAllResponseHeaders();
|
||||
const headersArray = headersString.trim().split("\n");
|
||||
headersArray.forEach((header) => {
|
||||
const [name, value] = header.trim().split(":");
|
||||
result.headers[name] = value.trim();
|
||||
});
|
||||
|
||||
if (
|
||||
xhr.response &&
|
||||
result.headers["content-type"] == "application/json"
|
||||
) {
|
||||
try {
|
||||
result.data = JSON.parse(xhr.response);
|
||||
} catch (error) {
|
||||
result.data = xhr.response;
|
||||
}
|
||||
} else {
|
||||
result.data = xhr.response;
|
||||
}
|
||||
|
||||
useApplicationStore().unavailable = false;
|
||||
if (xhr.status < 300) {
|
||||
if (options.callback) {
|
||||
options.callback(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
if (xhr.status == 503) {
|
||||
useApplicationStore().unavailable = true;
|
||||
}
|
||||
|
||||
if (options.callback) {
|
||||
options.callback(result);
|
||||
} else {
|
||||
reject(result);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
xhr.send(options.body as XMLHttpRequestBodyInit);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
} else {
|
||||
const fetchOptions: RequestInit = {
|
||||
method: options.method.toUpperCase() || "GET",
|
||||
headers: options.headers,
|
||||
signal: options.signal || null,
|
||||
};
|
||||
|
||||
if (
|
||||
(typeof options.body == "string" &&
|
||||
options.body.length > 0) ||
|
||||
options.body instanceof FormData
|
||||
) {
|
||||
fetchOptions.body = options.body;
|
||||
}
|
||||
|
||||
if (fetchOptions.method == "GET" && options.callback) {
|
||||
const cache = useCacheStore().getCacheByUrl(url);
|
||||
if (cache != null) {
|
||||
options.callback(cache);
|
||||
}
|
||||
}
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then(async (response) => {
|
||||
let data: string | object = "";
|
||||
if (response.headers.get("content-length") !== "0") {
|
||||
if (
|
||||
response &&
|
||||
response.headers.get("content-type") == null
|
||||
) {
|
||||
try {
|
||||
data = response.json
|
||||
? await response.json()
|
||||
: {};
|
||||
} catch (error) {
|
||||
try {
|
||||
data = response.text
|
||||
? await response.text()
|
||||
: "";
|
||||
} catch (error) {
|
||||
data = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data =
|
||||
response && response.json
|
||||
? await response.json()
|
||||
: {};
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: response.url,
|
||||
headers: response.headers,
|
||||
data: data,
|
||||
};
|
||||
|
||||
useApplicationStore().unavailable = false;
|
||||
if (response.status >= 300) {
|
||||
if (response.status === 503) {
|
||||
useApplicationStore().unavailable = true;
|
||||
}
|
||||
|
||||
if (options.callback) {
|
||||
options.callback(result);
|
||||
} else {
|
||||
reject(result);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.callback) {
|
||||
if (fetchOptions.method == "GET") {
|
||||
const modified = useCacheStore().updateCache(
|
||||
url,
|
||||
result,
|
||||
);
|
||||
|
||||
if (modified == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
options.callback(result);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Handle any errors thrown during the fetch process
|
||||
const { response, ...rest } = error;
|
||||
const result = {
|
||||
...rest,
|
||||
response: response && response.json(),
|
||||
};
|
||||
|
||||
if (options.callback) {
|
||||
options.callback(result);
|
||||
} else {
|
||||
reject(result);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
get: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||
let apiOptions = {} as ApiOptions;
|
||||
|
||||
if (typeof options == "string") {
|
||||
apiOptions.url = options;
|
||||
} else {
|
||||
apiOptions = options;
|
||||
}
|
||||
|
||||
apiOptions.method = "GET";
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
post: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||
let apiOptions = {} as ApiOptions;
|
||||
|
||||
if (typeof options == "string") {
|
||||
apiOptions.url = options;
|
||||
} else {
|
||||
apiOptions = options;
|
||||
}
|
||||
|
||||
apiOptions.method = "POST";
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
put: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||
let apiOptions = {} as ApiOptions;
|
||||
|
||||
if (typeof options == "string") {
|
||||
apiOptions.url = options;
|
||||
} else {
|
||||
apiOptions = options;
|
||||
}
|
||||
|
||||
apiOptions.method = "PUT";
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
delete: async function (
|
||||
options: ApiOptions | string,
|
||||
): Promise<ApiResponse> {
|
||||
let apiOptions = {} as ApiOptions;
|
||||
|
||||
if (typeof options == "string") {
|
||||
apiOptions.url = options;
|
||||
} else {
|
||||
apiOptions = options;
|
||||
}
|
||||
|
||||
apiOptions.method = "DELETE";
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
|
||||
chunk: async function (options: ApiOptions | string): Promise<ApiResponse> {
|
||||
let apiOptions = {} as ApiOptions;
|
||||
|
||||
// setup api options
|
||||
if (typeof options == "string") {
|
||||
apiOptions.url = options;
|
||||
} else {
|
||||
apiOptions = options;
|
||||
}
|
||||
|
||||
// set method to post by default
|
||||
if (!Object.prototype.hasOwnProperty.call(apiOptions, "method")) {
|
||||
apiOptions.method = "POST";
|
||||
}
|
||||
|
||||
// check for chunk option
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(apiOptions, "chunk") &&
|
||||
Object.prototype.hasOwnProperty.call(apiOptions, "body") &&
|
||||
apiOptions.body instanceof FormData
|
||||
) {
|
||||
if (apiOptions.body.has(apiOptions.chunk)) {
|
||||
const file = apiOptions.body.get(apiOptions.chunk);
|
||||
|
||||
if (file instanceof File) {
|
||||
const chunkSize = 2 * 1024 * 1024;
|
||||
let chunk = 0;
|
||||
let chunkCount = 1;
|
||||
let job_id = -1;
|
||||
|
||||
if (file.size > chunkSize) {
|
||||
chunkCount = Math.ceil(file.size / chunkSize);
|
||||
}
|
||||
|
||||
let result = null;
|
||||
for (chunk = 0; chunk < chunkCount; chunk++) {
|
||||
const offset = chunk * chunkSize;
|
||||
const fileChunk = file.slice(
|
||||
offset,
|
||||
offset + chunkSize,
|
||||
);
|
||||
|
||||
const chunkFormData = new FormData();
|
||||
if (job_id == -1) {
|
||||
for (const [field, value] of apiOptions.body) {
|
||||
chunkFormData.append(field, value);
|
||||
}
|
||||
|
||||
chunkFormData.append("name", file.name);
|
||||
chunkFormData.append("size", file.size.toString());
|
||||
chunkFormData.append("mime_type", file.type);
|
||||
} else {
|
||||
chunkFormData.append("job_id", job_id.toString());
|
||||
}
|
||||
|
||||
chunkFormData.set(apiOptions.chunk, fileChunk);
|
||||
chunkFormData.append("chunk", (chunk + 1).toString());
|
||||
chunkFormData.append(
|
||||
"chunk_count",
|
||||
chunkCount.toString(),
|
||||
);
|
||||
|
||||
const chunkOptions = {
|
||||
method: apiOptions.method,
|
||||
url: apiOptions.url,
|
||||
params: apiOptions.params || {},
|
||||
body: chunkFormData,
|
||||
headers: apiOptions.headers || {},
|
||||
progress: (progressEvent) => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
apiOptions,
|
||||
"progress",
|
||||
)
|
||||
) {
|
||||
apiOptions.progress({
|
||||
loaded:
|
||||
chunk * chunkSize +
|
||||
progressEvent.loaded,
|
||||
total: file.size,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
result = await this.send(chunkOptions);
|
||||
job_id = result.data.media_job.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await this.send(apiOptions);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an api result data as type.
|
||||
* @param result The api result object.
|
||||
* @param defaultValue The default data to return if no result exists.
|
||||
* @returns Data object.
|
||||
*/
|
||||
export function getApiResultData<T>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
result: any,
|
||||
defaultValue: T | null = null,
|
||||
): T | null {
|
||||
if (!result || !Object.prototype.hasOwnProperty.call(result, "data")) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const data = result.data as T;
|
||||
return data instanceof Object ? data : defaultValue;
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
export type Booleanish = boolean | "true" | "false";
|
||||
|
||||
export type EmptyObject = { [key: string]: never };
|
||||
|
||||
export interface SessionRequest {
|
||||
id: number;
|
||||
session_id: number;
|
||||
type: string;
|
||||
path: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: number;
|
||||
ip: string;
|
||||
useragent: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
ended_at: string;
|
||||
requests?: SessionRequest[];
|
||||
}
|
||||
|
||||
export interface SessionCollection {
|
||||
sessions: Session[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SessionRequestCollection {
|
||||
session: Session;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
id: string;
|
||||
title: string;
|
||||
hero: Media;
|
||||
content: string;
|
||||
start_at: string;
|
||||
end_at: string;
|
||||
publish_at: string;
|
||||
location: string;
|
||||
location_url: string;
|
||||
address: string;
|
||||
status: string;
|
||||
registration_type: string;
|
||||
registration_data: string;
|
||||
price: string;
|
||||
ages: string;
|
||||
attachments: Array<Media>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface EventResponse {
|
||||
event: Event;
|
||||
}
|
||||
|
||||
export interface EventCollection {
|
||||
events: Event[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
name: string;
|
||||
mime_type: string;
|
||||
security_type: string;
|
||||
size: number;
|
||||
storage: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
description: string;
|
||||
dimensions: string;
|
||||
variants: { [key: string]: string };
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
jobs: Array<MediaJob>;
|
||||
}
|
||||
|
||||
export interface MediaResponse {
|
||||
medium: Media;
|
||||
}
|
||||
|
||||
export interface MediaCollection {
|
||||
media: Array<Media>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface MediaJob {
|
||||
id: string;
|
||||
media_id: string;
|
||||
user_id: string;
|
||||
status: string;
|
||||
status_text: string;
|
||||
progress: number;
|
||||
progress_max: number;
|
||||
}
|
||||
|
||||
export interface MediaJobResponse {
|
||||
media_job: MediaJob;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
user_id: string;
|
||||
user: User;
|
||||
content: string;
|
||||
publish_at: string;
|
||||
hero: Media;
|
||||
gallery: Array<Media>;
|
||||
attachments: Array<Media>;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
user: User;
|
||||
content: string;
|
||||
publish_at: string;
|
||||
hero: Media;
|
||||
attachments: Array<Media>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ArticleResponse {
|
||||
article: Article;
|
||||
}
|
||||
|
||||
export interface ArticleCollection {
|
||||
articles: Array<Article>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone: string;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface UserCollection {
|
||||
users: Array<User>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
user: User;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface LogsDiscordResponse {
|
||||
log: {
|
||||
output: string;
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Shortlink {
|
||||
id: number;
|
||||
code: string;
|
||||
url: string;
|
||||
used: number;
|
||||
}
|
||||
|
||||
export interface ShortlinkCollection {
|
||||
shortlinks: Array<Shortlink>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ShortlinkResponse {
|
||||
shortlink: Shortlink;
|
||||
}
|
||||
|
||||
export interface ApiInfo {
|
||||
version: string;
|
||||
max_upload_size: number;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Test if array has a match using basic search (* means anything)
|
||||
*
|
||||
* @param {Array<string>} arr The array to search.
|
||||
* @param {string} str The string to find.
|
||||
* @returns {boolean} if the array has the string.
|
||||
*/
|
||||
export const arrayHasBasicMatch = (
|
||||
arr: Array<string>,
|
||||
str: string
|
||||
): boolean => {
|
||||
let matches = false;
|
||||
|
||||
arr.every((elem) => {
|
||||
elem = elem.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
||||
const regex = new RegExp("^" + elem.replace("*", ".*?") + "$", "i");
|
||||
if (str.match(regex)) {
|
||||
matches = true;
|
||||
}
|
||||
return !matches;
|
||||
});
|
||||
|
||||
return matches;
|
||||
};
|
||||
@@ -1,462 +0,0 @@
|
||||
export class SMDate {
|
||||
date: Date | null = null;
|
||||
dayString: string[] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
fullDayString: string[] = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
||||
monthString: string[] = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
];
|
||||
|
||||
fullMonthString: string[] = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
constructor(
|
||||
dateOrString: string | Date = "",
|
||||
options: { format?: string; utc?: boolean } = {},
|
||||
) {
|
||||
this.date = null;
|
||||
|
||||
if (typeof dateOrString === "string") {
|
||||
if (dateOrString.length > 0) {
|
||||
this.parse(dateOrString, options);
|
||||
}
|
||||
} else if (
|
||||
dateOrString instanceof Date &&
|
||||
!Number.isNaN(dateOrString.getTime())
|
||||
) {
|
||||
this.date = dateOrString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string date into a Date object
|
||||
* @param {string} dateString The date string.
|
||||
* @param {object} options (optional) Options object.
|
||||
* @param {string} options.format (optional) The format of the date string.
|
||||
* @param {boolean} options.utc (optional) The date string is UTC.
|
||||
* @returns {SMDate} SMDate object.
|
||||
*/
|
||||
public parse(
|
||||
dateString: string,
|
||||
{ format = "dmy", utc = false } = {},
|
||||
): SMDate {
|
||||
const now = new Date();
|
||||
let time = "";
|
||||
|
||||
if (dateString.toLowerCase() === "now") {
|
||||
this.date = now;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Cache regular expressions
|
||||
const isoDateRegex =
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,10})?Z$/i;
|
||||
const timeRegex =
|
||||
/^(\d+)(?::(\d+))?(?::(\d+))? ?(am?|a\.m\.|pm?|p\.m\.)?$/i;
|
||||
|
||||
// Test if the dateString is in ISO 8601
|
||||
if (isoDateRegex.test(dateString)) {
|
||||
format = "YMd";
|
||||
[dateString, time] = dateString.split("T");
|
||||
time = time.slice(0, -8);
|
||||
}
|
||||
|
||||
// Split the date string into an array of components based on the length of each date component
|
||||
const components = dateString.split(/[ /-]/);
|
||||
|
||||
const [day, month, year] =
|
||||
format === "dmy"
|
||||
? components
|
||||
: format === "mdy"
|
||||
? [components[1], components[0], components[2]]
|
||||
: [components[2], components[1], components[0]];
|
||||
|
||||
if (year === undefined || year.length === 3 || year.length >= 5) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// numeric
|
||||
for (const component of [day, month, year]) {
|
||||
if (isNaN(parseInt(component))) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const parsedDay = parseInt(day.padStart(2, "0"), 10);
|
||||
const parsedMonth = this.getMonthAsNumber(month);
|
||||
const parsedYear = parseInt(year.padStart(4, "20"), 10);
|
||||
let parsedHours: number = 0,
|
||||
parsedMinutes: number = 0,
|
||||
parsedSeconds: number = 0;
|
||||
|
||||
if (time.length == 0 && components.length > 3) {
|
||||
time = components.slice(3).join(" ");
|
||||
}
|
||||
const parsedTime = timeRegex.exec(time);
|
||||
if (time && parsedTime) {
|
||||
const [_, hourStr, minuteStr, secondStr, ampm] = parsedTime;
|
||||
parsedHours = parseInt(hourStr);
|
||||
parsedMinutes = parseInt(minuteStr || "0");
|
||||
parsedSeconds = parseInt(secondStr || "0");
|
||||
|
||||
if (parsedHours < 0 || parsedHours > 23) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (ampm) {
|
||||
if (/pm/i.test(ampm) && parsedHours < 12) {
|
||||
parsedHours += 12;
|
||||
} else if (/am/i.test(ampm) && parsedHours === 12) {
|
||||
parsedHours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
parsedMinutes < 0 ||
|
||||
parsedMinutes > 59 ||
|
||||
parsedSeconds < 0 ||
|
||||
parsedSeconds > 59
|
||||
) {
|
||||
return this;
|
||||
}
|
||||
|
||||
time = `${parsedHours.toString().padStart(2, "0")}:${parsedMinutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${parsedSeconds
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
} else {
|
||||
time = "00:00:00";
|
||||
}
|
||||
|
||||
const date = utc
|
||||
? new Date(
|
||||
Date.UTC(
|
||||
parsedYear,
|
||||
parsedMonth - 1,
|
||||
parsedDay,
|
||||
parsedHours,
|
||||
parsedMinutes,
|
||||
parsedSeconds,
|
||||
),
|
||||
)
|
||||
: new Date(
|
||||
parsedYear,
|
||||
parsedMonth - 1,
|
||||
parsedDay,
|
||||
parsedHours,
|
||||
parsedMinutes,
|
||||
parsedSeconds,
|
||||
);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (utc) {
|
||||
const isoDate = date.toISOString();
|
||||
const checkYear = parseInt(isoDate.substring(0, 4), 10);
|
||||
const checkMonth = parseInt(isoDate.substring(5, 7), 10);
|
||||
const checkDay = new Date(isoDate).getUTCDate();
|
||||
const checkHours = parseInt(isoDate.substring(11, 13), 10);
|
||||
const checkMinutes = parseInt(isoDate.substring(14, 16), 10);
|
||||
const checkSeconds = parseInt(isoDate.substring(17, 19), 10);
|
||||
if (
|
||||
checkYear !== parsedYear ||
|
||||
checkMonth !== parsedMonth ||
|
||||
checkDay !== parsedDay ||
|
||||
checkHours !== parsedHours ||
|
||||
checkMinutes !== parsedMinutes ||
|
||||
checkSeconds !== parsedSeconds
|
||||
) {
|
||||
return this;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
date.getFullYear() !== parsedYear ||
|
||||
date.getMonth() + 1 !== parsedMonth ||
|
||||
date.getDate() !== parsedDay ||
|
||||
date.getHours() !== parsedHours ||
|
||||
date.getMinutes() !== parsedMinutes ||
|
||||
date.getSeconds() !== parsedSeconds
|
||||
) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
this.date = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the date to a string.
|
||||
* @param {string} format The format to return.
|
||||
* @param {object} options (optional) Function options.
|
||||
* @param {boolean} options.utc (optional) Format the date to be as UTC instead of local.
|
||||
* @returns {string} The formatted date.
|
||||
*/
|
||||
public format(format: string, options: { utc?: boolean } = {}): string {
|
||||
if (this.date == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let result = format;
|
||||
|
||||
let year: string,
|
||||
month: string,
|
||||
date: string,
|
||||
day: number,
|
||||
hour: string,
|
||||
min: string,
|
||||
sec: string;
|
||||
if (options.utc) {
|
||||
const isoDate = this.date.toISOString();
|
||||
year = isoDate.substring(0, 4);
|
||||
month = isoDate.substring(5, 7);
|
||||
date = isoDate.substring(8, 10);
|
||||
day = new Date(isoDate).getUTCDay();
|
||||
hour = isoDate.substring(11, 13);
|
||||
min = isoDate.substring(14, 16);
|
||||
sec = isoDate.substring(17, 19);
|
||||
} else {
|
||||
year = this.date.getFullYear().toString();
|
||||
month = (this.date.getMonth() + 1).toString();
|
||||
date = this.date.getDate().toString();
|
||||
day = this.date.getDay();
|
||||
hour = this.date.getHours().toString();
|
||||
min = this.date.getMinutes().toString();
|
||||
sec = this.date.getSeconds().toString();
|
||||
}
|
||||
|
||||
const apm = parseInt(hour, 10) >= 12 ? "pm" : "am";
|
||||
/* eslint-disable indent */
|
||||
const apmhours = (
|
||||
parseInt(hour, 10) > 12
|
||||
? parseInt(hour, 10) - 12
|
||||
: parseInt(hour, 10) == 0
|
||||
? 12
|
||||
: parseInt(hour, 10)
|
||||
).toString();
|
||||
/* eslint-enable indent */
|
||||
|
||||
// year
|
||||
result = result.replace(/\byy\b/g, year.slice(-2));
|
||||
result = result.replace(/\byyyy\b/g, year);
|
||||
|
||||
// month
|
||||
result = result.replace(/\bM\b/g, month);
|
||||
result = result.replace(/\bMM\b/g, (0 + month).slice(-2));
|
||||
result = result.replace(
|
||||
/\bMMM\b/g,
|
||||
this.monthString[parseInt(month) - 1],
|
||||
);
|
||||
result = result.replace(
|
||||
/\bMMMM\b/g,
|
||||
this.fullMonthString[parseInt(month) - 1],
|
||||
);
|
||||
|
||||
// day
|
||||
result = result.replace(/\bd\b/g, date);
|
||||
result = result.replace(/\bdd\b/g, (0 + date).slice(-2));
|
||||
result = result.replace(/\bEEE\b/g, this.dayString[day]);
|
||||
result = result.replace(/\bEEEE\b/g, this.fullDayString[day]);
|
||||
|
||||
// hour
|
||||
result = result.replace(/\bH\b/g, hour);
|
||||
result = result.replace(/\bHH\b/g, (0 + hour).slice(-2));
|
||||
result = result.replace(/\bh\b/g, apmhours);
|
||||
result = result.replace(/\bhh\b/g, (0 + apmhours).slice(-2));
|
||||
|
||||
// min
|
||||
result = result.replace(/\bm\b/g, min);
|
||||
result = result.replace(/\bmm\b/g, (0 + min).slice(-2));
|
||||
|
||||
// sec
|
||||
result = result.replace(/\bs\b/g, sec);
|
||||
result = result.replace(/\bss\b/g, (0 + sec).slice(-2));
|
||||
|
||||
// am/pm
|
||||
result = result.replace(/\baa\b/g, apm);
|
||||
result = result.replace(/\bAA\b/g, apm.toUpperCase());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a relative date string from now.
|
||||
* @returns {string} A relative date string.
|
||||
*/
|
||||
public relative(): string {
|
||||
if (this.date === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let dif = Math.round((now.getTime() - this.date.getTime()) / 1000);
|
||||
const format = dif < 0 ? "in %" : "% ago";
|
||||
dif = Math.abs(dif);
|
||||
|
||||
if (dif < 60) {
|
||||
return "Just now";
|
||||
} else if (dif < 3600) {
|
||||
const v = Math.round(dif / 60);
|
||||
return format.replace("%", `${v} min${v != 1 ? "s" : ""}`);
|
||||
} else if (dif < 86400) {
|
||||
const v = Math.round(dif / 3600);
|
||||
return format.replace("%", `${v} hour${v != 1 ? "s" : ""}`);
|
||||
} else if (dif < 604800) {
|
||||
const v = Math.round(dif / 86400);
|
||||
return format.replace("%", `${v} day${v != 1 ? "s" : ""}`);
|
||||
} else if (dif < 2419200) {
|
||||
const v = Math.round(dif / 604800);
|
||||
return format.replace("%", `${v} week${v != 1 ? "s" : ""}`);
|
||||
} else {
|
||||
return (
|
||||
this.monthString[this.date.getMonth()] +
|
||||
" " +
|
||||
this.date.getDate() +
|
||||
", " +
|
||||
this.date.getFullYear()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the date is before the passed date.
|
||||
* @param {Date|SMDate} d (optional) The date to check. If none, use now
|
||||
* @returns {boolean} If the date is before the passed date.
|
||||
*/
|
||||
public isBefore(d: Date | SMDate = new SMDate("now")): boolean {
|
||||
const otherDate = d instanceof SMDate ? d.date : d;
|
||||
if (otherDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.date == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return otherDate > this.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the date is after the passed date.
|
||||
* @param {Date|SMDate} d (optional) The date to check. If none, use now
|
||||
* @returns {boolean} If the date is after the passed date.
|
||||
*/
|
||||
public isAfter(d: Date | SMDate = new SMDate("now")): boolean {
|
||||
const otherDate = d instanceof SMDate ? d.date : d;
|
||||
if (otherDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.date == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return otherDate < this.date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a month number from a string or a month number or month name
|
||||
* @param {string} monthString The month string as number or name
|
||||
* @returns {number} The month number
|
||||
*/
|
||||
private getMonthAsNumber(monthString: string): number {
|
||||
const months = this.fullMonthString.map((month) => month.toLowerCase());
|
||||
|
||||
const shortMonths = months.map((month) => month.slice(0, 3));
|
||||
const monthIndex = months.indexOf(monthString.toLowerCase());
|
||||
if (monthIndex !== -1) {
|
||||
return monthIndex + 1;
|
||||
}
|
||||
const shortMonthIndex = shortMonths.indexOf(monthString.toLowerCase());
|
||||
if (shortMonthIndex !== -1) {
|
||||
return shortMonthIndex + 1;
|
||||
}
|
||||
const monthNumber = parseInt(monthString, 10);
|
||||
if (!isNaN(monthNumber) && monthNumber >= 1 && monthNumber <= 12) {
|
||||
return monthNumber;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the current date is valid.
|
||||
* @returns {boolean} If the current date is valid.
|
||||
*/
|
||||
public isValid(): boolean {
|
||||
return this.date !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string with only the first occurrence of characters
|
||||
* @param {string} str The string to modify.
|
||||
* @param {string} characters The characters to use to test.
|
||||
* @returns {string} A string that only contains the first occurrence of the characters.
|
||||
*/
|
||||
private onlyFirstOccurrence(
|
||||
str: string,
|
||||
characters: string = "dMy",
|
||||
): string {
|
||||
let findCharacters = characters.split("");
|
||||
const replaceRegex = new RegExp("[^" + characters + "]", "g");
|
||||
let result = "";
|
||||
|
||||
str = str.replace(replaceRegex, "");
|
||||
if (str.length > 0) {
|
||||
str.split("").forEach((strChar) => {
|
||||
if (
|
||||
findCharacters.length > 0 &&
|
||||
findCharacters.includes(strChar)
|
||||
) {
|
||||
result += strChar;
|
||||
|
||||
const index = findCharacters.findIndex(
|
||||
(findChar) => findChar === strChar,
|
||||
);
|
||||
if (index !== -1) {
|
||||
findCharacters = findCharacters
|
||||
.slice(0, index)
|
||||
.concat(findCharacters.slice(index + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
type DebounceCallback = (...args: unknown[]) => void;
|
||||
type DebounceResult = (...args: unknown[]) => void;
|
||||
|
||||
/**
|
||||
* Call a function after a delay once.
|
||||
*
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {number} delay The delay before calling function.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const debounce = (
|
||||
fn: DebounceCallback,
|
||||
delay: number
|
||||
): DebounceResult => {
|
||||
let timeoutID: NodeJS.Timeout | null = null;
|
||||
return (...args) => {
|
||||
if (timeoutID != null) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const that = this;
|
||||
timeoutID = setTimeout(function () {
|
||||
fn.apply(that, args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
import { ApiResponse } from "./api";
|
||||
import {
|
||||
createValidationResult,
|
||||
defaultValidationResult,
|
||||
ValidationObject,
|
||||
ValidationResult,
|
||||
} from "./validate";
|
||||
|
||||
type FormObjectValidateFunction = (item?: string | null) => Promise<boolean>;
|
||||
type FormObjectLoadingFunction = (state?: boolean) => boolean;
|
||||
type FormObjectMessageFunction = (
|
||||
message?: string,
|
||||
type?: string,
|
||||
icon?: string,
|
||||
) => void;
|
||||
type FormObjectErrorFunction = (message: string) => void;
|
||||
type FormObjectApiErrorsFunction = (
|
||||
apiErrors: ApiResponse,
|
||||
callback?: (error: string, status: number) => void,
|
||||
) => void;
|
||||
|
||||
export interface FormObject {
|
||||
validate: FormObjectValidateFunction;
|
||||
loading: FormObjectLoadingFunction;
|
||||
message: FormObjectMessageFunction;
|
||||
error: FormObjectErrorFunction;
|
||||
apiErrors: FormObjectApiErrorsFunction;
|
||||
_loading: boolean;
|
||||
_message: string;
|
||||
_messageType: string;
|
||||
_messageIcon: string;
|
||||
controls: { [key: string]: FormControlObject };
|
||||
}
|
||||
|
||||
const defaultFormObject: FormObject = {
|
||||
validate: async function (item = null) {
|
||||
const keys = item ? [item] : Object.keys(this.controls);
|
||||
let valid = true;
|
||||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
if (
|
||||
typeof this.controls[key] == "object" &&
|
||||
Object.keys(this.controls[key]).includes("validation")
|
||||
) {
|
||||
const validationResult = await this.controls[
|
||||
key
|
||||
].validation.validator.validate(this.controls[key].value);
|
||||
this.controls[key].validation.result = validationResult;
|
||||
|
||||
if (!validationResult.valid) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return valid;
|
||||
},
|
||||
loading: function (state = undefined) {
|
||||
if (state !== undefined) {
|
||||
this._loading = state;
|
||||
}
|
||||
|
||||
return this._loading;
|
||||
},
|
||||
message: function (message = "", type = "", icon = "") {
|
||||
this._message = message;
|
||||
|
||||
if (type.length > 0) {
|
||||
this._messageType = type;
|
||||
}
|
||||
if (icon.length > 0) {
|
||||
this._messageIcon = icon;
|
||||
}
|
||||
},
|
||||
error: function (message = "") {
|
||||
if (message == "") {
|
||||
this.message("");
|
||||
} else {
|
||||
this.message(message, "error", "alert-circle-outline");
|
||||
}
|
||||
},
|
||||
apiErrors: function (
|
||||
apiResponse: ApiResponse,
|
||||
callback?: (error: string, status: number) => void,
|
||||
) {
|
||||
let foundKeys = false;
|
||||
|
||||
if (
|
||||
apiResponse.data &&
|
||||
typeof apiResponse.data === "object" &&
|
||||
"errors" in apiResponse.data
|
||||
) {
|
||||
const errors = apiResponse.data.errors as Record<string, string>;
|
||||
Object.keys(errors).forEach((key) => {
|
||||
if (
|
||||
typeof this.controls[key] === "object" &&
|
||||
Object.keys(this.controls[key]).includes("validation")
|
||||
) {
|
||||
foundKeys = true;
|
||||
this.controls[key].validation.result =
|
||||
createValidationResult(false, errors[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (foundKeys == false) {
|
||||
const errorMessage =
|
||||
(apiResponse?.json?.message as string) ||
|
||||
"An unknown server error occurred.\nPlease try again later.";
|
||||
|
||||
if (callback) {
|
||||
callback(errorMessage, apiResponse.status);
|
||||
} else {
|
||||
this.error(errorMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
controls: {},
|
||||
|
||||
_loading: false,
|
||||
_message: "",
|
||||
_messageType: "primary",
|
||||
_messageIcon: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new Form object.
|
||||
* @param {Record<string, FormControlObject>} controls The controls included in the form.
|
||||
* @returns {FormObject} Returns a form object.
|
||||
*/
|
||||
export const Form = (
|
||||
controls: Record<string, FormControlObject>,
|
||||
): FormObject => {
|
||||
const form = { ...defaultFormObject };
|
||||
form.controls = controls;
|
||||
|
||||
form._loading = false;
|
||||
form._message = "";
|
||||
form._messageType = "primary";
|
||||
form._messageIcon = "";
|
||||
|
||||
return form;
|
||||
};
|
||||
|
||||
interface FormControlValidation {
|
||||
validator: ValidationObject;
|
||||
result: ValidationResult;
|
||||
}
|
||||
|
||||
const getDefaultFormControlValidation = (): FormControlValidation => {
|
||||
return {
|
||||
validator: {
|
||||
validate: async (): Promise<ValidationResult> => {
|
||||
return defaultValidationResult;
|
||||
},
|
||||
},
|
||||
result: defaultValidationResult,
|
||||
};
|
||||
};
|
||||
|
||||
type FormControlClearValidations = () => void;
|
||||
type FormControlSetValidation = (
|
||||
valid: boolean,
|
||||
message?: string | Array<string>,
|
||||
) => void;
|
||||
type FormControlIsValid = () => boolean;
|
||||
|
||||
export interface FormControlObject {
|
||||
value: unknown;
|
||||
validate: () => Promise<ValidationResult>;
|
||||
validation: FormControlValidation;
|
||||
clearValidations: FormControlClearValidations;
|
||||
setValidationResult: FormControlSetValidation;
|
||||
isValid: FormControlIsValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new form control object.
|
||||
* @param {string} value The control name.
|
||||
* @param {ValidationObject | null} validator The control validation rules.
|
||||
* @returns {FormControlObject} The form control object.
|
||||
*/
|
||||
export const FormControl = (
|
||||
value: unknown = "",
|
||||
validator: ValidationObject | null = null,
|
||||
): FormControlObject => {
|
||||
return {
|
||||
value: value,
|
||||
validation:
|
||||
validator == null
|
||||
? getDefaultFormControlValidation()
|
||||
: {
|
||||
validator: validator,
|
||||
result: defaultValidationResult,
|
||||
},
|
||||
clearValidations: function () {
|
||||
this.validation.result = defaultValidationResult;
|
||||
},
|
||||
setValidationResult: function (
|
||||
valid: boolean,
|
||||
message?: string | Array<string>,
|
||||
) {
|
||||
this.validation.result = createValidationResult(valid, message);
|
||||
},
|
||||
validate: async function () {
|
||||
if (this.validation.validator) {
|
||||
this.validation.result =
|
||||
await this.validation.validator.validate(this.value);
|
||||
|
||||
return this.validation.result;
|
||||
}
|
||||
|
||||
return defaultValidationResult;
|
||||
},
|
||||
isValid: function () {
|
||||
return this.validation.result.valid;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import { ImportMetaExtras } from "../../../import-meta";
|
||||
import { urlStripAttributes } from "./url";
|
||||
|
||||
type ImageLoadCallback = (url: string) => void;
|
||||
|
||||
export const imageLoad = (
|
||||
url: string,
|
||||
callback: ImageLoadCallback,
|
||||
postfix = "size=thumb"
|
||||
) => {
|
||||
if (
|
||||
url.startsWith((import.meta as ImportMetaExtras).env.APP_URL) === true
|
||||
) {
|
||||
callback(urlStripAttributes(url) + "?" + postfix);
|
||||
const tmp = new Image();
|
||||
tmp.onload = function () {
|
||||
callback(url);
|
||||
};
|
||||
tmp.src = url;
|
||||
} else {
|
||||
// Image is not one we control
|
||||
callback(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const imageSize = (size: string, url: string) => {
|
||||
const availableSizes = [
|
||||
"thumb",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"xxlarge",
|
||||
];
|
||||
if (availableSizes.includes(size)) {
|
||||
if (
|
||||
url.startsWith((import.meta as ImportMetaExtras).env.APP_URL) ===
|
||||
true ||
|
||||
url.startsWith("/") === true
|
||||
) {
|
||||
return `${url}?size=${size}`;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
// Thumb 150 x 150
|
||||
export const imageThumb = (url: string) => {
|
||||
return imageSize("thumb", url);
|
||||
};
|
||||
|
||||
// Small 300 x 300
|
||||
export const imageSmall = (url: string) => {
|
||||
return imageSize("small", url);
|
||||
};
|
||||
|
||||
// Small 640 x 640
|
||||
export const imageMedium = (url: string) => {
|
||||
return imageSize("medium", url);
|
||||
};
|
||||
|
||||
// Large 1024 x 1024
|
||||
export const imageLarge = (url: string) => {
|
||||
return imageSize("large", url);
|
||||
};
|
||||
|
||||
// Large 1536 x 1536
|
||||
export const imageXLarge = (url: string) => {
|
||||
return imageSize("xlarge", url);
|
||||
};
|
||||
|
||||
// Large 2560 x 2560
|
||||
export const imageXXLarge = (url: string) => {
|
||||
return imageSize("xxlarge", url);
|
||||
};
|
||||
|
||||
// Full size
|
||||
export const imageFull = (url: string) => {
|
||||
return imageSize("full", url);
|
||||
};
|
||||
@@ -1,309 +0,0 @@
|
||||
import { ImportMetaExtras } from "../../../import-meta";
|
||||
import { Media, MediaJob } from "./api.types";
|
||||
import { strCaseCmp, toTitleCase } from "./string";
|
||||
|
||||
export const mediaGetVariantUrl = (
|
||||
media: Media,
|
||||
variant = "scaled",
|
||||
): string => {
|
||||
if (!media) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// If the variant is 'original', return the media url
|
||||
if (variant === "original") {
|
||||
return media.url;
|
||||
}
|
||||
|
||||
// If the variant key exists in media.variants, return the corresponding variant URL
|
||||
if (media.variants && media.variants[variant]) {
|
||||
return media.url.replace(media.name, media.variants[variant]);
|
||||
}
|
||||
|
||||
// If the variant key does not exist, return the 'scaled' variant
|
||||
return media.variants && media.variants["scaled"]
|
||||
? media.url.replace(media.name, media.variants["scaled"])
|
||||
: media.url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Media URL to a user friendly URL
|
||||
* @param {Media|string} mediaOrString Media object or URL string
|
||||
* @returns {string} User friendly URL
|
||||
*/
|
||||
export const mediaGetWebURL = (mediaOrString: Media | string): string => {
|
||||
const webUrl = (import.meta as ImportMetaExtras).env.APP_URL;
|
||||
const apiUrl = (import.meta as ImportMetaExtras).env.APP_URL_API;
|
||||
|
||||
let url =
|
||||
typeof mediaOrString === "string"
|
||||
? mediaOrString
|
||||
: (mediaOrString as Media).url;
|
||||
|
||||
// If the input is a string, use it as the URL directly
|
||||
if (typeof mediaOrString === "string") {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Is the URL an API request?
|
||||
if (url.startsWith(apiUrl)) {
|
||||
const fileUrlPath = url.substring(apiUrl.length);
|
||||
const fileUrlParts = fileUrlPath.split("/");
|
||||
|
||||
if (
|
||||
fileUrlParts.length >= 4 &&
|
||||
fileUrlParts[0].length === 0 &&
|
||||
strCaseCmp("media", fileUrlParts[1]) === true &&
|
||||
strCaseCmp("download", fileUrlParts[3]) === true
|
||||
) {
|
||||
url = webUrl + "/file/" + fileUrlParts[2];
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a mime matches.
|
||||
* @param {string} mimeExpected The mime expected.
|
||||
* @param {string} mimeToCheck The mime to check.
|
||||
* @returns {boolean} The mimeToCheck matches mimeExpected.
|
||||
*/
|
||||
export const mimeMatches = (
|
||||
mimeExpected: string,
|
||||
mimeToCheck: string,
|
||||
): boolean => {
|
||||
if (mimeExpected.length == 0) {
|
||||
mimeExpected = "*";
|
||||
}
|
||||
|
||||
const escapedExpectation = mimeExpected.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
"\\$&",
|
||||
);
|
||||
const pattern = escapedExpectation.replace(/\\\*/g, ".*");
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
|
||||
return regex.test(mimeToCheck);
|
||||
};
|
||||
|
||||
/**
|
||||
* MediaGetThumbnailCallback Type
|
||||
*/
|
||||
export type mediaGetThumbnailCallback = (url: string) => void;
|
||||
|
||||
/**
|
||||
* Get Media/File Thumbnail.
|
||||
* @param {Media|File} media The Media/File object.
|
||||
* @param {string|null} useVariant The variable to use.
|
||||
* @param {mediaGetThumbnailCallback|null} callback Callback with the thumbnail. Required when passing File.
|
||||
* @returns {string} The thumbnail url.
|
||||
*/
|
||||
export const mediaGetThumbnail = (
|
||||
media: Media | File,
|
||||
useVariant: string | null = "",
|
||||
callback: mediaGetThumbnailCallback | null = null,
|
||||
): string => {
|
||||
let url: string = "";
|
||||
|
||||
if (media) {
|
||||
if (media instanceof File) {
|
||||
if (callback != null) {
|
||||
if (mimeMatches("image/*", media.type) == true) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
callback(e.target.result.toString());
|
||||
};
|
||||
|
||||
reader.readAsDataURL(media);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
useVariant &&
|
||||
useVariant != "" &&
|
||||
useVariant != null &&
|
||||
media.variants &&
|
||||
media.variants[useVariant]
|
||||
) {
|
||||
url = media.url.replace(media.name, media.variants[useVariant]);
|
||||
} else if (media.thumbnail && media.thumbnail.length > 0) {
|
||||
url = media.thumbnail;
|
||||
} else if (media.variants && media.variants["thumb"]) {
|
||||
url = media.url.replace(media.name, media.variants["thumb"]);
|
||||
}
|
||||
}
|
||||
|
||||
if (url === "") {
|
||||
url = "/assets/fileicons/unknown.webp";
|
||||
}
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback(url);
|
||||
return "";
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the media is currently busy.
|
||||
* @param {Media} media The media item to check.
|
||||
* @returns {boolean} If the media is busy.
|
||||
*/
|
||||
export const mediaIsBusy = (media: Media): boolean => {
|
||||
let busy = false;
|
||||
|
||||
if (media.jobs) {
|
||||
media.jobs.forEach((item) => {
|
||||
if (
|
||||
item.status != "invalid" &&
|
||||
item.status != "complete" &&
|
||||
item.status != "failed"
|
||||
) {
|
||||
busy = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return busy;
|
||||
};
|
||||
|
||||
interface MediaStatus {
|
||||
busy: boolean;
|
||||
status: string;
|
||||
status_text: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Media status
|
||||
* @param {Media} media The media item to check.
|
||||
* @returns {MediaStatus} The media status.
|
||||
*/
|
||||
export const getMediaStatus = (media: Media): MediaStatus => {
|
||||
const status = {
|
||||
busy: false,
|
||||
status: "",
|
||||
status_text: "",
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
if (media.jobs) {
|
||||
for (const item of media.jobs) {
|
||||
if (
|
||||
item.status != "invalid" &&
|
||||
item.status != "complete" &&
|
||||
item.status != "failed"
|
||||
) {
|
||||
status.busy = true;
|
||||
status.status = item.status;
|
||||
status.status_text = item.status_text;
|
||||
status.progress = item.progress;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current Media status Text
|
||||
* @param {Media} media The media item to check.
|
||||
* @returns {string} Human readable string.
|
||||
*/
|
||||
export const getMediaStatusText = (media: Media): string => {
|
||||
let status = "";
|
||||
|
||||
if (media.jobs.length > 0) {
|
||||
if (
|
||||
media.jobs[0].status != "invalid" &&
|
||||
media.jobs[0].status != "failed" &&
|
||||
media.jobs[0].status != "complete"
|
||||
) {
|
||||
if (media.jobs[0].status_text != "") {
|
||||
status = toTitleCase(media.jobs[0].status_text);
|
||||
} else {
|
||||
status = toTitleCase(media.jobs[0].status);
|
||||
}
|
||||
|
||||
if (media.jobs[0].progress_max != 0) {
|
||||
status += ` ${Math.floor(
|
||||
(media.jobs[0].progress / media.jobs[0].progress_max) * 100,
|
||||
)}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
export interface MediaParams {
|
||||
id?: string;
|
||||
user_id?: string;
|
||||
title?: string;
|
||||
name?: string;
|
||||
mime_type?: string;
|
||||
permission?: string;
|
||||
size?: number;
|
||||
storage?: string;
|
||||
url?: string;
|
||||
thumbnail?: string;
|
||||
description?: string;
|
||||
dimensions?: string;
|
||||
variants?: { [key: string]: string };
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
jobs?: Array<MediaJob>;
|
||||
}
|
||||
|
||||
export interface MediaJobParams {
|
||||
id?: string;
|
||||
media_id?: string;
|
||||
user_id?: string;
|
||||
status?: string;
|
||||
status_text?: string;
|
||||
progress?: number;
|
||||
progress_max?: number;
|
||||
}
|
||||
|
||||
export const createMediaItem = (params?: MediaParams): Media => {
|
||||
const media = {
|
||||
id: params.id || "",
|
||||
user_id: params.user_id || "",
|
||||
title: params.title || "",
|
||||
name: params.name || "",
|
||||
mime_type: params.mime_type || "",
|
||||
permission: params.permission || "",
|
||||
size: params.size !== undefined ? params.size : 0,
|
||||
storage: params.storage || "",
|
||||
url: params.url || "",
|
||||
thumbnail: params.thumbnail || "",
|
||||
description: params.description || "",
|
||||
dimensions: params.dimensions || "",
|
||||
variants: params.variants || {},
|
||||
created_at: params.created_at || "",
|
||||
updated_at: params.updated_at || "",
|
||||
jobs: params.jobs || [],
|
||||
};
|
||||
|
||||
return media;
|
||||
};
|
||||
|
||||
export const createMediaJobItem = (params?: MediaJobParams): MediaJob => {
|
||||
const job = {
|
||||
id: params.id || "",
|
||||
media_id: params.media_id || "",
|
||||
user_id: params.user_id || "",
|
||||
status: params.status || "",
|
||||
status_text: params.status_text || "",
|
||||
progress: params.progress || 0,
|
||||
progress_max: params.progress_max || 0,
|
||||
};
|
||||
|
||||
return job;
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Sort a objects properties alphabetically
|
||||
*
|
||||
* @param {Record<string, unknown>} obj The object to sort
|
||||
* @returns {Record<string, unknown>} The object sorted
|
||||
*/
|
||||
export const sortProperties = (
|
||||
obj: Record<string, unknown>
|
||||
): Record<string, unknown> => {
|
||||
// convert object into array
|
||||
const sortable: [string, unknown][] = [];
|
||||
for (const key in obj)
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key))
|
||||
sortable.push([key, obj[key]]); // each item is an array in format [key, value]
|
||||
|
||||
// sort items by value
|
||||
sortable.sort(function (a, b) {
|
||||
const x = String(a[1]).toLowerCase(),
|
||||
y = String(b[1]).toLowerCase();
|
||||
return x < y ? -1 : x > y ? 1 : 0;
|
||||
});
|
||||
|
||||
const sortedObj: Record<string, unknown> = {};
|
||||
sortable.forEach((item) => {
|
||||
sortedObj[item[0]] = item[1];
|
||||
});
|
||||
|
||||
return sortedObj; // array in format [ [ key1, val1 ], [ key2, val2 ], ... ]
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
export interface SEOTags {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
robots: {
|
||||
index: boolean;
|
||||
follow: boolean;
|
||||
};
|
||||
url: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export const updateSEOTags = (tags: SEOTags): void => {
|
||||
const updateTag = (
|
||||
tag: string,
|
||||
queryAttribName: string,
|
||||
queryAttribValue: string,
|
||||
updateAttribName: string,
|
||||
updateAttribValue: string
|
||||
) => {
|
||||
const existingTag = document.querySelector(
|
||||
`${tag}[${queryAttribName}="${queryAttribValue}"]`
|
||||
);
|
||||
if (existingTag) {
|
||||
existingTag.setAttribute(updateAttribName, updateAttribValue);
|
||||
} else {
|
||||
const metaTag = document.createElement(tag);
|
||||
metaTag.setAttribute(queryAttribName, queryAttribValue);
|
||||
metaTag.setAttribute(updateAttribName, updateAttribValue);
|
||||
document.head.appendChild(metaTag);
|
||||
}
|
||||
};
|
||||
|
||||
const robotsIndexValue = tags.robots.index ? "index" : "noindex";
|
||||
const robotsFollowValue = tags.robots.follow ? "follow" : "nofollow";
|
||||
const robotsValue = `${robotsIndexValue}, ${robotsFollowValue}`;
|
||||
|
||||
document.title = `STEMMechanics | ${tags.title}`;
|
||||
updateTag("meta", "name", "description", "content", tags.description);
|
||||
updateTag("meta", "name", "keywords", "content", tags.keywords.join(", "));
|
||||
updateTag("meta", "name", "robots", "content", robotsValue);
|
||||
updateTag("link", "rel", "canonical", "href", tags.url);
|
||||
updateTag("meta", "property", "og:title", "content", tags.title);
|
||||
updateTag(
|
||||
"meta",
|
||||
"property",
|
||||
"og:description",
|
||||
"content",
|
||||
tags.description
|
||||
);
|
||||
updateTag("meta", "property", "og:image", "content", tags.image);
|
||||
updateTag("meta", "property", "og:url", "content", tags.url);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
const appId = "sandbox-sq0idb-FYI93DDPJk0wJvaU0ye4MQ";
|
||||
const locationId = "LQ0C6GMZEWVQ0";
|
||||
const square = null;
|
||||
|
||||
export const initCard = (): Object => {
|
||||
const scriptSrc = "https://sandbox.web.squarecdn.com/v1/square.js";
|
||||
if (!document.querySelector(`script[src="${scriptSrc}"]`)) {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = scriptSrc;
|
||||
script.onload = async () => {
|
||||
if (!window.Square) {
|
||||
console.log("Square failed to load properly");
|
||||
}
|
||||
|
||||
let payments;
|
||||
try {
|
||||
payments = window.Square.payments(appId, locationId);
|
||||
} catch (e) {
|
||||
console.log("Square: Missing credentials", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let card;
|
||||
try {
|
||||
card = await payments.card();
|
||||
await card.attach("#card-container");
|
||||
} catch (e) {
|
||||
console.error("Initializing Card failed", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
/**
|
||||
* Transforms a string to title case.
|
||||
* @param {string} str The string to transform.
|
||||
* @returns {string} A string transformed to title case.
|
||||
*/
|
||||
export const toTitleCase = (str: string): string => {
|
||||
// Replace underscores and hyphens with spaces
|
||||
str = str.replace(/[_-]+/g, " ");
|
||||
|
||||
// Capitalize the first letter of each word and make the rest lowercase
|
||||
str = str.replace(/\b\w+\b/g, (txt) => {
|
||||
return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
// Replace "cdn" with "CDN"
|
||||
str = str.replace(/\bCdn\b/gi, "CDN");
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a string to a excerpt.
|
||||
* @param {string} txt The text to convert.
|
||||
* @param {number} maxLen (optional) The maximum length of the excerpt.
|
||||
* @param {boolean} strip (optional) Strip HTML tags from the text.
|
||||
* @param stripHtml
|
||||
* @returns {string} The excerpt.
|
||||
*/
|
||||
export function excerpt(
|
||||
txt: string,
|
||||
maxLen: number = 150,
|
||||
stripHtml: boolean = true,
|
||||
): string {
|
||||
if (stripHtml) {
|
||||
txt = txt.replace(/<[^>]+>/g, "").replace(/ /g, " ");
|
||||
}
|
||||
|
||||
const words = txt.trim().split(/\s+/);
|
||||
let curLen = 0;
|
||||
const excerptWords: string[] = [];
|
||||
|
||||
for (const word of words) {
|
||||
if (curLen + word.length + 1 > maxLen) {
|
||||
break;
|
||||
}
|
||||
curLen += word.length + 1;
|
||||
excerptWords.push(word);
|
||||
}
|
||||
|
||||
let excerpt = excerptWords.join(" ");
|
||||
if (curLen < txt.length) {
|
||||
excerpt += "...";
|
||||
}
|
||||
|
||||
return excerpt;
|
||||
}
|
||||
|
||||
/**
|
||||
* String HTML tags from text.
|
||||
* @param {string} txt The text to strip tags.
|
||||
* @returns {string} The stripped text.
|
||||
*/
|
||||
export const stripHtmlTags = (txt: string): string => {
|
||||
return txt.replace(/<(p|br)([ /]*?>|[ /]+.*?>)|<[a-zA-Z/][^>]+(>|$)/g, " ");
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace HTML entities with real characters.
|
||||
* @param {string} txt The text to transform.
|
||||
* @returns {string} Transformed text
|
||||
*/
|
||||
export const replaceHtmlEntities = (txt: string): string => {
|
||||
const translate_re = /&(nbsp|amp|quot|lt|gt);/g;
|
||||
|
||||
return txt.replace(translate_re, function (match, entity) {
|
||||
switch (entity) {
|
||||
case "nbsp":
|
||||
return " ";
|
||||
case "amp":
|
||||
return "&";
|
||||
case "quot":
|
||||
return '"';
|
||||
case "lt":
|
||||
return "<";
|
||||
case "gt":
|
||||
return ">";
|
||||
default:
|
||||
return match;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a string to a number, ignoring items like dollar signs, etc.
|
||||
* @param {string} str The string to convert to a number
|
||||
* @returns {number} A number with the minimum amount of decimal places (or 0)
|
||||
*/
|
||||
export const stringToNumber = (str: string): number => {
|
||||
str = str.replace(/[^\d.-]/g, "");
|
||||
const num = parseFloat(str);
|
||||
return isNaN(num) ? 0 : Number(num.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a number or string to a price (0 or 0.00).
|
||||
* @param {number|string} numOrString The number of string to convert to a price.
|
||||
* @returns {string} The converted result.
|
||||
*/
|
||||
export const toPrice = (numOrString: number | string): string => {
|
||||
const num =
|
||||
typeof numOrString === "string"
|
||||
? stringToNumber(numOrString)
|
||||
: numOrString;
|
||||
return num.toFixed(num % 1 === 0 ? 0 : 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare 2 strings case insensitive
|
||||
* @param {string} string1 The first string for comparison.
|
||||
* @param {string} string2 The second string for comparison.
|
||||
* @returns {boolean} If the strings match.
|
||||
*/
|
||||
export const strCaseCmp = (string1: string, string2: string): boolean => {
|
||||
if (string1 !== undefined && string2 !== undefined) {
|
||||
return string1.toLowerCase() === string2.toLowerCase();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Ref } from "vue";
|
||||
|
||||
/**
|
||||
* Return the browser transiton end name.
|
||||
*
|
||||
* @returns {string} The browser transition end name.
|
||||
*/
|
||||
const transitionEndEventName = (): string => {
|
||||
const el = document.createElement("div"),
|
||||
transitions: Record<string, string> = {
|
||||
transition: "transitionend",
|
||||
OTransition: "otransitionend",
|
||||
MozTransition: "transitionend",
|
||||
WebkitTransition: "webkitTransitionEnd",
|
||||
};
|
||||
|
||||
for (const i in transitions) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(transitions, i) &&
|
||||
el.style[i] !== undefined
|
||||
) {
|
||||
return transitions[i];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for the element to render as Promise
|
||||
*
|
||||
* @param elem The
|
||||
* @returns
|
||||
*/
|
||||
const waitForElementRender = (elem: Ref): Promise<HTMLElement> => {
|
||||
return new Promise((resolve) => {
|
||||
if (document.contains(elem.value)) {
|
||||
return resolve(elem.value as HTMLElement);
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const MutationObserver =
|
||||
window.MutationObserver ||
|
||||
(window as any).WebKitMutationObserver ||
|
||||
(window as any).MozMutationObserver;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
const observer = new MutationObserver(() => {
|
||||
if (document.contains(elem.value)) {
|
||||
resolve(elem.value);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the enter transition on a element.
|
||||
*
|
||||
* @param {Ref} elem The element to run the enter transition.
|
||||
* @param {string} transition The transition name.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const transitionEnter = (elem: Ref, transition: string): void => {
|
||||
waitForElementRender(elem)
|
||||
.then((e: HTMLElement) => {
|
||||
window.setTimeout(() => {
|
||||
e.classList.replace(
|
||||
transition + "-enter-from",
|
||||
transition + "-enter-active"
|
||||
);
|
||||
const transitionName = transitionEndEventName();
|
||||
e.addEventListener(
|
||||
transitionName,
|
||||
() => {
|
||||
e.classList.replace(
|
||||
transition + "-enter-active",
|
||||
transition + "-enter-to"
|
||||
);
|
||||
},
|
||||
false
|
||||
);
|
||||
}, 1);
|
||||
})
|
||||
.catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the exit transition on a element then call a callback.
|
||||
*
|
||||
* @param {Ref} elem The element to run the enter transition.
|
||||
* @param {string} transition The transition name.
|
||||
* @param {TransitionLeaveCallback|null} callback The callback to run after the transition finishes.
|
||||
* @returns {void}
|
||||
*/
|
||||
type TransitionLeaveCallback = () => void;
|
||||
|
||||
export const transitionLeave = (
|
||||
elem: Ref,
|
||||
transition: string,
|
||||
callback: TransitionLeaveCallback | null = null
|
||||
): void => {
|
||||
elem.value.classList.remove(transition + "-enter-to");
|
||||
elem.value.classList.add(transition + "-leave-from");
|
||||
window.setTimeout(() => {
|
||||
elem.value.classList.replace(
|
||||
transition + "-leave-from",
|
||||
transition + "-leave-active"
|
||||
);
|
||||
const transitionName = transitionEndEventName();
|
||||
elem.value.addEventListener(
|
||||
transitionName,
|
||||
() => {
|
||||
elem.value.classList.replace(
|
||||
transition + "-leave-active",
|
||||
transition + "-leave-to"
|
||||
);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}, 1);
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Test if target is a boolean
|
||||
* @param {unknown} target The varible to test
|
||||
* @returns {boolean} If the varible is a boolean type
|
||||
*/
|
||||
export function isBool(target: unknown): boolean {
|
||||
return typeof target === "boolean";
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if target is a number
|
||||
* @param {unknown} target The varible to test
|
||||
* @returns {boolean} If the varible is a number type
|
||||
*/
|
||||
export function isNumber(target: unknown): boolean {
|
||||
return typeof target === "number";
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if target is an object
|
||||
* @param {unknown} target The varible to test
|
||||
* @returns {boolean} If the varible is a object type
|
||||
*/
|
||||
export function isObject(target: unknown): boolean {
|
||||
return typeof target === "object" && target !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if target is a string
|
||||
* @param {unknown} target The varible to test
|
||||
* @returns {boolean} If the varible is a string type
|
||||
*/
|
||||
export function isString(target: unknown): boolean {
|
||||
return typeof target === "string" && target !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to a human readable string.
|
||||
* @param {number} bytes The bytes to convert.
|
||||
* @param {number} decimalPlaces The number of places to force.
|
||||
* @returns {string} The bytes in human readable string.
|
||||
*/
|
||||
export const bytesReadable = (
|
||||
bytes: number,
|
||||
decimalPlaces: number = undefined,
|
||||
): string => {
|
||||
if (Number.isNaN(bytes)) {
|
||||
return "0 Bytes";
|
||||
}
|
||||
|
||||
if (Math.abs(bytes) < 1024) {
|
||||
return bytes + " Bytes";
|
||||
}
|
||||
|
||||
const units = ["KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
let u = -1;
|
||||
const r = 10 ** 1;
|
||||
let tempBytes = bytes;
|
||||
|
||||
while (
|
||||
Math.round(Math.abs(tempBytes) * r) / r >= 1024 &&
|
||||
u < units.length - 1
|
||||
) {
|
||||
tempBytes /= 1024;
|
||||
++u;
|
||||
}
|
||||
|
||||
if (decimalPlaces === undefined) {
|
||||
return tempBytes.toFixed(2).replace(/\.?0+$/, "") + " " + units[u];
|
||||
}
|
||||
|
||||
return tempBytes.toFixed(decimalPlaces) + " " + units[u];
|
||||
};
|
||||
@@ -1,147 +0,0 @@
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
export const urlStripAttributes = (url: string): string => {
|
||||
const urlObject = new URL(url);
|
||||
urlObject.search = "";
|
||||
urlObject.hash = "";
|
||||
return urlObject.toString();
|
||||
};
|
||||
|
||||
export const urlMatches = (
|
||||
fullUrl: string,
|
||||
testPath: string | string[],
|
||||
): boolean | number => {
|
||||
// Remove query string and fragment identifier from both URLs
|
||||
const urlWithoutParams = fullUrl.split(/[?#]/)[0];
|
||||
|
||||
if (Array.isArray(testPath)) {
|
||||
// Iterate over the array of test paths and return the index of the first matching path
|
||||
for (let i = 0; i < testPath.length; i++) {
|
||||
const pathWithoutParams = testPath[i].split(/[?#]/)[0];
|
||||
// Remove trailing slashes from both URLs
|
||||
const trimmedUrl = urlWithoutParams.replace(/\/$/, "");
|
||||
const trimmedPath = pathWithoutParams.replace(/\/$/, "");
|
||||
// Check if both URLs contain a domain and port
|
||||
const hasDomainAndPort =
|
||||
/^https?:\/\/[^/]+\//.test(trimmedUrl) &&
|
||||
/^https?:\/\/[^/]+\//.test(trimmedPath);
|
||||
|
||||
if (hasDomainAndPort) {
|
||||
// Do a full test with both URLs
|
||||
if (trimmedUrl === trimmedPath) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
// Remove the domain and test the paths
|
||||
const urlWithoutDomain = trimmedUrl.replace(
|
||||
/^https?:\/\/[^/]+/,
|
||||
"",
|
||||
);
|
||||
const pathWithoutDomain = trimmedPath.replace(
|
||||
/^https?:\/\/[^/]+/,
|
||||
"",
|
||||
);
|
||||
if (urlWithoutDomain === pathWithoutDomain) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no matching path is found, return false
|
||||
return false;
|
||||
} else {
|
||||
const pathWithoutParams = testPath.split(/[?#]/)[0];
|
||||
// Remove trailing slashes from both URLs
|
||||
const trimmedUrl = urlWithoutParams.replace(/\/$/, "");
|
||||
const trimmedPath = pathWithoutParams.replace(/\/$/, "");
|
||||
// Check if both URLs contain a domain and port
|
||||
const hasDomainAndPort =
|
||||
/^https?:\/\/[^/]+\//.test(trimmedUrl) &&
|
||||
/^https?:\/\/[^/]+\//.test(trimmedPath);
|
||||
|
||||
if (hasDomainAndPort) {
|
||||
// Do a full test with both URLs
|
||||
return trimmedUrl === trimmedPath;
|
||||
} else {
|
||||
// Remove the domain and test the paths
|
||||
const urlWithoutDomain = trimmedUrl.replace(
|
||||
/^https?:\/\/[^/]+/,
|
||||
"",
|
||||
);
|
||||
const pathWithoutDomain = trimmedPath.replace(
|
||||
/^https?:\/\/[^/]+/,
|
||||
"",
|
||||
);
|
||||
return urlWithoutDomain === pathWithoutDomain;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface Params {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const updateRouterParams = (router: Router, params: Params): void => {
|
||||
const query = { ...router.currentRoute.value.query };
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value === "") {
|
||||
if (key in params) {
|
||||
delete query[key];
|
||||
}
|
||||
} else {
|
||||
query[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
router.push({ query });
|
||||
};
|
||||
|
||||
export const getRouterParam = (
|
||||
route: RouteLocationNormalizedLoaded,
|
||||
param: string,
|
||||
defaultValue: string = "",
|
||||
): string => {
|
||||
if (route.query[param] !== undefined) {
|
||||
const val = route.query[param];
|
||||
|
||||
if (Array.isArray(val) == true) {
|
||||
if (val.length > 0) {
|
||||
return val[0];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export const extractFileNameFromUrl = (url: string): string => {
|
||||
const matches = url.match(/\/([^/]+\.[^/]+)$/);
|
||||
if (!matches) {
|
||||
return "";
|
||||
}
|
||||
const fileName = matches[1];
|
||||
return fileName;
|
||||
};
|
||||
|
||||
export const addQueryParam = (
|
||||
url: string,
|
||||
name: string,
|
||||
value: string,
|
||||
): string => {
|
||||
const urlObject = new URL(url);
|
||||
const queryParams = new URLSearchParams(urlObject.search);
|
||||
|
||||
if (queryParams.has(name)) {
|
||||
queryParams.set(name, value);
|
||||
} else {
|
||||
// Add the new query parameter
|
||||
queryParams.append(name, value);
|
||||
}
|
||||
|
||||
urlObject.search = queryParams.toString();
|
||||
return urlObject.toString();
|
||||
};
|
||||
@@ -1,164 +0,0 @@
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { extractFileNameFromUrl } from "./url";
|
||||
|
||||
/**
|
||||
* Tests if an object or string is empty.
|
||||
* @param {unknown} value The object or string.
|
||||
* @returns {boolean} If the object or string is empty.
|
||||
*/
|
||||
export const isEmpty = (value: unknown): boolean => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length === 0;
|
||||
} else if (
|
||||
value instanceof File ||
|
||||
value instanceof Blob ||
|
||||
value instanceof Map ||
|
||||
value instanceof Set
|
||||
) {
|
||||
return value.size === 0;
|
||||
} else if (value instanceof FormData) {
|
||||
return [...value.entries()].length === 0;
|
||||
} else if (typeof value === "object") {
|
||||
return !value || Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the file extension
|
||||
* @param {string} fileName The filename with extension.
|
||||
* @returns {string} The file extension.
|
||||
*/
|
||||
export const getFileExtension = (fileName: string): string => {
|
||||
if (fileName.includes(".")) {
|
||||
return fileName.split(".").pop();
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a url to a file type icon based on file name.
|
||||
* @param {string} fileName The filename with extension.
|
||||
* @returns {string} The url to the file type icon.
|
||||
*/
|
||||
export const getFileIconImagePath = (fileName: string): string => {
|
||||
const ext = getFileExtension(fileName);
|
||||
if (ext.length > 0) {
|
||||
return `/assets/fileicons/${ext}.webp`;
|
||||
}
|
||||
|
||||
return "/assets/fileicons/unknown.webp";
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a url to a file preview icon based on file url.
|
||||
* @param {string} url The url of the file.
|
||||
* @returns {string} The url to the file preview icon.
|
||||
*/
|
||||
export const getFilePreview = (url: string): string => {
|
||||
const ext = getFileExtension(extractFileNameFromUrl(url));
|
||||
if (ext.length > 0) {
|
||||
if (/(gif|jpe?g|png)/i.test(ext)) {
|
||||
return `${url}?size=thumb`;
|
||||
}
|
||||
|
||||
return `/assets/fileicons/${ext}.webp`;
|
||||
}
|
||||
|
||||
return "/assets/fileicons/unknown.webp";
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamps a number between 2 numbers.
|
||||
* @param {number} n The number to clamp.
|
||||
* @param {number} min The minimum allowable number.
|
||||
* @param {number} max The maximum allowable number.
|
||||
* @returns {number} The clamped number.
|
||||
*/
|
||||
export const clamp = (n: number, min: number, max: number): number => {
|
||||
if (n < min) return min;
|
||||
if (n > max) return max;
|
||||
return n;
|
||||
};
|
||||
|
||||
type RandomIDVerifyCallback = (id: string) => boolean;
|
||||
|
||||
/**
|
||||
* Generate a random ID.
|
||||
* @param {string} prefix Any prefix to add to the ID.
|
||||
* @param {number} length The length of the ID string (default = 6).
|
||||
* @param {RandomIDVerifyCallback|null} callback Callback that if returns true generates a ID string.
|
||||
* @returns {string} A random string.
|
||||
*/
|
||||
export const generateRandomId = (
|
||||
prefix: string = "",
|
||||
length: number = 6,
|
||||
callback: RandomIDVerifyCallback | null = null,
|
||||
): string => {
|
||||
let randomId = "";
|
||||
const letters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
|
||||
do {
|
||||
randomId = prefix;
|
||||
for (let i = 0; i < length; i++) {
|
||||
randomId += letters.charAt(
|
||||
Math.floor(Math.random() * letters.length),
|
||||
);
|
||||
}
|
||||
} while (callback != null ? callback(randomId) : false);
|
||||
|
||||
return randomId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random element ID.
|
||||
* @param {string} prefix Any prefix to add to the ID.
|
||||
* @param {number} length The length of the ID string (default = 6).
|
||||
* @returns {string} A random string non-existent in the document.
|
||||
*/
|
||||
export const generateRandomElementId = (
|
||||
prefix: string = "",
|
||||
length: number = 6,
|
||||
): string => {
|
||||
return generateRandomId(prefix, length, (s) => {
|
||||
return document.getElementById(s) != null;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return if the current user has a permission.
|
||||
* @param {string} permission The permission to check.
|
||||
* @returns {boolean} If the user has the permission.
|
||||
*/
|
||||
export const userHasPermission = (permission: string): boolean => {
|
||||
const userStore = useUserStore();
|
||||
return userStore.permissions && userStore.permissions.includes(permission);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert File Name to Title
|
||||
* @param {string} fileName The filename with extension.
|
||||
* @returns {string} The title.
|
||||
*/
|
||||
export const convertFileNameToTitle = (fileName: string): string => {
|
||||
// Remove file extension
|
||||
fileName = fileName.replace(/\.[^/.]+$/, "");
|
||||
|
||||
// Replace underscores with space
|
||||
fileName = fileName.replace(/_/g, " ");
|
||||
|
||||
// Replace dashes that are not surrounded by spaces with space
|
||||
fileName = fileName.replace(/(?<! )-(?! )/g, " ");
|
||||
|
||||
// Remove double spaces
|
||||
fileName = fileName.replace(/\s{2,}/g, " ");
|
||||
|
||||
// Capitalize the first letter and convert to lowercase
|
||||
fileName =
|
||||
fileName.charAt(0).toUpperCase() + fileName.slice(1).toLowerCase();
|
||||
|
||||
return fileName;
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Test if target is a UUID
|
||||
*
|
||||
* @param {string} uuid The variable to test
|
||||
* @returns {boolean} If the varible is a UUID
|
||||
*/
|
||||
export const isUUID = (uuid: string): boolean => {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
||||
uuid
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random UUID.
|
||||
*
|
||||
* @returns {string} A random UUID.
|
||||
*/
|
||||
export const randomUUID = (): string => {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
@@ -1,978 +0,0 @@
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
import { SMDate } from "./datetime";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
|
||||
export interface ValidationObject {
|
||||
validate: (value: unknown) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
invalidMessages: Array<string>;
|
||||
}
|
||||
|
||||
export const defaultValidationResult: ValidationResult = {
|
||||
valid: true,
|
||||
invalidMessages: [],
|
||||
};
|
||||
|
||||
export const createValidationResult = (
|
||||
valid: boolean,
|
||||
message: string | Array<string> = ""
|
||||
) => {
|
||||
if (typeof message == "string") {
|
||||
message = [message];
|
||||
}
|
||||
|
||||
return {
|
||||
valid: valid,
|
||||
invalidMessages: message,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Validation Min
|
||||
*/
|
||||
const VALIDATION_MIN_TYPE = ["String", "Number"];
|
||||
type ValidationMinType = (typeof VALIDATION_MIN_TYPE)[number];
|
||||
|
||||
interface ValidationMinOptions {
|
||||
min: number;
|
||||
type?: ValidationMinType;
|
||||
invalidMessage?: string | ((options: ValidationMinOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationMinObject extends ValidationMinOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationMinOptions: ValidationMinOptions = {
|
||||
min: 1,
|
||||
type: "String",
|
||||
invalidMessage: (options: ValidationMinOptions) => {
|
||||
return options.type == "String"
|
||||
? `Required to be at least ${options.min} characters.`
|
||||
: `Required to be at least ${options.min}.`;
|
||||
},
|
||||
};
|
||||
|
||||
export function Min(
|
||||
minOrOptions: number | ValidationMinOptions,
|
||||
options?: ValidationMinOptions
|
||||
);
|
||||
export function Min(options: ValidationMinOptions): ValidationMinObject;
|
||||
|
||||
/**
|
||||
* Validate field length or number is at minimum or higher/larger
|
||||
*
|
||||
* @param minOrOptions minimum number or options data
|
||||
* @param options options data
|
||||
* @returns ValidationMinObject
|
||||
*/
|
||||
export function Min(
|
||||
minOrOptions: number | ValidationMinOptions,
|
||||
options?: ValidationMinOptions
|
||||
): ValidationMinObject {
|
||||
if (typeof minOrOptions === "number") {
|
||||
options = { ...defaultValidationMinOptions, ...(options || {}) };
|
||||
options.min = minOrOptions;
|
||||
} else {
|
||||
options = { ...defaultValidationMinOptions, ...(minOrOptions || {}) };
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
this.type == "String"
|
||||
? value.toString().length >= this.min
|
||||
: parseInt(value) >= this.min,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation Max
|
||||
*/
|
||||
const VALIDATION_MAX_TYPE = ["String", "Number"];
|
||||
type ValidationMaxType = (typeof VALIDATION_MAX_TYPE)[number];
|
||||
|
||||
interface ValidationMaxOptions {
|
||||
max: number;
|
||||
type?: ValidationMaxType;
|
||||
invalidMessage?: string | ((options: ValidationMaxOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationMaxObject extends ValidationMaxOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationMaxOptions: ValidationMaxOptions = {
|
||||
max: 1,
|
||||
type: "String",
|
||||
invalidMessage: (options: ValidationMaxOptions) => {
|
||||
return options.type == "String"
|
||||
? `Required to be less than ${options.max + 1} characters.`
|
||||
: `Required to be less than ${options.max + 1}.`;
|
||||
},
|
||||
};
|
||||
|
||||
export function Max(
|
||||
maxOrOptions: number | ValidationMaxOptions,
|
||||
options?: ValidationMaxOptions
|
||||
): ValidationMaxObject;
|
||||
export function Max(options: ValidationMaxOptions): ValidationMaxObject;
|
||||
|
||||
/**
|
||||
* Validate field length or number is at maximum or smaller
|
||||
*
|
||||
* @param maxOrOptions maximum number or options data
|
||||
* @param options options data
|
||||
* @returns ValidationMaxObject
|
||||
*/
|
||||
export function Max(
|
||||
maxOrOptions: number | ValidationMaxOptions,
|
||||
options?: ValidationMaxOptions
|
||||
): ValidationMaxObject {
|
||||
if (typeof maxOrOptions === "number") {
|
||||
options = { ...defaultValidationMaxOptions, ...(options || {}) };
|
||||
options.max = maxOrOptions;
|
||||
} else {
|
||||
options = { ...defaultValidationMaxOptions, ...(maxOrOptions || {}) };
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
this.type == "String"
|
||||
? value.toString().length <= this.max
|
||||
: parseInt(value) <= this.max,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation Length
|
||||
*/
|
||||
interface ValidationLengthOptions {
|
||||
length: number;
|
||||
invalidMessage?: string | ((options: ValidationLengthOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationLengthObject extends ValidationLengthOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationLengthOptions: ValidationLengthOptions = {
|
||||
length: 1,
|
||||
invalidMessage: (options: ValidationLengthOptions) => {
|
||||
return `Required to be ${options.length} characters.`;
|
||||
},
|
||||
};
|
||||
|
||||
export function Length(
|
||||
lengthOrOptions: number | ValidationLengthOptions,
|
||||
options?: ValidationLengthOptions
|
||||
): ValidationLengthObject;
|
||||
export function Length(
|
||||
options: ValidationLengthOptions
|
||||
): ValidationLengthObject;
|
||||
|
||||
/**
|
||||
* Validate field length
|
||||
*
|
||||
* @param lengthOrOptions string length or options data
|
||||
* @param options options data
|
||||
* @returns ValidationLengthObject
|
||||
*/
|
||||
export function Length(
|
||||
lengthOrOptions: number | ValidationLengthOptions,
|
||||
options?: ValidationLengthOptions
|
||||
): ValidationLengthObject {
|
||||
if (typeof lengthOrOptions === "number") {
|
||||
options = { ...defaultValidationLengthOptions, ...(options || {}) };
|
||||
options.length = lengthOrOptions;
|
||||
} else {
|
||||
options = {
|
||||
...defaultValidationLengthOptions,
|
||||
...(lengthOrOptions || {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: value.toString().length == this.length,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* PASSWORD
|
||||
*/
|
||||
interface ValidationPasswordOptions {
|
||||
invalidMessage?: string | ((options: ValidationPasswordOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationPasswordObject extends ValidationPasswordOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationPasswordOptions: ValidationPasswordOptions = {
|
||||
invalidMessage:
|
||||
"Your password needs to have at least a letter, a number and a special character.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid password format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationPasswordObject
|
||||
*/
|
||||
export function Password(
|
||||
options?: ValidationPasswordOptions
|
||||
): ValidationPasswordObject {
|
||||
options = { ...defaultValidationPasswordOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: /(?=.*[A-Za-z])(?=.*\d)(?=.*[.@$!%*#?&])[A-Za-z\d.@$!%*#?&]{1,}$/.test(
|
||||
value
|
||||
),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* EMAIL
|
||||
*/
|
||||
interface ValidationEmailOptions {
|
||||
invalidMessage?: string | ((options: ValidationEmailOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationEmailObject extends ValidationEmailOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationEmailOptions: ValidationEmailOptions = {
|
||||
invalidMessage: "Your email is not in a supported format.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Email format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationEmailObject
|
||||
*/
|
||||
export function Email(options?: ValidationEmailOptions): ValidationEmailObject {
|
||||
options = { ...defaultValidationEmailOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
value.length == 0 ||
|
||||
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* PHONE
|
||||
*/
|
||||
interface ValidationPhoneOptions {
|
||||
invalidMessage?: string | ((options: ValidationPhoneOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationPhoneObject extends ValidationPhoneOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationPhoneOptions: ValidationPhoneOptions = {
|
||||
invalidMessage: "Your Phone number is not in a supported format.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Phone format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationPhoneObject
|
||||
*/
|
||||
export function Phone(options?: ValidationPhoneOptions): ValidationPhoneObject {
|
||||
options = { ...defaultValidationPhoneOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
value.length == 0 ||
|
||||
/^(\+|00)?[0-9][0-9 \-().]{7,32}$/.test(value),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* NUMBER
|
||||
*/
|
||||
interface ValidationNumberOptions {
|
||||
invalidMessage?: string | ((options: ValidationNumberOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationNumberObject extends ValidationNumberOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationNumberOptions: ValidationNumberOptions = {
|
||||
invalidMessage: "Must be a number.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Whole number format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationNumberObject
|
||||
*/
|
||||
export function Number(
|
||||
options?: ValidationNumberOptions
|
||||
): ValidationNumberObject {
|
||||
options = { ...defaultValidationNumberOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: value.length == 0 || /^0?\d+$/.test(value),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DATE
|
||||
*/
|
||||
interface ValidationDateOptions {
|
||||
before?: string | ((value: string) => string);
|
||||
after?: string | ((value: string) => string);
|
||||
invalidMessage?: string | ((options: ValidationDateOptions) => string);
|
||||
invalidBeforeMessage?:
|
||||
| string
|
||||
| ((options: ValidationDateOptions) => string);
|
||||
invalidAfterMessage?: string | ((options: ValidationDateOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationDateObject extends ValidationDateOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationDateOptions: ValidationDateOptions = {
|
||||
before: "",
|
||||
after: "",
|
||||
invalidMessage: "Must be a valid date.",
|
||||
invalidBeforeMessage: (options: ValidationDateOptions) => {
|
||||
return `Must be a date before ${options.before}.`;
|
||||
},
|
||||
invalidAfterMessage: (options: ValidationDateOptions) => {
|
||||
return `Must be a date after ${options.after}.`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Date format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationDateObject
|
||||
*/
|
||||
export function Date(options?: ValidationDateOptions): ValidationDateObject {
|
||||
options = { ...defaultValidationDateOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
const parsedDate = new SMDate(value);
|
||||
|
||||
if (parsedDate.isValid() == true) {
|
||||
const beforeDate = new SMDate(
|
||||
typeof (options["before"] = options?.before || "") ===
|
||||
"function"
|
||||
? options.before(value)
|
||||
: options.before
|
||||
);
|
||||
const afterDate = new SMDate(
|
||||
typeof (options["after"] = options?.after || "") ===
|
||||
"function"
|
||||
? options.after(value)
|
||||
: options.after
|
||||
);
|
||||
if (
|
||||
beforeDate.isValid() == true &&
|
||||
parsedDate.isBefore(beforeDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidBeforeMessage";
|
||||
}
|
||||
if (
|
||||
afterDate.isValid() == true &&
|
||||
parsedDate.isAfter(afterDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidAfterMessage";
|
||||
}
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
valid: valid,
|
||||
invalidMessages: [
|
||||
typeof this[invalidMessageType] === "string"
|
||||
? this[invalidMessageType]
|
||||
: this[invalidMessageType](this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TIME
|
||||
*/
|
||||
interface ValidationTimeOptions {
|
||||
before?: string | ((value: string) => string);
|
||||
after?: string | ((value: string) => string);
|
||||
invalidMessage?: string | ((options: ValidationTimeOptions) => string);
|
||||
invalidBeforeMessage?:
|
||||
| string
|
||||
| ((options: ValidationTimeOptions) => string);
|
||||
invalidAfterMessage?: string | ((options: ValidationTimeOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationTimeObject extends ValidationTimeOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationTimeOptions: ValidationTimeOptions = {
|
||||
before: "",
|
||||
after: "",
|
||||
invalidMessage: "Must be a valid time.",
|
||||
invalidBeforeMessage: (options: ValidationTimeOptions) => {
|
||||
return `Must be a time before ${options.before}.`;
|
||||
},
|
||||
invalidAfterMessage: (options: ValidationTimeOptions) => {
|
||||
return `Must be a time after ${options.after}.`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Time format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationTimeObject
|
||||
*/
|
||||
export function Time(options?: ValidationTimeOptions): ValidationTimeObject {
|
||||
options = { ...defaultValidationTimeOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
const parsedTime = new SMDate(value);
|
||||
if (parsedTime.isValid() == true) {
|
||||
const beforeTime = new SMDate(
|
||||
typeof (options["before"] = options?.before || "") ===
|
||||
"function"
|
||||
? options.before(value)
|
||||
: options.before
|
||||
);
|
||||
const afterTime = new SMDate(
|
||||
typeof (options["after"] = options?.after || "") ===
|
||||
"function"
|
||||
? options.after(value)
|
||||
: options.after
|
||||
);
|
||||
|
||||
if (
|
||||
beforeTime.isValid() == true &&
|
||||
parsedTime.isBefore(beforeTime) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidBeforeMessage";
|
||||
}
|
||||
if (
|
||||
afterTime.isValid() == true &&
|
||||
parsedTime.isAfter(afterTime) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidAfterMessage";
|
||||
}
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
valid: valid,
|
||||
invalidMessages: [
|
||||
typeof this[invalidMessageType] === "string"
|
||||
? this[invalidMessageType]
|
||||
: this[invalidMessageType](this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DATETIME
|
||||
*/
|
||||
interface ValidationDateTimeOptions {
|
||||
before?: string | ((value: string) => string);
|
||||
after?: string | ((value: string) => string);
|
||||
invalidMessage?: string | ((options: ValidationDateTimeOptions) => string);
|
||||
invalidBeforeMessage?:
|
||||
| string
|
||||
| ((options: ValidationDateTimeOptions) => string);
|
||||
invalidAfterMessage?:
|
||||
| string
|
||||
| ((options: ValidationDateTimeOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationDateTimeObject extends ValidationDateTimeOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationDateTimeOptions: ValidationDateTimeOptions = {
|
||||
before: "",
|
||||
after: "",
|
||||
invalidMessage: "Must be a valid date and time.",
|
||||
invalidBeforeMessage: (options: ValidationDateTimeOptions) => {
|
||||
return `Must be a date/time before ${options.before}.`;
|
||||
},
|
||||
invalidAfterMessage: (options: ValidationDateTimeOptions) => {
|
||||
return `Must be a date/time after ${options.after}.`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Date format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationDateObject
|
||||
*/
|
||||
export function DateTime(
|
||||
options?: ValidationDateTimeOptions
|
||||
): ValidationDateTimeObject {
|
||||
options = { ...defaultValidationDateTimeOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
let valid = true;
|
||||
let invalidMessageType = "invalidMessage";
|
||||
|
||||
const parsedDate = new SMDate(value);
|
||||
|
||||
if (parsedDate.isValid() == true) {
|
||||
const beforeDate = new SMDate(
|
||||
typeof (options["before"] = options?.before || "") ===
|
||||
"function"
|
||||
? options.before(value)
|
||||
: options.before
|
||||
);
|
||||
const afterDate = new SMDate(
|
||||
typeof (options["after"] = options?.after || "") ===
|
||||
"function"
|
||||
? options.after(value)
|
||||
: options.after
|
||||
);
|
||||
if (
|
||||
beforeDate.isValid() == true &&
|
||||
parsedDate.isBefore(beforeDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidBeforeMessage";
|
||||
}
|
||||
if (
|
||||
afterDate.isValid() == true &&
|
||||
parsedDate.isAfter(afterDate) == false
|
||||
) {
|
||||
valid = false;
|
||||
invalidMessageType = "invalidAfterMessage";
|
||||
}
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
valid: valid,
|
||||
invalidMessages: [
|
||||
typeof this[invalidMessageType] === "string"
|
||||
? this[invalidMessageType]
|
||||
: this[invalidMessageType](this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* CUSTOM
|
||||
*/
|
||||
type ValidationCustomCallback = (value: string) => Promise<boolean | string>;
|
||||
|
||||
interface ValidationCustomOptions {
|
||||
callback: ValidationCustomCallback;
|
||||
invalidMessage?: string | ((options: ValidationCustomOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationCustomObject extends ValidationCustomOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationCustomOptions: ValidationCustomOptions = {
|
||||
callback: async () => {
|
||||
return true;
|
||||
},
|
||||
invalidMessage: "This field is invalid.",
|
||||
};
|
||||
|
||||
export function Custom(
|
||||
callbackOrOptions: ValidationCustomCallback | ValidationCustomOptions,
|
||||
options?: ValidationCustomOptions
|
||||
);
|
||||
export function Custom(
|
||||
options: ValidationCustomOptions
|
||||
): ValidationCustomObject;
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Custom format
|
||||
*
|
||||
* @param callbackOrOptions
|
||||
* @param options options data
|
||||
* @returns ValidationCustomObject
|
||||
*/
|
||||
export function Custom(
|
||||
callbackOrOptions: ValidationCustomCallback | ValidationCustomOptions,
|
||||
options?: ValidationCustomOptions
|
||||
): ValidationCustomObject {
|
||||
if (typeof callbackOrOptions === "function") {
|
||||
options = { ...defaultValidationCustomOptions, ...(options || {}) };
|
||||
options.callback = callbackOrOptions;
|
||||
} else {
|
||||
options = {
|
||||
...defaultValidationCustomOptions,
|
||||
...(callbackOrOptions || {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: async function (value: string): Promise<ValidationResult> {
|
||||
const validateResult = {
|
||||
valid: true,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
};
|
||||
|
||||
const callbackResult =
|
||||
typeof this.callback === "function"
|
||||
? await this.callback(value)
|
||||
: true;
|
||||
|
||||
if (typeof callbackResult === "string") {
|
||||
if (callbackResult.length > 0) {
|
||||
validateResult.valid = false;
|
||||
validateResult.invalidMessages = [callbackResult];
|
||||
}
|
||||
} else if (callbackResult !== true) {
|
||||
validateResult.valid = false;
|
||||
}
|
||||
|
||||
return validateResult;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* And
|
||||
*
|
||||
* @param list
|
||||
*/
|
||||
export const And = (list: Array<ValidationObject>) => {
|
||||
return {
|
||||
list: list,
|
||||
validate: async function (value: string) {
|
||||
const validationResult: ValidationResult = {
|
||||
valid: true,
|
||||
invalidMessages: [],
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
this.list.map(async (item: ValidationObject) => {
|
||||
const validationItemResult = await item.validate(value);
|
||||
if (validationItemResult.valid == false) {
|
||||
validationResult.valid = false;
|
||||
validationResult.invalidMessages =
|
||||
validationResult.invalidMessages.concat(
|
||||
validationItemResult.invalidMessages
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validationResult;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Required
|
||||
*/
|
||||
interface ValidationRequiredOptions {
|
||||
invalidMessage?: string | ((options: ValidationRequiredOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationRequiredObject extends ValidationRequiredOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationRequiredOptions: ValidationRequiredOptions = {
|
||||
invalidMessage: "This field is required.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field contains value
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationRequiredObject
|
||||
*/
|
||||
export function Required(
|
||||
options?: ValidationRequiredOptions
|
||||
): ValidationRequiredObject {
|
||||
options = { ...defaultValidationRequiredOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: unknown): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: !isEmpty(value),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Required If
|
||||
*/
|
||||
type ValidationRequiredIfCheck = boolean | Array<boolean>;
|
||||
|
||||
interface ValidationRequiredIfOptions {
|
||||
check: ValidationRequiredIfCheck;
|
||||
invalidMessage?:
|
||||
| string
|
||||
| ((options: ValidationRequiredIfOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationRequiredIfObject extends ValidationRequiredIfOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationRequiredIfOptions: ValidationRequiredIfOptions = {
|
||||
check: true,
|
||||
invalidMessage: "This field is required.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field contains value
|
||||
*
|
||||
* @param checkOrOptions
|
||||
* @param options options data
|
||||
* @returns ValidationRequiredIfObject
|
||||
*/
|
||||
export function RequiredIf(
|
||||
checkOrOptions: boolean | Array<boolean> | ValidationRequiredIfOptions,
|
||||
options?: ValidationRequiredIfOptions
|
||||
): ValidationRequiredIfObject {
|
||||
if (
|
||||
typeof checkOrOptions === "boolean" ||
|
||||
Array.isArray(checkOrOptions) === true
|
||||
) {
|
||||
options = { ...defaultValidationRequiredIfOptions, ...(options || {}) };
|
||||
options.check = checkOrOptions;
|
||||
} else {
|
||||
options = {
|
||||
...defaultValidationRequiredIfOptions,
|
||||
...(checkOrOptions || {}),
|
||||
};
|
||||
}
|
||||
|
||||
options = { ...defaultValidationRequiredIfOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: unknown): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: Array.isArray(value)
|
||||
? value.every((item) => !!item)
|
||||
: value == true,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Url
|
||||
*/
|
||||
interface ValidationUrlOptions {
|
||||
invalidMessage?: string | ((options: ValidationUrlOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationUrlObject extends ValidationUrlOptions {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationUrlOptions: ValidationUrlOptions = {
|
||||
invalidMessage: "Not a supported Url format.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate field is in a valid Email format
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationEmailObject
|
||||
*/
|
||||
export function Url(options?: ValidationUrlOptions): ValidationUrlObject {
|
||||
options = { ...defaultValidationUrlOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid:
|
||||
value.length > 0
|
||||
? /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*(:\d+)?([/?#][^\s]*)?$/.test(
|
||||
value
|
||||
)
|
||||
: true,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FileSize
|
||||
*/
|
||||
interface ValidationFileSizeOptions {
|
||||
size: number;
|
||||
invalidMessage?: string | ((options: ValidationFileSizeOptions) => string);
|
||||
}
|
||||
|
||||
interface ValidationFileSizeObject extends ValidationFileSizeOptions {
|
||||
validate: (value: File) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
const defaultValidationFileSizeOptions: ValidationFileSizeOptions = {
|
||||
size: 1024 * 1024 * 1024, // 1 Mb
|
||||
invalidMessage: (options) => {
|
||||
return `The file size must be less than ${bytesReadable(options.size)}`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate file is equal or less than size.
|
||||
*
|
||||
* @param options options data
|
||||
* @returns ValidationEmailObject
|
||||
*/
|
||||
export function FileSize(
|
||||
options?: ValidationFileSizeOptions
|
||||
): ValidationFileSizeObject {
|
||||
options = { ...defaultValidationFileSizeOptions, ...(options || {}) };
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: File): Promise<ValidationResult> {
|
||||
const isValid =
|
||||
value instanceof File ? value.size < options.size : true;
|
||||
|
||||
return Promise.resolve({
|
||||
valid: isValid,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
: this.invalidMessage(this),
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +0,0 @@
|
||||
import Router from "@/router";
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import { createApp } from "vue";
|
||||
import App from "./views/App.vue";
|
||||
import "uno.css";
|
||||
import "../css/app.scss";
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createApp(App).use(pinia).use(Router).mount("#app");
|
||||
238
resources/js/media-picker.js
Normal file
238
resources/js/media-picker.js
Normal file
@@ -0,0 +1,238 @@
|
||||
const SMMediaPicker = {
|
||||
upload: (files) => {
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
return SM.mimeMatches(file.type, Alpine.store('media').require_mime_type);
|
||||
});
|
||||
|
||||
const titles = Array.from(validFiles).map((file) => SM.toTitleCase(file.name));
|
||||
|
||||
SM.upload(validFiles, (response) => {
|
||||
SMMediaPicker.open(
|
||||
Alpine.store('media').selected,
|
||||
{
|
||||
require_mime_type: Alpine.store('media').require_mime_type,
|
||||
allow_multiple: Alpine.store('media').allow_multiple,
|
||||
allow_uploads: Alpine.store('media').allow_uploads
|
||||
},
|
||||
Alpine.store('media').callback
|
||||
);
|
||||
}, titles);
|
||||
},
|
||||
|
||||
gotoLink: (url) => {
|
||||
if(url !== null) {
|
||||
const page = new URL(url).searchParams.get('page');
|
||||
SMMediaPicker.query(page, document.querySelector('input[name="search"]').value);
|
||||
}
|
||||
},
|
||||
|
||||
updateSelection: (name) => {
|
||||
if(Alpine.store('media').selected.some(i => i === name)) {
|
||||
Alpine.store('media').selected = Alpine.store('media').selected.filter(i => i !== name);
|
||||
} else {
|
||||
if(!Alpine.store('media').allow_multiple) {
|
||||
Alpine.store('media').selected = [name];
|
||||
} else {
|
||||
Alpine.store('media').selected.push(name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
search: () => {
|
||||
SMMediaPicker.query(null, document.querySelector('input[name="search"]').value);
|
||||
},
|
||||
|
||||
query: (page, search) => {
|
||||
let params = {
|
||||
mime_type: Alpine.store('media').require_mime_type,
|
||||
per_page: Alpine.store('media').per_page,
|
||||
search: search,
|
||||
'selected[]': Alpine.store('media').selected
|
||||
};
|
||||
|
||||
if(page !== null) {
|
||||
params.page = page;
|
||||
}
|
||||
|
||||
axios.get('/media', {
|
||||
params: params
|
||||
})
|
||||
.then(response => {
|
||||
response.data.links[0].label = '<i class="fa-solid fa-angle-left"></i>';
|
||||
response.data.links[response.data.links.length - 1].label = '<i class="fa-solid fa-angle-right"></i>';
|
||||
|
||||
response.data.data.forEach((file) => {
|
||||
file.extension = file.name.split('.').pop();
|
||||
});
|
||||
|
||||
Alpine.store('media').current_page = response.data.current_page;
|
||||
Alpine.store('media').per_page = response.data.per_page;
|
||||
Alpine.store('media').to = response.data.to;
|
||||
Alpine.store('media').total = response.data.total;
|
||||
Alpine.store('media').items = response.data.data;
|
||||
|
||||
Alpine.store('media').pagination = [];
|
||||
Alpine.nextTick(() => {
|
||||
Alpine.store('media').pagination = response.data.links;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
|
||||
html: `
|
||||
<div class="flex flex-col h-full w-full" x-data="{tab: 'browser', showFileDrop: false}">
|
||||
<ul class="flex -mb-[1px] z-10">
|
||||
<li x-show="$store.media.allow_uploads" class="cursor-pointer border px-3 py-2 rounded-t-lg hover:border-t-gray-300 hover:border-x-gray-300" :class="{ 'border-gray-300': tab === 'upload', 'border-b-white': tab === 'upload', 'border-transparent': tab !== 'upload' }" x-on:click.prevent="tab='upload'">Upload</li>
|
||||
<li class="cursor-pointer border px-3 py-2 rounded-t-lg hover:border-t-gray-300 hover:border-x-gray-300" :class="{ 'border-gray-300': tab === 'browser', 'border-b-white': tab === 'browser', 'border-transparent': tab !== 'browser' }" x-on:click.prevent="tab='browser'">Browser</li>
|
||||
</ul>
|
||||
<div
|
||||
class="flex-1 border border-gray-300"
|
||||
x-on:dragenter.prevent="$store.media.allow_uploads ? showFileDrop = true : showFileDrop = false"
|
||||
x-on:dragover.prevent="$store.media.allow_uploads ? showFileDrop = true : showFileDrop = false">
|
||||
<div
|
||||
id="content-upload"
|
||||
class="w-full h-full flex flex-col px-4 py-8 justify-center items-center"
|
||||
x-show="tab === 'upload'">
|
||||
<h3 class="text-2xl font-bold mb-2">Drop files to upload</h3>
|
||||
<p>or</p>
|
||||
<label class="inline-block my-2 bg-white border border-gray-300 hover:bg-gray-300 justify-center rounded-md text-gray-700 px-8 py-1.5 text-sm font-semibold leading-6 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition" for="media_upload">Select files</label>
|
||||
<input class="hidden" id="media_upload" name="media_upload" multiple type="file" x-on:change="SMMediaPicker.upload(event.target.files)" x-bind:accept="$store.media.require_mime_type" />
|
||||
<p class="text-xs">Maximum upload size: ${SM.bytesToString(SM.maxUploadSize())}</p>
|
||||
</div>
|
||||
<div id="content-browser" class="flex flex-col h-full w-full p-4" x-show="tab === 'browser'">
|
||||
<form x-on:submit.prevent="SMMediaPicker.search()">
|
||||
<div class="flex mb-2">
|
||||
<input class="bg-white flex-grow px-2.5 py-1 text-xs text-gray-900 bg-transparent rounded-l-lg border appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer border-gray-300 focus:ring-indigo-300" autocomplete="off" placeholder="Search" type="text" name="search" />
|
||||
<button class="hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color rounded-l-none px-4 justify-center rounded-md text-white py-1.5 text-xs font-semibold leading-6 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition"><i class="fa-solid fa-magnifying-glass"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul class="flex p-2 gap-4 overflow-auto justify-center flex-row flex-wrap">
|
||||
<template x-for="item in $store.media.items" :key="item.name">
|
||||
<li
|
||||
class="cursor-pointer flex text-center p-1 flex-items-center flex-col h-40 w-56 border-2 rounded relative"
|
||||
:class="{'border-primary-color': $store.media.selected.some(i => i === item.name), 'border-white': !$store.media.selected.some(i => i === item.name)}"
|
||||
x-on:click="SMMediaPicker.updateSelection(item.name)"
|
||||
>
|
||||
<div x-show="$store.media.selected.some(i => i === item.name)" class="absolute -top-1.5 -right-2 w-6 h-6 bg-primary-color text-white flex items-center justify-center text-lg border border-white rounded"><i class="fa-solid fa-check"></i></div>
|
||||
<div class="flex-grow flex items-center justify-center pointer-events-none select-none">
|
||||
<img x-bind:src="item.thumbnail" class="rounded max-h-32" />
|
||||
</div>
|
||||
<div class="text-xs whitespace-nowrap overflow-hidden text-ellipsis" x-text="item.name" x-bind:title="item.name"></div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<div class="flex flex-1 items-end">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<p x-show="$store.media.total > 0" class="text-xs" x-text="'Showing ' + ((($store.media.current_page - 1) * $store.media.per_page) + 1) + ' to ' + ($store.media.current_page * $store.media.per_page > $store.media.total ? $store.media.total : $store.media.current_page * $store.media.per_page) + ' of ' + ($store.media.total) + ' results'"></p>
|
||||
<p x-show="$store.media.total === 0" class="text-xs">No items found</p>
|
||||
<ul class="flex border rounded-lg text-sm">
|
||||
<template x-for="link in $store.media.pagination">
|
||||
<li
|
||||
class="px-3 py-1.5 w-9 border-r last:border-r-0 text-center select-none"
|
||||
:class="{
|
||||
'bg-gray-100': link.url === null,
|
||||
'text-gray-400': link.url === null,
|
||||
'text-primary-color': link.url !== null && link.label == $store.media.current_page,
|
||||
'bg-sky-100': link.url !== null && link.label == $store.media.current_page,
|
||||
'cursor-pointer': link.url !== null,
|
||||
'hover:text-primary-color': link.url !== null,
|
||||
'hover:bg-sky-100': link.url !== null
|
||||
}"
|
||||
x-html="link.label"
|
||||
x-on:click="SMMediaPicker.gotoLink(link.url)"></li>
|
||||
</div>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
x-show="showFileDrop"
|
||||
class="fixed flex top-0 left-0 w-full h-full z-10 bg-sky-800 bg-opacity-95 text-white items-center p-4"
|
||||
x-on:dragenter.prevent="showFileDrop = true"
|
||||
x-on:dragover.prevent="showFileDrop = true"
|
||||
x-on:drop.prevent="SMMediaPicker.upload($event.dataTransfer.files); showFileDrop = false;"
|
||||
x-on:dragleave.prevent="showFileDrop = false">
|
||||
<h2
|
||||
class="pointer-events-none flex w-full h-full justify-center items-center text-lg font-bold border-dashed border">
|
||||
Drop files to upload
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
onOpen: () => {
|
||||
SMMediaPicker.query(null, '');
|
||||
},
|
||||
|
||||
preClose: () => {
|
||||
/* empty */
|
||||
},
|
||||
|
||||
open: (selected, options = {}, callback = null) => {
|
||||
if(!options.hasOwnProperty('require_mime_type')) options.require_mime_type = '*';
|
||||
if(!options.hasOwnProperty('allow_multiple')) options.allow_multiple = false;
|
||||
if(!options.hasOwnProperty('allow_uploads')) options.allow_uploads = false;
|
||||
|
||||
if(selected === null || selected === '') selected = [];
|
||||
if(!Array.isArray(selected)) selected = [selected];
|
||||
Alpine.store('media').selected = selected;
|
||||
|
||||
Alpine.store('media').require_mime_type = options.require_mime_type;
|
||||
Alpine.store('media').allow_multiple = options.allow_multiple;
|
||||
Alpine.store('media').allow_uploads = options.allow_uploads;
|
||||
Alpine.store('media').callback = callback;
|
||||
|
||||
Swal.fire({
|
||||
title: options.allow_uploads ? 'Select or Upload Media' : 'Select Media',
|
||||
html: SMMediaPicker.html,
|
||||
confirmButtonText: 'Select',
|
||||
confirmButtonColor: '#0284C7',
|
||||
cancelButtonText: 'Cancel',
|
||||
showCancelButton: true,
|
||||
focusConfirm: false,
|
||||
reverseButtons: true,
|
||||
didOpen: SMMediaPicker.onOpen,
|
||||
preConfirm: SMMediaPicker.preClose,
|
||||
customClass: {
|
||||
container: 'sm-media-picker-container',
|
||||
popup: 'sm-media-picker',
|
||||
}
|
||||
}).then((result) => {
|
||||
if(result.isConfirmed && callback) {
|
||||
if(Alpine.store('media').allow_multiple) {
|
||||
callback(Alpine.store('media').selected);
|
||||
} else {
|
||||
if(Alpine.store('media').selected.length > 0) {
|
||||
callback(Alpine.store('media').selected[0]);
|
||||
} else {
|
||||
callback('');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
window.SMMediaPicker = SMMediaPicker;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Alpine.store('media', {
|
||||
require_mime_type: '*',
|
||||
allow_multiple: true,
|
||||
allow_uploads: false,
|
||||
current_page: 1,
|
||||
per_page: 24,
|
||||
to: 0,
|
||||
total: 0,
|
||||
items: [],
|
||||
selected: [],
|
||||
pagination: [],
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,619 +0,0 @@
|
||||
import { useUserStore } from "@/store/UserStore";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { api } from "../helpers/api";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { updateSEOTags } from "../helpers/seo";
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
meta: {
|
||||
title: "Home",
|
||||
description:
|
||||
"STEMMechanics, a family-run company based in Cairns, Queensland, creates fantastic STEM-focused programs and activities that are both entertaining and educational.",
|
||||
},
|
||||
component: () => import("@/views/Home.vue"),
|
||||
},
|
||||
{
|
||||
path: "/blog",
|
||||
name: "blog",
|
||||
meta: {
|
||||
title: "Blog",
|
||||
},
|
||||
component: () => import(/* webpackPrefetch: true */ "@/views/Blog.vue"),
|
||||
},
|
||||
{
|
||||
path: "/article",
|
||||
redirect: "/blog",
|
||||
children: [
|
||||
{
|
||||
path: ":slug",
|
||||
name: "article",
|
||||
component: () => import("@/views/Article.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/workshops",
|
||||
name: "workshops",
|
||||
meta: {
|
||||
title: "Workshops",
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackPreload: true */ "@/views/Workshops.vue"),
|
||||
},
|
||||
{
|
||||
path: "/event",
|
||||
redirect: "/workshops",
|
||||
children: [
|
||||
{
|
||||
path: ":id",
|
||||
name: "event",
|
||||
component: () => import("@/views/Event.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/verify-email",
|
||||
name: "verify-email",
|
||||
meta: {
|
||||
title: "Verify Email",
|
||||
},
|
||||
component: () => import("@/views/EmailVerify.vue"),
|
||||
},
|
||||
{
|
||||
path: "/resend-verify-email",
|
||||
name: "resend-verify-email",
|
||||
meta: {
|
||||
title: "Resend Verification Email",
|
||||
},
|
||||
component: () => import("@/views/ResendEmailVerify.vue"),
|
||||
},
|
||||
{
|
||||
path: "/reset-password",
|
||||
name: "reset-password",
|
||||
meta: {
|
||||
title: "Reset Password",
|
||||
},
|
||||
component: () => import("@/views/ResetPassword.vue"),
|
||||
},
|
||||
{
|
||||
path: "/privacy",
|
||||
name: "privacy",
|
||||
meta: {
|
||||
title: "Privacy Policy",
|
||||
},
|
||||
component: () => import("@/views/Privacy.vue"),
|
||||
},
|
||||
{
|
||||
path: "/rules",
|
||||
name: "rules",
|
||||
meta: {
|
||||
title: "Rules",
|
||||
},
|
||||
component: () => import("@/views/Rules.vue"),
|
||||
},
|
||||
{
|
||||
path: "/community",
|
||||
name: "community",
|
||||
meta: {
|
||||
title: "Community",
|
||||
},
|
||||
component: () => import("@/views/Community.vue"),
|
||||
},
|
||||
{
|
||||
path: "/minecraft",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "minecraft",
|
||||
meta: {
|
||||
title: "Minecraft",
|
||||
},
|
||||
component: () => import("@/views/Minecraft.vue"),
|
||||
},
|
||||
{
|
||||
path: "curve",
|
||||
name: "minecraft-curve",
|
||||
meta: {
|
||||
title: "Minecraft Curve",
|
||||
},
|
||||
component: () => import("@/views/MinecraftCurve.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
meta: {
|
||||
title: "Login",
|
||||
middleware: "guest",
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackPrefetch: true */ "@/views/Login.vue"),
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
name: "logout",
|
||||
meta: {
|
||||
title: "Logout",
|
||||
},
|
||||
component: () => import("@/views/Logout.vue"),
|
||||
},
|
||||
{
|
||||
path: "/contact",
|
||||
name: "contact",
|
||||
meta: {
|
||||
title: "Contact",
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackPrefetch: true */ "@/views/Contact.vue"),
|
||||
},
|
||||
{
|
||||
path: "/conduct",
|
||||
redirect: { name: "code-of-conduct" },
|
||||
},
|
||||
{
|
||||
path: "/code-of-conduct",
|
||||
name: "code-of-conduct",
|
||||
meta: {
|
||||
title: "Code of Conduct",
|
||||
},
|
||||
component: () => import("@/views/CodeOfConduct.vue"),
|
||||
},
|
||||
{
|
||||
path: "/terms",
|
||||
redirect: { name: "terms-and-conditions" },
|
||||
},
|
||||
{
|
||||
path: "/terms-and-conditions",
|
||||
name: "terms-and-conditions",
|
||||
meta: {
|
||||
title: "Terms and Conditions",
|
||||
},
|
||||
component: () => import("@/views/TermsAndConditions.vue"),
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "register",
|
||||
meta: {
|
||||
title: "Register",
|
||||
},
|
||||
component: () =>
|
||||
import(/* webpackPrefetch: true */ "@/views/Register.vue"),
|
||||
},
|
||||
{
|
||||
path: "/dashboard",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard",
|
||||
meta: {
|
||||
title: "Dashboard",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackPrefetch: true */ "@/views/dashboard/Dashboard.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "analytics",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-analytics-list",
|
||||
meta: {
|
||||
title: "Analytics",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/AnalyticsList.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-analytics-item",
|
||||
meta: {
|
||||
title: "Analytics Session",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/AnalyticsItem.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "articles",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-article-list",
|
||||
meta: {
|
||||
title: "Articles",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/ArticleList.vue"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
name: "dashboard-article-create",
|
||||
meta: {
|
||||
title: "Create Article",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/ArticleEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-article-edit",
|
||||
meta: {
|
||||
title: "Edit Article",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/ArticleEdit.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "events",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-event-list",
|
||||
meta: {
|
||||
title: "Events",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/EventList.vue"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
name: "dashboard-event-create",
|
||||
meta: {
|
||||
title: "Create Event",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/EventEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-event-edit",
|
||||
meta: {
|
||||
title: "Event",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/EventEdit.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "details",
|
||||
name: "dashboard-account-details",
|
||||
meta: {
|
||||
title: "Account Details",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () => import("@/views/dashboard/UserEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-user-list",
|
||||
meta: {
|
||||
title: "Users",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/UserList.vue"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
name: "dashboard-user-create",
|
||||
meta: {
|
||||
title: "Create User",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/UserEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-user-edit",
|
||||
meta: {
|
||||
title: "Edit User",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/UserEdit.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "media",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-media-list",
|
||||
meta: {
|
||||
title: "Media",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/MediaList.vue"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
name: "dashboard-media-create",
|
||||
meta: {
|
||||
title: "Upload Media",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/MediaEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-media-edit",
|
||||
meta: {
|
||||
title: "Edit Media",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/MediaEdit.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "shortlinks",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-shortlink-list",
|
||||
meta: {
|
||||
title: "Shortlink",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/ShortlinkList.vue"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
name: "dashboard-shortlink-create",
|
||||
meta: {
|
||||
title: "Create Shortlink",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/ShortlinkEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-shortlink-edit",
|
||||
meta: {
|
||||
title: "Edit Shortlink",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/ShortlinkEdit.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "discord-bot-logs",
|
||||
name: "dashboard-discord-bot-logs",
|
||||
meta: {
|
||||
title: "Discord Bot Logs",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () => import("@/views/dashboard/DiscordBotLogs.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/forgot-password",
|
||||
name: "forgot-password",
|
||||
meta: {
|
||||
title: "Forgot Password",
|
||||
},
|
||||
component: () => import("@/views/ForgotPassword.vue"),
|
||||
},
|
||||
{
|
||||
path: "/file/:id",
|
||||
name: "file",
|
||||
meta: {
|
||||
title: "File",
|
||||
},
|
||||
component: () => import("@/views/File.vue"),
|
||||
},
|
||||
{
|
||||
path: "/cart",
|
||||
name: "cart",
|
||||
meta: {
|
||||
title: "Cart",
|
||||
},
|
||||
component: () => import("@/views/Cart.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)",
|
||||
name: "not-found",
|
||||
meta: {
|
||||
title: "Page not found",
|
||||
hideInEditor: true,
|
||||
},
|
||||
component: () => import("@/views/404.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior() {
|
||||
return { top: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
// export let activeRoutes = [];
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore();
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
applicationStore.hydrated = false;
|
||||
applicationStore.clearDynamicTitle();
|
||||
|
||||
if (applicationStore.pageLoaderTimeout !== 0) {
|
||||
window.clearTimeout(applicationStore.pageLoaderTimeout);
|
||||
applicationStore.pageLoaderTimeout = window.setTimeout(() => {
|
||||
const pageLoadingElem = document.getElementById("sm-page-loading");
|
||||
if (pageLoadingElem !== null) {
|
||||
pageLoadingElem.style.display = "flex";
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (to.meta.middleware == "authenticated") {
|
||||
if (userStore.id) {
|
||||
api.get({
|
||||
url: "/me",
|
||||
})
|
||||
.then((res) => {
|
||||
userStore.setUserDetails(res.data.user);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if (err.status == 401) {
|
||||
userStore.clearUser();
|
||||
|
||||
window.location.href = `/login?redirect=${to.fullPath}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!userStore.id) {
|
||||
next({
|
||||
name: "login",
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
api.post({
|
||||
url: "/analytics",
|
||||
body: {
|
||||
type: "pageview",
|
||||
path: to.fullPath,
|
||||
},
|
||||
}).catch(() => {
|
||||
/* empty */
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
if (from.name !== undefined) {
|
||||
document.body.classList.remove(`page-${from.name}`);
|
||||
}
|
||||
document.body.classList.add(`page-${to.name}`);
|
||||
|
||||
window.setTimeout(() => {
|
||||
const getMetaValue = (tag, defaultValue = "") => {
|
||||
const getMeta = (obj, tag) => {
|
||||
const tagHierarchy = tag.split(".");
|
||||
|
||||
const nearestWithMeta = obj.matched
|
||||
.slice()
|
||||
.reverse()
|
||||
.reduce(
|
||||
(acc, r) => acc || (r.meta && r.meta[tagHierarchy[0]]),
|
||||
null,
|
||||
);
|
||||
if (nearestWithMeta) {
|
||||
let result = nearestWithMeta;
|
||||
for (let i = 1; i < tagHierarchy.length; i++) {
|
||||
result = result[tagHierarchy[i]];
|
||||
if (!result) break;
|
||||
}
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const nearestMeta = getMeta(to, tag);
|
||||
if (nearestMeta == null) {
|
||||
const previousMeta = getMeta(from, tag);
|
||||
if (previousMeta == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return previousMeta;
|
||||
}
|
||||
return nearestMeta;
|
||||
};
|
||||
|
||||
updateSEOTags({
|
||||
title: getMetaValue("title"),
|
||||
description: getMetaValue("description"),
|
||||
keywords: getMetaValue("keywords", []),
|
||||
robots: {
|
||||
index: getMetaValue(
|
||||
"robots.index",
|
||||
!to.meta.middleware
|
||||
? true
|
||||
: to.meta.middleware != "authenticated",
|
||||
),
|
||||
follow: getMetaValue(
|
||||
"robots.follow",
|
||||
!to.meta.middleware
|
||||
? true
|
||||
: to.meta.middleware != "authenticated",
|
||||
),
|
||||
},
|
||||
url: getMetaValue("url", to.path),
|
||||
image: getMetaValue("image", ""),
|
||||
});
|
||||
}, 10);
|
||||
|
||||
window.setTimeout(() => {
|
||||
const autofocusElement = document.querySelector("[autofocus]");
|
||||
if (autofocusElement) {
|
||||
autofocusElement.focus();
|
||||
}
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
const target = document.querySelector(hash);
|
||||
if (target) {
|
||||
target.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
|
||||
if (applicationStore.pageLoaderTimeout !== 0) {
|
||||
window.clearTimeout(applicationStore.pageLoaderTimeout);
|
||||
applicationStore.pageLoaderTimeout = 0;
|
||||
}
|
||||
|
||||
const pageLoadingElem = document.getElementById("sm-page-loading");
|
||||
if (pageLoadingElem !== null) {
|
||||
pageLoadingElem.style.display = "none";
|
||||
}
|
||||
|
||||
applicationStore.hydrated = true;
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,94 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
type ApplicationStoreEventKeyUpCallback = (event: KeyboardEvent) => boolean;
|
||||
type ApplicationStoreEventKeyPressCallback = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export interface ApplicationStore {
|
||||
hydrated: boolean;
|
||||
unavailable: boolean;
|
||||
dynamicTitle: string;
|
||||
eventKeyUpStack: ApplicationStoreEventKeyUpCallback[];
|
||||
eventKeyPressStack: ApplicationStoreEventKeyPressCallback[];
|
||||
pageLoaderTimeout: number;
|
||||
_addedListener: boolean;
|
||||
}
|
||||
|
||||
export const useApplicationStore = defineStore({
|
||||
id: "application",
|
||||
state: (): ApplicationStore => ({
|
||||
hydrated: false,
|
||||
unavailable: false,
|
||||
dynamicTitle: "",
|
||||
eventKeyUpStack: [],
|
||||
eventKeyPressStack: [],
|
||||
pageLoaderTimeout: 0,
|
||||
_addedListener: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async setDynamicTitle(title: string) {
|
||||
this.$state.dynamicTitle = title;
|
||||
document.title = `STEMMechanics | ${title}`;
|
||||
},
|
||||
|
||||
clearDynamicTitle() {
|
||||
this.$state.dynamicTitle = "";
|
||||
},
|
||||
|
||||
addKeyUpListener(callback: ApplicationStoreEventKeyUpCallback) {
|
||||
this.eventKeyUpStack.push(callback);
|
||||
|
||||
if (!this._addedListener) {
|
||||
document.addEventListener("keyup", (event: KeyboardEvent) => {
|
||||
this.eventKeyUpStack.every(
|
||||
(item: ApplicationStoreEventKeyUpCallback) => {
|
||||
const result = item(event);
|
||||
if (result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeKeyUpListener(callback: ApplicationStoreEventKeyUpCallback) {
|
||||
this.eventKeyUpStack = this.eventKeyUpStack.filter(
|
||||
(item: ApplicationStoreEventKeyUpCallback) => item !== callback,
|
||||
);
|
||||
},
|
||||
|
||||
addKeyPressListener(callback: ApplicationStoreEventKeyPressCallback) {
|
||||
this.eventKeyPressStack.push(callback);
|
||||
|
||||
if (!this._addedListener) {
|
||||
document.addEventListener(
|
||||
"keypress",
|
||||
(event: KeyboardEvent) => {
|
||||
this.eventKeyPressStack.every(
|
||||
(item: ApplicationStoreEventKeyPressCallback) => {
|
||||
const result = item(event);
|
||||
if (result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
removeKeyPressListener(
|
||||
callback: ApplicationStoreEventKeyPressCallback,
|
||||
) {
|
||||
this.eventKeyPressStack = this.eventKeyPressStack.filter(
|
||||
(item: ApplicationStoreEventKeyPressCallback) =>
|
||||
item !== callback,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { DefineStoreOptions, defineStore } from "pinia";
|
||||
|
||||
interface CacheItem {
|
||||
url: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export const useCacheStore = defineStore({
|
||||
id: "cache",
|
||||
state: () => ({
|
||||
cache: [] as CacheItem[],
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// Method to retrieve cached JSON data based on a URL
|
||||
getCacheByUrl(url: string) {
|
||||
const cachedItem = this.cache.find((item) => item.url === url);
|
||||
return cachedItem ? cachedItem.data : null;
|
||||
},
|
||||
|
||||
// Method to update the cache with new data and check for modifications
|
||||
updateCache(url: string, newData: unknown): boolean {
|
||||
const index = this.cache.findIndex((item) => item.url === url);
|
||||
|
||||
if (index !== -1) {
|
||||
// If the URL is already in the cache, check for modifications
|
||||
const existingData = this.cache[index].data;
|
||||
|
||||
if (JSON.stringify(existingData) === JSON.stringify(newData)) {
|
||||
// Data is not modified, return false
|
||||
return false;
|
||||
} else {
|
||||
// Data is modified, update the cache
|
||||
this.cache[index].data = newData;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// If the URL is not in the cache, add it
|
||||
this.cache.push({ url, data: newData });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
// Method to clear the cache for a specific URL
|
||||
clearCacheByUrl(url: string) {
|
||||
const index = this.cache.findIndex((item) => item.url === url);
|
||||
if (index !== -1) {
|
||||
this.cache.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
// Method to clear the entire cache
|
||||
clearCache() {
|
||||
this.cache = [];
|
||||
},
|
||||
},
|
||||
|
||||
persist: true,
|
||||
} as DefineStoreOptions<string, unknown, unknown, unknown> & {
|
||||
persist?: boolean;
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export interface ToastOptions {
|
||||
id?: number;
|
||||
title?: string;
|
||||
content: string;
|
||||
type?: string;
|
||||
loader?: boolean;
|
||||
}
|
||||
|
||||
export interface ToastItem {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
loader: boolean;
|
||||
}
|
||||
|
||||
export interface ToastStore {
|
||||
toasts: ToastItem[];
|
||||
}
|
||||
|
||||
export const defaultToastItem: ToastItem = {
|
||||
id: 0,
|
||||
title: "",
|
||||
content: "",
|
||||
type: "primary",
|
||||
loader: false,
|
||||
};
|
||||
|
||||
export const useToastStore = defineStore({
|
||||
id: "toasts",
|
||||
state: (): ToastStore => ({
|
||||
toasts: [],
|
||||
}),
|
||||
|
||||
actions: {
|
||||
addToast(toast: ToastOptions): number {
|
||||
while (
|
||||
!toast.id ||
|
||||
toast.id == 0 ||
|
||||
this.toasts.find((item: ToastItem) => item.id === toast.id)
|
||||
) {
|
||||
toast.id =
|
||||
Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
|
||||
}
|
||||
|
||||
toast.title = toast.title || defaultToastItem.title;
|
||||
toast.type = toast.type || defaultToastItem.type;
|
||||
|
||||
this.toasts.push(toast);
|
||||
return toast.id;
|
||||
},
|
||||
|
||||
clearToast(id: number): void {
|
||||
this.toasts = this.toasts.filter(
|
||||
(item: ToastItem) => item.id !== id
|
||||
);
|
||||
},
|
||||
|
||||
updateToast(id: number, updatedFields: Partial<ToastOptions>): void {
|
||||
const toastToUpdate = this.toasts.find(
|
||||
(item: ToastItem) => item.id === id
|
||||
);
|
||||
|
||||
if (toastToUpdate) {
|
||||
toastToUpdate.title =
|
||||
updatedFields.title || toastToUpdate.title;
|
||||
toastToUpdate.content =
|
||||
updatedFields.content || toastToUpdate.content;
|
||||
toastToUpdate.type = updatedFields.type || toastToUpdate.type;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
updatedFields,
|
||||
"loader"
|
||||
)
|
||||
) {
|
||||
toastToUpdate.loader = updatedFields.loader;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { defineStore, DefineStoreOptions } from "pinia";
|
||||
|
||||
export interface UserDetails {
|
||||
id: string;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
display_name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
id: string;
|
||||
token: string;
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
displayName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: "user",
|
||||
state: (): UserState => {
|
||||
return {
|
||||
id: "",
|
||||
token: "",
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
displayName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
permissions: [],
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
async setUserDetails(user: UserDetails) {
|
||||
this.$state.id = user.id;
|
||||
this.$state.username = user.username;
|
||||
this.$state.firstName = user.first_name;
|
||||
this.$state.lastName = user.last_name;
|
||||
this.$state.displayName = user.display_name;
|
||||
this.$state.email = user.email;
|
||||
this.$state.phone = user.phone;
|
||||
this.$state.permissions = user.permissions || [];
|
||||
},
|
||||
|
||||
async setUserToken(token: string) {
|
||||
this.$state.token = token;
|
||||
},
|
||||
|
||||
clearUser() {
|
||||
this.$state.id = null;
|
||||
this.$state.token = null;
|
||||
this.$state.username = null;
|
||||
this.$state.firstName = null;
|
||||
this.$state.lastName = null;
|
||||
this.$state.displayName = null;
|
||||
this.$state.email = null;
|
||||
this.$state.phone = null;
|
||||
this.$state.permissions = [];
|
||||
},
|
||||
},
|
||||
|
||||
persist: true,
|
||||
} as DefineStoreOptions<string, unknown, unknown, unknown> & { persist?: boolean });
|
||||
@@ -1,33 +0,0 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
|
||||
describe("format()", () => {
|
||||
it("should return an empty string when the first argument is not a Date object", () => {
|
||||
const result = new SMDate("not a date").format("yyyy-MM-dd");
|
||||
expect(result).toEqual("");
|
||||
});
|
||||
|
||||
it("should format the date correctly", () => {
|
||||
const date = new Date("2022-02-19T12:34:56");
|
||||
const result = new SMDate(date).format("yyyy-MM-dd HH:mm:ss");
|
||||
expect(result).toEqual("2022-02-19 12:34:56");
|
||||
});
|
||||
|
||||
it("should handle single-digit month and day", () => {
|
||||
const date = new Date("2022-01-01T00:00:00");
|
||||
const result = new SMDate(date).format("yy-M-d");
|
||||
expect(result).toEqual("22-1-1");
|
||||
});
|
||||
|
||||
it("should handle day of week and month name abbreviations", () => {
|
||||
const date = new Date("2022-03-22T00:00:00");
|
||||
const result = new SMDate(date).format("EEE, MMM dd, yyyy");
|
||||
expect(result).toEqual("Tue, Mar 22, 2022");
|
||||
});
|
||||
|
||||
it("should handle 12-hour clock with am/pm", () => {
|
||||
const date = new Date("2022-01-01T12:34:56");
|
||||
const result = new SMDate(date).format("hh:mm:ss aa");
|
||||
expect(result).toEqual("12:34:56 pm");
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
|
||||
describe("toTitleCase()", () => {
|
||||
it("should return a converted title case string", () => {
|
||||
const result = toTitleCase("titlecase");
|
||||
expect(result).toEqual("Titlecase");
|
||||
});
|
||||
|
||||
it("should return a converted title case string and spaces", () => {
|
||||
const result = toTitleCase("titlecase_and_more");
|
||||
expect(result).toEqual("Titlecase And More");
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { Email } from "../helpers/validate";
|
||||
|
||||
describe("Email()", () => {
|
||||
it("should return valid=false when an invalid email address is passed to the validate function", async () => {
|
||||
const v = Email();
|
||||
const result = await v.validate("invalid email");
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("should return valid=false when an invalid email address is passed to the validate function", async () => {
|
||||
const v = Email();
|
||||
const result = await v.validate("fake@outlook");
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it("should return valid=true when an valid email address is passed to the validate function", async () => {
|
||||
const v = Email();
|
||||
const result = await v.validate("fake@outlook.com");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("should return valid=true when an valid email address is passed to the validate function", async () => {
|
||||
const v = Email();
|
||||
const result = await v.validate("fake@outlook.com.au");
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
40
resources/js/tooltip.js
Normal file
40
resources/js/tooltip.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Create a new div element for the tooltip
|
||||
var tooltipDiv = document.createElement("div");
|
||||
tooltipDiv.style.display = "none";
|
||||
tooltipDiv.classList.add('absolute');
|
||||
tooltipDiv.classList.add('bg-yellow-200');
|
||||
tooltipDiv.classList.add('border');
|
||||
tooltipDiv.classList.add('border-yellow-400');
|
||||
tooltipDiv.classList.add('text-yellow-900');
|
||||
tooltipDiv.classList.add('select-none');
|
||||
tooltipDiv.classList.add('p-1');
|
||||
tooltipDiv.classList.add('rounded-sm');
|
||||
tooltipDiv.classList.add('shadow-md');
|
||||
tooltipDiv.classList.add('text-xs');
|
||||
tooltipDiv.classList.add('z-10');
|
||||
tooltipDiv.classList.add('max-w-48');
|
||||
document.body.appendChild(tooltipDiv);
|
||||
|
||||
// Add event listeners to the body
|
||||
document.body.addEventListener('mouseover', showTooltip);
|
||||
document.body.addEventListener('mouseout', hideTooltip);
|
||||
document.body.addEventListener('touchstart', showTooltip);
|
||||
document.body.addEventListener('touchend', hideTooltip);
|
||||
|
||||
function showTooltip(event) {
|
||||
// Check if the event target has a title attribute
|
||||
if (event.target.hasAttribute('data-tooltip')) {
|
||||
// Show the tooltip and position it
|
||||
tooltipDiv.style.display = "block";
|
||||
tooltipDiv.style.left = ((event.pageX || event.touches[0].pageX) + 5) + 'px';
|
||||
tooltipDiv.style.top = ((event.pageY || event.touches[0].pageY) - 5) + 'px';
|
||||
tooltipDiv.textContent = event.target.getAttribute('data-tooltip');
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip(event) {
|
||||
// Check if the event target has a title attribute
|
||||
if (event.target !== tooltipDiv && !event.target.hasAttribute('data-tooltip')) {
|
||||
tooltipDiv.style.display = "none";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<SMPageStatus :status="404" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPageStatus from "../components/SMPageStatus.vue";
|
||||
</script>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<SMNavbar />
|
||||
<main class="flex-1">
|
||||
<SMLoading v-if="loading" class="h-95" />
|
||||
<router-view v-else v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</main>
|
||||
<SMPageFooter />
|
||||
<SMToastList />
|
||||
<SMDialogList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMNavbar from "../components/SMNavbar.vue";
|
||||
import SMPageFooter from "../components/SMPageFooter.vue";
|
||||
import SMToastList from "../components/SMToastList.vue";
|
||||
import SMDialogList from "../components/SMDialog";
|
||||
import SMLoading from "../components/SMLoading.vue";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const loading = ref(true);
|
||||
let loadingTimeout = null;
|
||||
|
||||
watch(
|
||||
() => useApplicationStore().hydrated,
|
||||
(newValue) => {
|
||||
if (newValue == true) {
|
||||
if (loadingTimeout != null) {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
}
|
||||
loading.value = false;
|
||||
} else {
|
||||
if (loadingTimeout == null) {
|
||||
loadingTimeout = setTimeout(() => {
|
||||
loading.value = true;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<SMLoading class="pt-24 pb-48" v-if="pageLoading" />
|
||||
<SMPageStatus
|
||||
v-else-if="!pageLoading && pageStatus != 200"
|
||||
:status="pageStatus" />
|
||||
<template v-else>
|
||||
<div
|
||||
class="max-w-4xl mx-auto h-96 text-center mb-8 relative rounded-4 overflow-hidden">
|
||||
<div
|
||||
class="blur bg-cover bg-center absolute top-0 left-0 w-full h-full -z-1 opacity-50"
|
||||
:style="{
|
||||
backgroundImage: `url('${backgroundImageUrl}')`,
|
||||
}"></div>
|
||||
<img :src="backgroundImageUrl" class="h-full" />
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto flex flex-col px-4">
|
||||
<h1 class="pb-2 text-gray-6">
|
||||
{{ article.title }}
|
||||
</h1>
|
||||
<div
|
||||
class="flex flex-1 flex-justify-between flex-items-center pb-4">
|
||||
<div>
|
||||
<div class="font-bold text-gray-4">
|
||||
{{ formattedDate(article.publish_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
v-if="userHasPermission('admin/articles') && article.id"
|
||||
role="button"
|
||||
:to="{
|
||||
name: 'dashboard-article-edit',
|
||||
params: { id: article.id },
|
||||
}"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm border-1 bg-white border-sky-6 text-sky-600 text-center"
|
||||
>Edit Article</router-link
|
||||
>
|
||||
</div>
|
||||
<SMHTML :html="article.content" />
|
||||
<SMImageGallery
|
||||
v-if="article.gallery.length > 0"
|
||||
:model-value="article.gallery" />
|
||||
<SMAttachments
|
||||
v-if="article.attachments.length > 0"
|
||||
:model-value="article.attachments || []" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, Ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMAttachments from "../components/SMAttachments.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Article, ArticleCollection, User } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import { userHasPermission } from "../helpers/utils";
|
||||
import SMLoading from "../components/SMLoading.vue";
|
||||
import SMPageStatus from "../components/SMPageStatus.vue";
|
||||
import SMHTML from "../components/SMHTML.vue";
|
||||
import SMImageGallery from "../components/SMImageGallery.vue";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* The article data.
|
||||
*/
|
||||
let article: Ref<Article> = ref({
|
||||
id: "",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
title: "",
|
||||
slug: "",
|
||||
user_id: "",
|
||||
user: { display_name: "" },
|
||||
content: "",
|
||||
publish_at: "",
|
||||
hero: {},
|
||||
gallery: [],
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
/**
|
||||
* The current page error.
|
||||
*/
|
||||
let pageStatus = ref(200);
|
||||
|
||||
/**
|
||||
* Is the page loading.
|
||||
*/
|
||||
let pageLoading = ref(false);
|
||||
|
||||
/**
|
||||
* Article user.
|
||||
*/
|
||||
let articleUser: User | null = null;
|
||||
|
||||
/**
|
||||
* Thumbnail image URL.
|
||||
*/
|
||||
let backgroundImageUrl = ref("");
|
||||
|
||||
/**
|
||||
* Load the page data.
|
||||
*/
|
||||
const handleLoad = async () => {
|
||||
let slug = useRoute().params.slug || "";
|
||||
pageLoading.value = true;
|
||||
|
||||
if (slug.length > 0) {
|
||||
let result = await api.get({
|
||||
url: "/articles",
|
||||
params: {
|
||||
slug: `=${slug}`,
|
||||
limit: 1,
|
||||
},
|
||||
callback: (result) => {
|
||||
if (result.status < 300) {
|
||||
const data = result.data as ArticleCollection;
|
||||
|
||||
if (data && data.articles && data.total && data.total > 0) {
|
||||
article.value = data.articles[0];
|
||||
|
||||
article.value.publish_at = new SMDate(
|
||||
article.value.publish_at,
|
||||
{
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
},
|
||||
).format("yyyy/MM/dd HH:mm:ss");
|
||||
|
||||
backgroundImageUrl.value = mediaGetVariantUrl(
|
||||
article.value.hero,
|
||||
"large",
|
||||
);
|
||||
applicationStore.setDynamicTitle(article.value.title);
|
||||
} else {
|
||||
pageStatus.value = 404;
|
||||
}
|
||||
} else {
|
||||
pageStatus.value = result.status;
|
||||
}
|
||||
|
||||
pageLoading.value = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
pageStatus.value = 404;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format Date
|
||||
* @param dateStr Date string.
|
||||
* @returns Formatted date.
|
||||
*/
|
||||
const formattedDate = (dateStr) => {
|
||||
return new SMDate(dateStr, { format: "yMd" }).format("MMMM d, yyyy");
|
||||
};
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<SMMastHead title="Blog" />
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
<div class="flex space-between gap-4 py-8">
|
||||
<SMInput
|
||||
type="text"
|
||||
label="Search articles"
|
||||
v-model="searchInput"
|
||||
@keyup.enter="handleSearch"
|
||||
@blur="handleSearch">
|
||||
<template #append
|
||||
><button
|
||||
type="button"
|
||||
class="font-medium px-4 py-3.1 rounded-r-2 hover:shadow-md transition bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
@click="handleSearch">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="h-6">
|
||||
<path
|
||||
d="M796-121 533-384q-30 26-69.959 40.5T378-329q-108.162 0-183.081-75Q120-479 120-585t75-181q75-75 181.5-75t181 75Q632-691 632-584.85 632-542 618-502q-14 40-42 75l264 262-44 44ZM377-389q81.25 0 138.125-57.5T572-585q0-81-56.875-138.5T377-781q-82.083 0-139.542 57.5Q180-666 180-585t57.458 138.5Q294.917-389 377-389Z"
|
||||
fill="currentColor" />
|
||||
</svg></button
|
||||
></template>
|
||||
</SMInput>
|
||||
</div>
|
||||
<SMPagination
|
||||
v-if="articlesTotal > articlesPerPage"
|
||||
class="mb-4"
|
||||
v-model="articlesPage"
|
||||
:total="articlesTotal"
|
||||
:per-page="articlesPerPage" />
|
||||
<SMLoading v-if="pageLoading" />
|
||||
<div
|
||||
v-else-if="articles.length > 0"
|
||||
class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
<SMArticleCard
|
||||
v-for="(article, index) in articles"
|
||||
:key="index"
|
||||
:article="article" />
|
||||
</div>
|
||||
<div v-else class="py-12 text-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
class="h-24 text-gray-5">
|
||||
<path
|
||||
d="M453-280h60v-240h-60v240Zm26.982-314q14.018 0 23.518-9.2T513-626q0-14.45-9.482-24.225-9.483-9.775-23.5-9.775-14.018 0-23.518 9.775T447-626q0 13.6 9.482 22.8 9.483 9.2 23.5 9.2Zm.284 514q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80-397.681 80-480.5q0-82.819 31.5-155.659Q143-709 197.5-763t127.341-85.5Q397.681-880 480.5-880q82.819 0 155.659 31.5Q709-817 763-763t85.5 127Q880-563 880-480.266q0 82.734-31.5 155.5T763-197.684q-54 54.316-127 86Q563-80 480.266-80Zm.234-60Q622-140 721-239.5t99-241Q820-622 721.188-721 622.375-820 480-820q-141 0-240.5 98.812Q140-622.375 140-480q0 141 99.5 240.5t241 99.5Zm-.5-340Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<p class="text-lg text-gray-5">
|
||||
{{ articlesError || "No posts where found" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref, watch } from "vue";
|
||||
import SMPagination from "../components/SMPagination.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Article, ArticleCollection } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMLoading from "../components/SMLoading.vue";
|
||||
import SMArticleCard from "../components/SMArticleCard.vue";
|
||||
|
||||
const message = ref("");
|
||||
const pageLoading = ref(true);
|
||||
const articles: Ref<Article[]> = ref([]);
|
||||
|
||||
const articlesPerPage = 24;
|
||||
let articlesPage = ref(1);
|
||||
let articlesTotal = ref(0);
|
||||
|
||||
const articlesError = ref("");
|
||||
|
||||
let searchInput = ref("");
|
||||
let oldSearchInput = "";
|
||||
|
||||
const handleSearch = () => {
|
||||
if (oldSearchInput != searchInput.value) {
|
||||
oldSearchInput = searchInput.value;
|
||||
articlesPage.value = 1;
|
||||
handleLoad();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the page data.
|
||||
*/
|
||||
const handleLoad = () => {
|
||||
message.value = "";
|
||||
pageLoading.value = true;
|
||||
articles.value = [];
|
||||
|
||||
let params = {
|
||||
limit: articlesPerPage,
|
||||
page: articlesPage.value,
|
||||
};
|
||||
|
||||
if (searchInput.value.length > 0) {
|
||||
params[
|
||||
"filter"
|
||||
] = `(title:${searchInput.value},OR,content:${searchInput.value})`;
|
||||
}
|
||||
|
||||
api.get({
|
||||
url: "/articles",
|
||||
params: params,
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as ArticleCollection;
|
||||
|
||||
articles.value = data.articles;
|
||||
articlesTotal.value = data.total;
|
||||
articles.value.forEach((article) => {
|
||||
article.publish_at = new SMDate(article.publish_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status != 404) {
|
||||
message.value =
|
||||
error.data?.message ||
|
||||
"The server is currently not available";
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
pageLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => articlesPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
},
|
||||
);
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-blog {
|
||||
.articles {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.page-blog .articles {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.page-blog .articles {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<div>CART</div>
|
||||
</template>
|
||||
@@ -1,150 +0,0 @@
|
||||
<template>
|
||||
<SMMastHead title="Code of Conduct" />
|
||||
<div class="pb-12">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<p class="pt-16 pb-2">
|
||||
STEMMechanics supports the international community open to
|
||||
everyone without discrimination. We want this community to be a
|
||||
safe and welcoming place for both newcomers and current members.
|
||||
Everyone should feel comfortable and accepted regardless of
|
||||
their personal background and affiliation our projects and
|
||||
workshops.
|
||||
</p>
|
||||
<SMHeader text="Philosophy" class="pt-16 pb-2" />
|
||||
<p>
|
||||
In the STEMMechanics community, participants from all over the
|
||||
world come together to create and work on STEM projects. This is
|
||||
made possible by the support, hard work, and enthusiasm of
|
||||
people who collaborate towards the common goal of creating great
|
||||
ideas. Cooperation at such a scale requires common guidelines to
|
||||
ensure a positive and inspiring atmosphere in the community.
|
||||
</p>
|
||||
<p>
|
||||
This is why we have this Code of Conduct: it explains the type
|
||||
of community we want to have. The rules below are not applied to
|
||||
all interactions with a simple matching algorithm. Human
|
||||
interactions happen in context and are complex. Perceived
|
||||
violations are evaluated by real humans who will try to
|
||||
interpret the interactions and the rules with kindness.
|
||||
Accordingly, there is no need to hypothesize on how these rules
|
||||
would affect normal interactions. Be reasonable, the
|
||||
<a href="#coc-team">Code of Conduct team</a> surely will be as
|
||||
well.
|
||||
</p>
|
||||
<SMHeader text="Application" class="pt-16 pb-2" />
|
||||
<p>
|
||||
This Code of Conduct applies to all users, contributors and
|
||||
participants who engage with the STEMMechanics workshops,
|
||||
projects and its community platforms.
|
||||
</p>
|
||||
<SMHeader text="Expectations" class="pt-16 pb-2" />
|
||||
<ul class="list-disc">
|
||||
<li>
|
||||
Politeness is expected at all times. Be kind and courteous.
|
||||
</li>
|
||||
<li>
|
||||
Always assume positive intent from others. Be aware that
|
||||
differences in culture and English proficiency make written
|
||||
communication more difficult than face-to-face communication
|
||||
and that your interpretation of messages may not be the one
|
||||
the author intended. Conversely, if someone asks you to
|
||||
rephrase something you said, be ready to do so without
|
||||
feeling judged.
|
||||
</li>
|
||||
<li>
|
||||
Feedback is always welcome but keep your criticism
|
||||
constructive. We encourage you to open discussions,
|
||||
proposals, issues, and bug reports. Use the community
|
||||
platforms to discuss improvements, not to vent out
|
||||
frustration. Similarly, when other users offer you feedback
|
||||
please accept it gracefully.
|
||||
</li>
|
||||
</ul>
|
||||
<SMHeader text="Restricted conduct" class="pt-16 pb-2" />
|
||||
<p>
|
||||
Participating in restricted conduct will lead to a warning from
|
||||
community moderators and/or the Code of Conduct team and may
|
||||
lead to exclusion from the community in the form of a ban from
|
||||
one or all platforms.
|
||||
</p>
|
||||
<ul class="list-disc">
|
||||
<li>
|
||||
STEMMechanics is committed to providing a friendly and safe
|
||||
environment for everyone, regardless of level of experience,
|
||||
gender identity and expression, sexual orientation,
|
||||
disability, physical appearance, body size, race, ethnicity,
|
||||
language proficiency, age, political orientation,
|
||||
nationality, religion, or other similar characteristics. We
|
||||
do not tolerate harassment or discrimination of participants
|
||||
in any form.
|
||||
</li>
|
||||
<li>
|
||||
In particular, we strive to be welcoming to all and to
|
||||
ensure that anyone can take a more active role in the
|
||||
community and a project. Targeted harassment of minorities
|
||||
or individuals is unacceptable.
|
||||
</li>
|
||||
<li>Aggressive or offensive behavior is not acceptable.</li>
|
||||
<li>
|
||||
You will be excluded from participating in the community if
|
||||
you insult, demean, harass, intentionally make others
|
||||
uncomfortable by any means, or participate in any other
|
||||
hateful conduct, either publicly or privately.
|
||||
</li>
|
||||
<li>
|
||||
Likewise, any spamming, trolling, flaming, baiting, or other
|
||||
attention-stealing behavior is not welcome and will result
|
||||
in exclusion from the community.
|
||||
</li>
|
||||
<li>
|
||||
Any form of retaliation against a participant who contacts
|
||||
the Code of Conduct team is completely unacceptable,
|
||||
regardless of the outcome of the complaint. Any such
|
||||
behavior will result in exclusion from the community.
|
||||
</li>
|
||||
<li>
|
||||
For certainty, any conduct which could reasonably be
|
||||
considered inappropriate in a professional setting is not
|
||||
acceptable.
|
||||
</li>
|
||||
</ul>
|
||||
<SMHeader text="Reporting a breach" class="pt-16 pb-2" />
|
||||
<p>
|
||||
If you witness or are involved in an interaction with another
|
||||
community member that you think may violate this Code of
|
||||
Conduct, please contact STEMMechanics
|
||||
<a href="#coc-team">Code of Conduct team</a>.
|
||||
</p>
|
||||
<p>
|
||||
STEMMechanics recognizes that it can be difficult to come
|
||||
forward in cases of a violation of the Code of Conduct. To make
|
||||
it easier to report violations, we provide a single point of
|
||||
contact via email at:
|
||||
<a href="conduct@stemmechanics.com.au"
|
||||
>conduct@stemmechanics.com.au</a
|
||||
>. If you are more comfortable reaching out to a single person,
|
||||
you are also welcome to contact one or more members of the team
|
||||
using their personal emails listed below, or via direct
|
||||
messaging on community platforms where they are present.
|
||||
</p>
|
||||
<SMHeader
|
||||
id="coc-item"
|
||||
text="Code of Conduct team"
|
||||
class="pt-16 pb-2" />
|
||||
<ul class="list-disc">
|
||||
<li>James Collins, james@stemmechanics.com.au</li>
|
||||
<ul class="list-circle">
|
||||
<li>
|
||||
GitHub / Discord / Reddit / Twitter:
|
||||
<span class="italic">nomadjimbob</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
import SMHeader from "../components/SMHeader.vue";
|
||||
</script>
|
||||
@@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<SMMastHead title="Community"
|
||||
>STEMMechanics has an active community across multiple channels. By
|
||||
joining our communities, you agree to follow the
|
||||
<router-link :to="{ name: 'code-of-conduct' }"
|
||||
>Code of Conduct</router-link
|
||||
>.</SMMastHead
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 pt-8">
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
<a
|
||||
:href="community.url"
|
||||
class="min-w-84 decoration-none bg-white border-1 border-gray-3 rounded-xl transition hover:shadow-md"
|
||||
v-for="(community, index) in communities"
|
||||
:key="index">
|
||||
<div
|
||||
class="h-36 bg-cover bg-no-repeat bg-center rounded-t-xl"
|
||||
:style="{
|
||||
backgroundImage: `url(${community.thumbnail})`,
|
||||
}"></div>
|
||||
<h2 class="p-4">{{ community.title }}</h2>
|
||||
<p class="text-sm text-gray-5 px-4 pb-8">
|
||||
{{ community.content }}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
|
||||
const communities = [
|
||||
{
|
||||
thumbnail: "/assets/community-discord.webp",
|
||||
url: "https://discord.gg/yNzk4x7mpD",
|
||||
title: "Discord",
|
||||
content:
|
||||
"A vibrant community for discussion, user support, showcases... and custom emoji!",
|
||||
},
|
||||
{
|
||||
thumbnail: "/assets/community-minecraft.webp",
|
||||
url: "/minecraft",
|
||||
title: "Minecraft",
|
||||
content:
|
||||
"Our usual hang-out to kill zombies and build redstone contraptions.",
|
||||
},
|
||||
{
|
||||
thumbnail: "/assets/community-github.webp",
|
||||
url: "https://github.com/stemmechanics",
|
||||
title: "GitHub",
|
||||
content: "All our open-source projects. Send bug reports here.",
|
||||
},
|
||||
{
|
||||
thumbnail: "/assets/community-youtube.webp",
|
||||
url: "https://youtube.com/stemmechanics",
|
||||
title: "YouTube",
|
||||
content: "Channel for official STEMMechanics videos.",
|
||||
},
|
||||
{
|
||||
thumbnail: "/assets/community-facebook.webp",
|
||||
url: "https://facebook.com/stemmechanics",
|
||||
title: "Facebook",
|
||||
content: "Community for discussions and showcasing workshops.",
|
||||
},
|
||||
{
|
||||
thumbnail: "/assets/community-mastodon.webp",
|
||||
url: "https://mastodon.au/@stemmechanics",
|
||||
title: "Mastodon",
|
||||
content: "Connect with us in the Fediverse.",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-community {
|
||||
.communities {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
|
||||
.community-card {
|
||||
text-decoration: none;
|
||||
color: var(--base-color-text);
|
||||
background-color: var(--base-color-light);
|
||||
box-shadow: var(--base-shadow);
|
||||
|
||||
&:hover {
|
||||
filter: none;
|
||||
|
||||
.thumbnail {
|
||||
filter: brightness(115%);
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
aspect-ratio: 16 / 9;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: var(--card-background-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
word-break: break-word;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 90%;
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.page-community .communities {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.page-community .communities {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<SMMastHead title="Contact us" />
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<SMHeader text="Questions & Support" class="pt-16 pb-2" />
|
||||
<p>
|
||||
If you have a question or would like help with a project, you can
|
||||
send it our way using the form on this page or be emailing
|
||||
<a href="mailto:hello@stemmechanics.com.au"
|
||||
>hello@stemmechanics.com.au</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
You can find us on various social media platforms, and if you join
|
||||
our
|
||||
<a href="https://discord.gg/yNzk4x7mpD">Discord</a>
|
||||
server, you'll have the opportunity to connect with our team,
|
||||
participants, and other individuals who share similar interests.
|
||||
</p>
|
||||
<SMSocialIcons />
|
||||
<SMHeader text="Wanting a workshop?" class="pt-16 pb-2" />
|
||||
<p>
|
||||
We provide both public and private workshops as well as run events
|
||||
on behalf of your organisation. If you would like to discuss a
|
||||
potential opportunity, send us an email at
|
||||
<a href="mailto:hello@stemmechanics.com.au"
|
||||
>hello@stemmechanics.com.au</a
|
||||
>.
|
||||
</p>
|
||||
<SMHeader text="Where are you located?" class="pt-16 pb-2" />
|
||||
<p>
|
||||
We do not have a physical address as our workshops are delivered
|
||||
across Queensland. Visit the
|
||||
<router-link :to="{ name: 'workshops' }">workshops</router-link>
|
||||
page for each specific location.
|
||||
</p>
|
||||
<p>Official mail can be sent to the following postal address:</p>
|
||||
<div class="mt-8 text-center">
|
||||
<p class="mt-4">
|
||||
STEMMechanics<br />PO Box 36<br />Edmonton, QLD, 4869<br />Australia
|
||||
</p>
|
||||
<p class=""><strong>ABN: </strong>15 772 281 735</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
import SMSocialIcons from "../components/SMSocialIcons.vue";
|
||||
import SMHeader from "../components/SMHeader.vue";
|
||||
</script>
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="max-w-2xl mx-auto border-1 bg-white rounded-xl mt-7xl text-gray-5 px-12 py-8">
|
||||
<template v-if="!formDone">
|
||||
<SMForm ref="formObject" v-model="form" @submit="handleSubmit">
|
||||
<h1 class="mb-4">Email Verify</h1>
|
||||
<p class="mb-4">
|
||||
Enter your verification code below. If you have not yet
|
||||
received one,
|
||||
<router-link to="/resend-verify-email"
|
||||
>request a new code</router-link
|
||||
>.
|
||||
</p>
|
||||
<SMInput class="mb-4" autofocus control="code" />
|
||||
<div class="flex flex-justify-end items-center pt-4">
|
||||
<input
|
||||
v-if="!form.loading()"
|
||||
type="submit"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
value="Verify Code" />
|
||||
<SMLoading v-else small />
|
||||
</div>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1 class="mb-4">Email Verified!</h1>
|
||||
<p class="mb-4">Hurrah, Your email has been verified!</p>
|
||||
<div class="flex flex-justify-center items-center pt-4">
|
||||
<router-link
|
||||
role="button"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
:to="{ name: 'login' }"
|
||||
>Login</router-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Form, FormControl } from "../helpers/form";
|
||||
import { And, Max, Min, Required } from "../helpers/validate";
|
||||
import { useToastStore } from "../store/ToastStore";
|
||||
import SMLoading from "../components/SMLoading.vue";
|
||||
|
||||
// const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formDone = ref(false);
|
||||
const formObject = ref(null);
|
||||
let form = reactive(
|
||||
Form({
|
||||
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||
}),
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
form.loading(true);
|
||||
|
||||
await api.post({
|
||||
url: "/users/verifyEmail",
|
||||
body: {
|
||||
code: form.controls.code.value,
|
||||
// captcha_token: captcha,
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
} catch (error) {
|
||||
form.apiErrors(error, (message) => {
|
||||
useToastStore().addToast({
|
||||
title: "An error occurred",
|
||||
content: message,
|
||||
type: "danger",
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
form.loading(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (useRoute().query.code !== undefined) {
|
||||
const code = useRoute().query.code;
|
||||
|
||||
if (Array.isArray(code)) {
|
||||
if (code.length > 0) {
|
||||
form.controls.code.value = code[0];
|
||||
}
|
||||
} else {
|
||||
form.controls.code.value = code;
|
||||
}
|
||||
|
||||
formObject.value.submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,357 +0,0 @@
|
||||
<template>
|
||||
<SMLoading class="pt-24 pb-48" v-if="pageLoading" />
|
||||
<SMPageStatus
|
||||
v-else-if="!pageLoading && pageStatus != 200"
|
||||
:status="pageStatus" />
|
||||
<div v-else>
|
||||
<div
|
||||
class="max-w-4xl mx-auto h-96 text-center mb-8 relative rounded-4 overflow-hidden">
|
||||
<div
|
||||
class="blur bg-cover bg-center absolute top-0 left-0 w-full h-full -z-1 opacity-50"
|
||||
:style="{
|
||||
backgroundImage: `url('${mediaGetVariantUrl(
|
||||
event.hero,
|
||||
'large',
|
||||
)}')`,
|
||||
}"></div>
|
||||
<img
|
||||
:src="mediaGetVariantUrl(event.hero, 'large')"
|
||||
class="h-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="max-w-4xl mx-auto px-4 flex flex-col-reverse sm:flex-row">
|
||||
<div class="sm:pr-8 mt-4 sm:mt-0">
|
||||
<h1 class="pb-6">{{ event.title }}</h1>
|
||||
<SMHTML class="mb-8" :html="event.content" />
|
||||
<SMAttachments :model-value="event.attachments" />
|
||||
</div>
|
||||
<div class="sm:min-w-68">
|
||||
<div
|
||||
v-if="
|
||||
event.status == 'closed' ||
|
||||
((event.status == 'open' ||
|
||||
event.status == 'full') &&
|
||||
expired)
|
||||
"
|
||||
class="text-xs px-4 py-2 b-1 border-red-400 bg-red-100 text-red-900 text-center rounded">
|
||||
Registration for this event has closed.
|
||||
</div>
|
||||
<div
|
||||
v-if="event.status == 'full' && expired == false"
|
||||
class="text-xs px-4 py-2 b-1 border-red-400 bg-red-100 text-red-900 text-center rounded">
|
||||
This event is at capacity.
|
||||
</div>
|
||||
<div
|
||||
v-if="event.status == 'soon'"
|
||||
class="text-xs px-4 py-2 b-1 border-yellow-400 bg-yellow-100 text-yellow-900 text-center rounded">
|
||||
Registration for this event will open soon.
|
||||
</div>
|
||||
<div
|
||||
v-if="event.status == 'cancelled'"
|
||||
class="text-xs px-4 py-2 b-1 border-red-400 bg-red-100 text-red-900 text-center rounded">
|
||||
This event has been cancelled.
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
event.status == 'open' &&
|
||||
expired == false &&
|
||||
event.registration_type == 'none'
|
||||
"
|
||||
class="text-xs px-4 py-2 b-1 border-yellow-400 bg-yellow-100 text-yellow-900 text-center rounded">
|
||||
Registration not required for this event.<br />Arrive
|
||||
early to avoid disappointment as seating maybe limited.
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
event.status == 'open' &&
|
||||
expired == false &&
|
||||
event.registration_type == 'link'
|
||||
"
|
||||
class="workshop-registration workshop-registration-url">
|
||||
<a
|
||||
role="button"
|
||||
:href="registerUrl"
|
||||
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-green-600 hover:bg-green-500 text-white block text-center"
|
||||
>Register for Event</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
event.status == 'open' &&
|
||||
expired == false &&
|
||||
event.registration_type == 'message'
|
||||
"
|
||||
class="text-xs px-4 py-2 b-1 border-yellow-400 bg-yellow-100 text-yellow-900 text-center rounded">
|
||||
{{ event.registration_data }}
|
||||
</div>
|
||||
<router-link
|
||||
v-if="userHasPermission('admin/events') && event.id"
|
||||
role="button"
|
||||
:to="{
|
||||
name: 'dashboard-event-edit',
|
||||
params: { id: event.id },
|
||||
}"
|
||||
class="font-medium mt-4 px-6 py-1.5 rounded-md hover:shadow-md transition text-sm border-1 bg-white border-sky-6 text-sky-600 block text-center"
|
||||
>Edit Event</router-link
|
||||
>
|
||||
<div class="text-gray-6">
|
||||
<h3 class="flex flex-items-center pb-2 pt-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 pr-1"
|
||||
viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Zm300 230q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
Date / Time
|
||||
</h3>
|
||||
<p
|
||||
v-for="(line, index) in workshopDate"
|
||||
:key="index"
|
||||
class="pl-6 text-sm mt-0">
|
||||
{{ line }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-gray-6">
|
||||
<h3 class="flex flex-items-center pb-2 pt-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 pr-2"
|
||||
viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M480.089-490Q509-490 529.5-510.589q20.5-20.588 20.5-49.5Q550-589 529.411-609.5q-20.588-20.5-49.5-20.5Q451-630 430.5-609.411q-20.5 20.588-20.5 49.5Q410-531 430.589-510.5q20.588 20.5 49.5 20.5ZM480-159q133-121 196.5-219.5T740-552q0-117.79-75.292-192.895Q589.417-820 480-820t-184.708 75.105Q220-669.79 220-552q0 75 65 173.5T480-159Zm0 79Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Zm0-472Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
Location
|
||||
</h3>
|
||||
<p class="pl-6 text-sm mt-0">
|
||||
<template v-if="event.location == 'online'"
|
||||
>Online event</template
|
||||
>
|
||||
<template
|
||||
v-else-if="event.location_url.length == 0"
|
||||
>{{ event.address }}</template
|
||||
>
|
||||
<template v-else
|
||||
><a
|
||||
:href="event.location_url"
|
||||
no-follow
|
||||
target="_blank"
|
||||
>{{ event.address }}</a
|
||||
></template
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="event.ages" class="text-gray-6">
|
||||
<h3 class="flex flex-items-center pb-2 pt-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 pr-2"
|
||||
viewBox="0 -960 960 960">
|
||||
<path
|
||||
d="M626-533q22.5 0 38.25-15.75T680-587q0-22.5-15.75-38.25T626-641q-22.5 0-38.25 15.75T572-587q0 22.5 15.75 38.25T626-533Zm-292 0q22.5 0 38.25-15.75T388-587q0-22.5-15.75-38.25T334-641q-22.5 0-38.25 15.75T280-587q0 22.5 15.75 38.25T334-533Zm146 272q66 0 121.5-35.5T682-393H278q26 61 81 96.5T480-261Zm0 181q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 340q142.375 0 241.188-98.812Q820-337.625 820-480t-98.812-241.188Q622.375-820 480-820t-241.188 98.812Q140-622.375 140-480t98.812 241.188Q337.625-140 480-140Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
{{ computedAges }}
|
||||
</h3>
|
||||
<p
|
||||
class="text-sm border-l-4 pl-2 ml-2 border-yellow-400">
|
||||
{{ computedAgeNotice }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="event.price" class="text-gray-6">
|
||||
<h3 class="flex flex-items-center pb-2 pt-6">
|
||||
<div class="w-6 text-center font-normal">$</div>
|
||||
{{ computedPrice }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, Ref, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMAttachments from "../components/SMAttachments.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Event, EventResponse } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { stringToNumber } from "../helpers/string";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import { userHasPermission } from "../helpers/utils";
|
||||
import SMLoading from "../components/SMLoading.vue";
|
||||
import SMPageStatus from "../components/SMPageStatus.vue";
|
||||
import SMHTML from "../components/SMHTML.vue";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* Event data
|
||||
*/
|
||||
const event: Ref<Event | null> = ref(null);
|
||||
|
||||
const route = useRoute();
|
||||
const pageLoading = ref(true);
|
||||
|
||||
/**
|
||||
* Page error.
|
||||
*/
|
||||
let pageStatus = ref(200);
|
||||
|
||||
const workshopDate = computed(() => {
|
||||
let str: string[] = [];
|
||||
|
||||
if (Object.keys(event.value).length > 0) {
|
||||
if (
|
||||
event.value.end_at.length > 0 &&
|
||||
event.value.start_at.substring(
|
||||
0,
|
||||
event.value.start_at.indexOf(" "),
|
||||
) !=
|
||||
event.value.end_at.substring(0, event.value.end_at.indexOf(" "))
|
||||
) {
|
||||
str = [
|
||||
new SMDate(event.value.start_at, { format: "ymd" }).format(
|
||||
"dd/MM/yyyy",
|
||||
),
|
||||
];
|
||||
if (event.value.end_at.length > 0) {
|
||||
str[0] =
|
||||
str[0] +
|
||||
" - " +
|
||||
new SMDate(event.value.end_at, { format: "ymd" }).format(
|
||||
"dd/MM/yyyy",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
str = [
|
||||
new SMDate(event.value.start_at, { format: "ymd" }).format(
|
||||
"EEEE dd MMM yyyy",
|
||||
),
|
||||
new SMDate(event.value.start_at, { format: "ymd" }).format(
|
||||
"h:mm aa",
|
||||
) +
|
||||
" - " +
|
||||
new SMDate(event.value.end_at, { format: "ymd" }).format(
|
||||
"h:mm aa",
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a computed price amount, if a form of 0, return "Free"
|
||||
*/
|
||||
const computedPrice = computed(() => {
|
||||
if (
|
||||
event.value.price.toLowerCase() == "tbc" ||
|
||||
event.value.price.toLowerCase() == "tbd"
|
||||
) {
|
||||
return event.value.price.toUpperCase();
|
||||
}
|
||||
|
||||
const parsedPrice = stringToNumber(event.value.price || "0");
|
||||
if (parsedPrice == 0) {
|
||||
return "Free";
|
||||
}
|
||||
|
||||
return event.value.price;
|
||||
});
|
||||
|
||||
const registerUrl = computed(() => {
|
||||
let href = "";
|
||||
|
||||
if (event.value?.registration_type == "link") {
|
||||
return event.value?.registration_data;
|
||||
} else if (event.value?.registration_type == "email") {
|
||||
return "mailto:" + event.value?.registration_data;
|
||||
}
|
||||
|
||||
return href;
|
||||
});
|
||||
|
||||
const expired = computed(() => {
|
||||
return new SMDate(event.value.end_at, {
|
||||
format: "ymd",
|
||||
}).isBefore();
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a human readable Ages string.
|
||||
*/
|
||||
const computedAges = computed(() => {
|
||||
const trimmed = event.value.ages.trim();
|
||||
const regex = /^(\d+)(\s*\+?\s*|\s*-\s*\d+\s*)?$/;
|
||||
|
||||
if (regex.test(trimmed)) {
|
||||
return `Ages ${trimmed}`;
|
||||
}
|
||||
|
||||
return event.value.ages;
|
||||
});
|
||||
|
||||
/**
|
||||
* Display a age notice if required.
|
||||
*/
|
||||
const computedAgeNotice = computed(() => {
|
||||
const trimmed = event.value.ages.trim();
|
||||
const regex = /^(\d+)(\s*\+?\s*|\s*-\s*\d+\s*)?$/;
|
||||
|
||||
if (regex.test(trimmed)) {
|
||||
const age = parseInt(trimmed, 10);
|
||||
if (age <= 8) {
|
||||
return "Parental supervision may be required for children 8 years of age and under.";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
/**
|
||||
* Load the page data.
|
||||
*/
|
||||
const handleLoad = async () => {
|
||||
pageLoading.value = true;
|
||||
|
||||
try {
|
||||
let result = await api.get({
|
||||
url: "/events/{event}",
|
||||
params: {
|
||||
event: route.params.id,
|
||||
},
|
||||
});
|
||||
|
||||
const eventData = result.data as EventResponse;
|
||||
|
||||
if (eventData && eventData.event) {
|
||||
event.value = eventData.event;
|
||||
event.value.start_at = new SMDate(event.value.start_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
event.value.end_at = new SMDate(event.value.end_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
|
||||
applicationStore.setDynamicTitle(event.value.title);
|
||||
} else {
|
||||
pageStatus.value = 404;
|
||||
}
|
||||
} catch (error) {
|
||||
pageStatus.value = error.status;
|
||||
} finally {
|
||||
pageLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user