drop axios/date-fns/fontawesome

This commit is contained in:
2023-02-14 15:01:06 +10:00
parent ac4d3d8ad0
commit afc3c94b04
75 changed files with 3416 additions and 2000 deletions

View File

@@ -240,7 +240,7 @@ class UserController extends ApiController
}
return $this->respondError([
'code' => 'The code was not found or has expired'
'code' => 'The code was not found or has expired.'
]);
}
@@ -278,7 +278,7 @@ class UserController extends ApiController
}//end if
return $this->respondWithErrors([
'code' => 'The code was not found or has expired'
'code' => 'The code was not found or has expired.'
]);
}

View File

@@ -0,0 +1,698 @@
<?php
namespace App\Services;
class AnimatedGifService
{
/**
* Check if a GIF file at a path is animated or not
*
* @param string $filenameOrBlob GIF file path or data blob if dataSize > 0.
* @param integer $dataSize GIF blob size.
* @return boolean GIF file/blob is animated.
*/
public static function isAnimatedGif(string $filenameOrBlob, int $dataSize = 0)
{
$regex = '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s';
$count = 0;
if ($dataSize > 0) {
if (($fh = @fopen($filenameOrBlob, 'rb')) === false) {
return false;
}
$chunk = false;
while (feof($fh) === false && $count < 2) {
$chunk = ($chunk !== '' ? substr($chunk, -20) : "") . fread($fh, (1024 * 100)); //read 100kb at a time
$count += preg_match_all($regex, $chunk, $matches);
}
fclose($fh);
} else {
$count = preg_match_all($regex, $filenameOrBlob, $matches);
}
return $count > 1;
}
/**
* Extract frames of a GIF
*
* @param string $filenameOrBlob GIF filename path
* @param integer $dataSize GIF blob size.
* @param boolean $originalFrames Get original frames (with transparent background)
*
* @return array
*/
public function extract(string $filenameOrBlob, int $dataSize = 0, $originalFrames = false)
{
if (self::isAnimatedGif($filenameOrBlob) === false) {
return [];
}
$this->reset();
$this->parseFramesInfo($filename);
$prevImg = null;
for ($i = 0; $i < count($this->frameSources); $i++) {
$this->frames[$i] = [];
$this->frameDurations[$i] = $this->frames[$i]['duration'] = $this->frameSources[$i]['delay_time'];
$img = imagecreatefromstring($this->fileHeader["gifheader"] . $this->frameSources[$i]["graphicsextension"] . $this->frameSources[$i]["imagedata"] . chr(0x3b));
if (!$originalFrames) {
if ($i > 0) {
$prevImg = $this->frames[($i - 1)]['image'];
} else {
$prevImg = $img;
}
$sprite = imagecreate($this->gifMaxWidth, $this->gifMaxHeight);
imagesavealpha($sprite, true);
$transparent = imagecolortransparent($prevImg);
if ($transparent > -1 && imagecolorstotal($prevImg) > $transparent) {
$actualTrans = imagecolorsforindex($prevImg, $transparent);
imagecolortransparent($sprite, imagecolorallocate($sprite, $actualTrans['red'], $actualTrans['green'], $actualTrans['blue']));
}
if ((int) $this->frameSources[$i]['disposal_method'] == 1 && $i > 0) {
imagecopy($sprite, $prevImg, 0, 0, 0, 0, $this->gifMaxWidth, $this->gifMaxHeight);
}
imagecopyresampled($sprite, $img, $this->frameSources[$i]["offset_left"], $this->frameSources[$i]["offset_top"], 0, 0, $this->gifMaxWidth, $this->gifMaxHeight, $this->gifMaxWidth, $this->gifMaxHeight);
$img = $sprite;
}//end if
$this->frameImages[$i] = $this->frames[$i]['image'] = $img;
}//end for
return $this->frames;
}
}
class GifFrameExtractor
{
// Properties
// ===================================================================================
/**
* @var resource
*/
private $gif;
/**
* @var array
*/
private $frames;
/**
* @var array
*/
private $frameDurations;
/**
* @var array
*/
private $frameImages;
/**
* @var array
*/
private $framePositions;
/**
* @var array
*/
private $frameDimensions;
/**
* @var integer
*
* (old: $this->index)
*/
private $frameNumber;
/**
* @var array
*
* (old: $this->imagedata)
*/
private $frameSources;
/**
* @var array
*
* (old: $this->fileHeader)
*/
private $fileHeader;
/**
* @var integer The reader pointer in the file source
*
* (old: $this->pointer)
*/
private $pointer;
/**
* @var integer
*/
private $gifMaxWidth;
/**
* @var integer
*/
private $gifMaxHeight;
/**
* @var integer
*/
private $totalDuration;
/**
* @var integer
*/
private $handle;
/**
* @var array
*
* (old: globaldata)
*/
private $globaldata;
/**
* @var array
*
* (old: orgvars)
*/
private $orgvars;
// Methods
// ===================================================================================
/**
* Parse the frame informations contained in the GIF file
*
* @param string $filename GIF filename path
*/
private function parseFramesInfo($filename)
{
$this->openFile($filename);
$this->parseGifHeader();
$this->parseGraphicsExtension(0);
$this->getApplicationData();
$this->getApplicationData();
$this->getFrameString(0);
$this->parseGraphicsExtension(1);
$this->getCommentData();
$this->getApplicationData();
$this->getFrameString(1);
while (!$this->checkByte(0x3b) && !$this->checkEOF()) {
$this->getCommentData(1);
$this->parseGraphicsExtension(2);
$this->getFrameString(2);
$this->getApplicationData();
}
}
/**
* Parse the gif header (old: get_gif_header)
*/
private function parseGifHeader()
{
$this->pointerForward(10);
if ($this->readBits(($mybyte = $this->readByteInt()), 0, 1) == 1) {
$this->pointerForward(2);
$this->pointerForward(pow(2, ($this->readBits($mybyte, 5, 3) + 1)) * 3);
} else {
$this->pointerForward(2);
}
$this->fileHeader["gifheader"] = $this->dataPart(0, $this->pointer);
// Decoding
$this->orgvars["gifheader"] = $this->fileHeader["gifheader"];
$this->orgvars["background_color"] = $this->orgvars["gifheader"][11];
}
/**
* Parse the application data of the frames (old: get_application_data)
*/
private function getApplicationData()
{
$startdata = $this->readByte(2);
if ($startdata == chr(0x21) . chr(0xff)) {
$start = ($this->pointer - 2);
$this->pointerForward($this->readByteInt());
$this->readDataStream($this->readByteInt());
$this->fileHeader["applicationdata"] = $this->dataPart($start, ($this->pointer - $start));
} else {
$this->pointerRewind(2);
}
}
/**
* Parse the comment data of the frames (old: get_comment_data)
*/
private function getCommentData()
{
$startdata = $this->readByte(2);
if ($startdata == chr(0x21) . chr(0xfe)) {
$start = ($this->pointer - 2);
$this->readDataStream($this->readByteInt());
$this->fileHeader["commentdata"] = $this->dataPart($start, ($this->pointer - $start));
} else {
$this->pointerRewind(2);
}
}
/**
* Parse the graphic extension of the frames (old: get_graphics_extension)
*
* @param integer $type
*/
private function parseGraphicsExtension($type)
{
$startdata = $this->readByte(2);
if ($startdata == chr(0x21) . chr(0xf9)) {
$start = ($this->pointer - 2);
$this->pointerForward($this->readByteInt());
$this->pointerForward(1);
if ($type == 2) {
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->dataPart($start, ($this->pointer - $start));
} elseif ($type == 1) {
$this->orgvars["hasgx_type_1"] = 1;
$this->globaldata["graphicsextension"] = $this->dataPart($start, ($this->pointer - $start));
} elseif ($type == 0) {
$this->orgvars["hasgx_type_0"] = 1;
$this->globaldata["graphicsextension_0"] = $this->dataPart($start, ($this->pointer - $start));
}
} else {
$this->pointerRewind(2);
}//end if
}
/**
* Get the full frame string block (old: get_image_block)
*
* @param integer $type
*/
private function getFrameString($type)
{
if ($this->checkByte(0x2c)) {
$start = $this->pointer;
$this->pointerForward(9);
if ($this->readBits(($mybyte = $this->readByteInt()), 0, 1) == 1) {
$this->pointerForward(pow(2, ($this->readBits($mybyte, 5, 3) + 1)) * 3);
}
$this->pointerForward(1);
$this->readDataStream($this->readByteInt());
$this->frameSources[$this->frameNumber]["imagedata"] = $this->dataPart($start, ($this->pointer - $start));
if ($type == 0) {
$this->orgvars["hasgx_type_0"] = 0;
if (isset($this->globaldata["graphicsextension_0"])) {
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension_0"];
} else {
$this->frameSources[$this->frameNumber]["graphicsextension"] = null;
}
unset($this->globaldata["graphicsextension_0"]);
} elseif ($type == 1) {
if (isset($this->orgvars["hasgx_type_1"]) && $this->orgvars["hasgx_type_1"] == 1) {
$this->orgvars["hasgx_type_1"] = 0;
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension"];
unset($this->globaldata["graphicsextension"]);
} else {
$this->orgvars["hasgx_type_0"] = 0;
$this->frameSources[$this->frameNumber]["graphicsextension"] = $this->globaldata["graphicsextension_0"];
unset($this->globaldata["graphicsextension_0"]);
}
}//end if
$this->parseFrameData();
$this->frameNumber++;
}//end if
}
/**
* Parse frame data string into an array (old: parse_image_data)
*/
private function parseFrameData()
{
$this->frameSources[$this->frameNumber]["disposal_method"] = $this->getImageDataBit("ext", 3, 3, 3);
$this->frameSources[$this->frameNumber]["user_input_flag"] = $this->getImageDataBit("ext", 3, 6, 1);
$this->frameSources[$this->frameNumber]["transparent_color_flag"] = $this->getImageDataBit("ext", 3, 7, 1);
$this->frameSources[$this->frameNumber]["delay_time"] = $this->dualByteVal($this->getImageDataByte("ext", 4, 2));
$this->totalDuration += (int) $this->frameSources[$this->frameNumber]["delay_time"];
$this->frameSources[$this->frameNumber]["transparent_color_index"] = ord($this->getImageDataByte("ext", 6, 1));
$this->frameSources[$this->frameNumber]["offset_left"] = $this->dualByteVal($this->getImageDataByte("dat", 1, 2));
$this->frameSources[$this->frameNumber]["offset_top"] = $this->dualByteVal($this->getImageDataByte("dat", 3, 2));
$this->frameSources[$this->frameNumber]["width"] = $this->dualByteVal($this->getImageDataByte("dat", 5, 2));
$this->frameSources[$this->frameNumber]["height"] = $this->dualByteVal($this->getImageDataByte("dat", 7, 2));
$this->frameSources[$this->frameNumber]["local_color_table_flag"] = $this->getImageDataBit("dat", 9, 0, 1);
$this->frameSources[$this->frameNumber]["interlace_flag"] = $this->getImageDataBit("dat", 9, 1, 1);
$this->frameSources[$this->frameNumber]["sort_flag"] = $this->getImageDataBit("dat", 9, 2, 1);
$this->frameSources[$this->frameNumber]["color_table_size"] = (pow(2, ($this->getImageDataBit("dat", 9, 5, 3) + 1)) * 3);
$this->frameSources[$this->frameNumber]["color_table"] = substr($this->frameSources[$this->frameNumber]["imagedata"], 10, $this->frameSources[$this->frameNumber]["color_table_size"]);
$this->frameSources[$this->frameNumber]["lzw_code_size"] = ord($this->getImageDataByte("dat", 10, 1));
$this->framePositions[$this->frameNumber] = [
'x' => $this->frameSources[$this->frameNumber]["offset_left"],
'y' => $this->frameSources[$this->frameNumber]["offset_top"],
];
$this->frameDimensions[$this->frameNumber] = [
'width' => $this->frameSources[$this->frameNumber]["width"],
'height' => $this->frameSources[$this->frameNumber]["height"],
];
// Decoding
$this->orgvars[$this->frameNumber]["transparent_color_flag"] = $this->frameSources[$this->frameNumber]["transparent_color_flag"];
$this->orgvars[$this->frameNumber]["transparent_color_index"] = $this->frameSources[$this->frameNumber]["transparent_color_index"];
$this->orgvars[$this->frameNumber]["delay_time"] = $this->frameSources[$this->frameNumber]["delay_time"];
$this->orgvars[$this->frameNumber]["disposal_method"] = $this->frameSources[$this->frameNumber]["disposal_method"];
$this->orgvars[$this->frameNumber]["offset_left"] = $this->frameSources[$this->frameNumber]["offset_left"];
$this->orgvars[$this->frameNumber]["offset_top"] = $this->frameSources[$this->frameNumber]["offset_top"];
// Updating the max width
if ($this->gifMaxWidth < $this->frameSources[$this->frameNumber]["width"]) {
$this->gifMaxWidth = $this->frameSources[$this->frameNumber]["width"];
}
// Updating the max height
if ($this->gifMaxHeight < $this->frameSources[$this->frameNumber]["height"]) {
$this->gifMaxHeight = $this->frameSources[$this->frameNumber]["height"];
}
}
/**
* Get the image data byte (old: get_imagedata_byte)
*
* @param string $type
* @param integer $start
* @param integer $length
*
* @return string
*/
private function getImageDataByte($type, $start, $length)
{
if ($type == "ext") {
return substr($this->frameSources[$this->frameNumber]["graphicsextension"], $start, $length);
}
// "dat"
return substr($this->frameSources[$this->frameNumber]["imagedata"], $start, $length);
}
/**
* Get the image data bit (old: get_imagedata_bit)
*
* @param string $type
* @param integer $byteIndex
* @param integer $bitStart
* @param integer $bitLength
*
* @return number
*/
private function getImageDataBit($type, $byteIndex, $bitStart, $bitLength)
{
if ($type == "ext") {
return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["graphicsextension"], $byteIndex, 1)), $bitStart, $bitLength);
}
// "dat"
return $this->readBits(ord(substr($this->frameSources[$this->frameNumber]["imagedata"], $byteIndex, 1)), $bitStart, $bitLength);
}
/**
* Return the value of 2 ASCII chars (old: dualbyteval)
*
* @param string $s
*
* @return integer
*/
private function dualByteVal($s)
{
$i = (ord($s[1]) * 256 + ord($s[0]));
return $i;
}
/**
* Read the data stream (old: read_data_stream)
*
* @param integer $firstLength
*/
private function readDataStream($firstLength)
{
$this->pointerForward($firstLength);
$length = $this->readByteInt();
if ($length != 0) {
while ($length != 0) {
$this->pointerForward($length);
$length = $this->readByteInt();
}
}
}
/**
* Open the gif file (old: loadfile)
*
* @param string $filename
*/
private function openFile($filename)
{
$this->handle = fopen($filename, "rb");
$this->pointer = 0;
$imageSize = getimagesize($filename);
$this->gifWidth = $imageSize[0];
$this->gifHeight = $imageSize[1];
}
/**
* Close the read gif file (old: closefile)
*/
private function closeFile()
{
fclose($this->handle);
$this->handle = 0;
}
/**
* Read the file from the beginning to $byteCount in binary (old: readbyte)
*
* @param integer $byteCount
*
* @return string
*/
private function readByte($byteCount)
{
$data = fread($this->handle, $byteCount);
$this->pointer += $byteCount;
return $data;
}
/**
* Read a byte and return ASCII value (old: readbyte_int)
*
* @return integer
*/
private function readByteInt()
{
$data = fread($this->handle, 1);
$this->pointer++;
return ord($data);
}
/**
* Convert a $byte to decimal (old: readbits)
*
* @param string $byte
* @param integer $start
* @param integer $length
*
* @return number
*/
private function readBits($byte, $start, $length)
{
$bin = str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
$data = substr($bin, $start, $length);
return bindec($data);
}
/**
* Rewind the file pointer reader (old: p_rewind)
*
* @param integer $length
*/
private function pointerRewind($length)
{
$this->pointer -= $length;
fseek($this->handle, $this->pointer);
}
/**
* Forward the file pointer reader (old: p_forward)
*
* @param integer $length
*/
private function pointerForward($length)
{
$this->pointer += $length;
fseek($this->handle, $this->pointer);
}
/**
* Get a section of the data from $start to $start + $length (old: datapart)
*
* @param integer $start
* @param integer $length
*
* @return string
*/
private function dataPart($start, $length)
{
fseek($this->handle, $start);
$data = fread($this->handle, $length);
fseek($this->handle, $this->pointer);
return $data;
}
/**
* Check if a character if a byte (old: checkbyte)
*
* @param integer $byte
*
* @return boolean
*/
private function checkByte($byte)
{
if (fgetc($this->handle) == chr($byte)) {
fseek($this->handle, $this->pointer);
return true;
}
fseek($this->handle, $this->pointer);
return false;
}
/**
* Check the end of the file (old: checkEOF)
*
* @return boolean
*/
private function checkEOF()
{
if (fgetc($this->handle) === false) {
return true;
}
fseek($this->handle, $this->pointer);
return false;
}
/**
* Reset and clear this current object
*/
private function reset()
{
$this->gif = null;
$this->totalDuration = $this->gifMaxHeight = $this->gifMaxWidth = $this->handle = $this->pointer = $this->frameNumber = 0;
$this->frameDimensions = $this->framePositions = $this->frameImages = $this->frameDurations = $this->globaldata = $this->orgvars = $this->frames = $this->fileHeader = $this->frameSources = [];
}
// Getter / Setter
// ===================================================================================
/**
* Get the total of all added frame duration
*
* @return integer
*/
public function getTotalDuration()
{
return $this->totalDuration;
}
/**
* Get the number of extracted frames
*
* @return integer
*/
public function getFrameNumber()
{
return $this->frameNumber;
}
/**
* Get the extracted frames (images and durations)
*
* @return array
*/
public function getFrames()
{
return $this->frames;
}
/**
* Get the extracted frame positions
*
* @return array
*/
public function getFramePositions()
{
return $this->framePositions;
}
/**
* Get the extracted frame dimensions
*
* @return array
*/
public function getFrameDimensions()
{
return $this->frameDimensions;
}
/**
* Get the extracted frame images
*
* @return array
*/
public function getFrameImages()
{
return $this->frameImages;
}
/**
* Get the extracted frame durations
*
* @return array
*/
public function getFrameDurations()
{
return $this->frameDurations;
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Services;
use ImageIntervention;
class ImageService
{
}

314
package-lock.json generated
View File

@@ -1,20 +1,13 @@
{
"name": "Website",
"name": "website",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/vue-fontawesome": "^3.0.2",
"@vitejs/plugin-vue": "^4.0.0",
"@vuepic/vue-datepicker": "^3.6.4",
"date-fns": "^2.29.3",
"dotenv": "^16.0.3",
"element-plus": "^2.2.27",
"normalize.css": "^8.0.1",
"pinia": "^2.0.28",
"pinia-plugin-persistedstate": "^3.0.1",
@@ -32,7 +25,6 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"axios": "^1.1.2",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jsdoc": "^39.6.4",
@@ -56,22 +48,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==",
"engines": {
"node": ">=10"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.0.10.tgz",
"integrity": "sha512-ygEZ1mwPjcPo/OulhzLE7mtDrQBWI8vZzEWSNB2W/RNCRjoQGwbaK4N8lV4rid7Ts4qvySU3njMN7YCiSlSaTQ==",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@es-joy/jsdoccomment": {
"version": "0.36.1",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz",
@@ -439,85 +415,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@floating-ui/core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.1.tgz",
"integrity": "sha512-PL7g3dhA4dHgZfujkuD8Q+tfJJynEtnNQSPzmucCnxMvkxf4cLBJw/ZYqZUn4HCh33U3WHrAfv2R2tbi9UCSmw=="
},
"node_modules/@floating-ui/dom": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
"integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
"dependencies": {
"@floating-ui/core": "^1.1.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz",
"integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz",
"integrity": "sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.2.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.2.1.tgz",
"integrity": "sha512-L8l4MfdHPmZlJ72PvzdfwOwbwcCAL0vx48tJRnI6u1PJXh+j2f3yDoKyQgO3qjEsgD5Fr2tQV/cPP8F/k6aUig==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.2.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.2.1.tgz",
"integrity": "sha512-wiqcNDNom75x+pe88FclpKz7aOSqS2lOivZeicMV5KRwOAeypxEYWAK/0v+7r+LrEY30+qzh8r2XDaEHvoLsMA==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.2.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz",
"integrity": "sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.2.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/vue-fontawesome": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.3.tgz",
"integrity": "sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==",
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"vue": ">= 3.0.0 < 4"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -644,16 +541,6 @@
"node": ">= 8"
}
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@types/eslint": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.0.tgz",
@@ -685,19 +572,6 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
},
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
},
"node_modules/@types/lodash-es": {
"version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz",
"integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@@ -710,11 +584,6 @@
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz",
@@ -1037,39 +906,6 @@
"vue": ">=3.2.0"
}
},
"node_modules/@vueuse/core": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.12.0.tgz",
"integrity": "sha512-h/Di8Bvf6xRcvS/PvUVheiMYYz3U0tH3X25YxONSaAUBa841ayMwxkuzx/DGUMCW/wHWzD8tRy2zYmOC36r4sg==",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.12.0",
"@vueuse/shared": "9.12.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/metadata": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.12.0.tgz",
"integrity": "sha512-9oJ9MM9lFLlmvxXUqsR1wLt1uF7EVbP5iYaHJYqk+G2PbMjY6EXvZeTjbdO89HgoF5cI6z49o2zT/jD9SVoNpQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.12.0.tgz",
"integrity": "sha512-TWuJLACQ0BVithVTRbex4Wf1a1VaRuSpVeyEd4vMUWl54PzlE0ciFUshKCXnlLuD0lxIaLK4Ypj3NXYzZh4+SQ==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -1331,28 +1167,6 @@
"node": ">=8"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/axios": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.1.tgz",
"integrity": "sha512-78pWJsQTceInlyaeBQeYZ/QgZeWS8hGeKiIJiDKQe3hEyBb7sEMq0K4gjx+Va6WHTYO4zI/RRl8qGRzn0YMadA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1538,18 +1352,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -1622,11 +1424,6 @@
"date-fns": ">=2.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -1650,15 +1447,6 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1702,31 +1490,6 @@
"integrity": "sha512-47o4PPgxfU1KMNejz+Dgaodf7YTcg48uOfV1oM6cs3adrl2+7R+dHkt3Jpxqo0LRCbGJEzTKMUt0RdvByb/leg==",
"peer": true
},
"node_modules/element-plus": {
"version": "2.2.29",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.29.tgz",
"integrity": "sha512-g4dcrURrKkR5uUX8n5RVnnqGnimoki9HfqS4yHHG6XwCHBkZGozdq4x+478BzeWUe31h++BO+7dakSx4VnM8RQ==",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.3",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
@@ -1799,11 +1562,6 @@
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -2195,40 +1953,6 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2638,22 +2362,8 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -2681,11 +2391,6 @@
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -2718,6 +2423,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -2726,6 +2432,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -2794,11 +2501,6 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
},
"node_modules/normalize.css": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
@@ -3034,12 +2736,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",

View File

@@ -9,7 +9,6 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"axios": "^1.1.2",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-jsdoc": "^39.6.4",
@@ -22,16 +21,9 @@
"vite": "^4.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/vue-fontawesome": "^3.0.2",
"@vitejs/plugin-vue": "^4.0.0",
"@vuepic/vue-datepicker": "^3.6.4",
"date-fns": "^2.29.3",
"dotenv": "^16.0.3",
"element-plus": "^2.2.27",
"normalize.css": "^8.0.1",
"pinia": "^2.0.28",
"pinia-plugin-persistedstate": "^3.0.1",

BIN
public/img/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -43,50 +43,31 @@ h1 {
font-weight: 800;
}
label {
display: block;
margin-bottom: map-get($spacer, 1);
&.required:after {
content: " *";
color: $danger-color;
}
&.inline {
display: inline-block;
margin-right: map-get($spacer, 3);
&:after {
content: ":";
}
}
}
p {
line-height: 1.5rem;
margin-top: 0;
}
input,
select,
textarea {
box-sizing: border-box;
display: block;
width: 100%;
border: 1px solid $border-color;
border-radius: 12px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $font-color;
margin-bottom: map-get($spacer, 4);
// input,
// select,
// textarea {
// box-sizing: border-box;
// display: block;
// width: 100%;
// border: 1px solid $border-color;
// border-radius: 12px;
// padding: map-get($spacer, 2) map-get($spacer, 3);
// color: $font-color;
// margin-bottom: map-get($spacer, 4);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
// -webkit-appearance: none;
// -moz-appearance: none;
// appearance: none;
// }
textarea {
resize: none;
}
// textarea {
// resize: none;
// }
select {
padding-right: 2.5rem;
@@ -178,39 +159,6 @@ code {
}
}
/* Loader */
.loader-cover {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
bottom: 0;
right: 0;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(4px);
background-color: rgba(255, 255, 255, 0.5);
.loader {
display: flex;
flex-direction: column;
padding: map-get($spacer, 5) calc(map-get($spacer, 5) * 2);
border: 1px solid transparent;
border-radius: 24px;
svg {
font-size: calc(map-get($spacer, 5) * 1.5);
}
span {
font-size: map-get($spacer, 4);
padding-top: map-get($spacer, 3);
}
}
}
/* Button */
button.button,
a.button,
@@ -301,53 +249,6 @@ label.button {
}
}
/* Form Group */
.form-group {
margin-bottom: map-get($spacer, 3);
padding: 0 4px;
flex: 1;
input,
textarea {
margin-bottom: map-get($spacer, 1);
}
.form-group-info {
font-size: 85%;
margin-bottom: map-get($spacer, 1);
}
.form-group-error {
// display: none;
font-size: 85%;
margin-bottom: map-get($spacer, 1);
color: $danger-color;
}
.form-group-help {
font-size: 85%;
margin-bottom: map-get($spacer, 1);
color: $secondary-color;
svg {
vertical-align: middle !important;
}
}
&.has-error {
input,
textarea,
.input-file-group,
.input-media-group .input-media-display {
border: 2px solid $danger-color;
}
.form-group-error {
display: block;
}
}
}
/* Page Errors */
.page-error {
display: flex;

View File

@@ -125,33 +125,33 @@
/* Margin */
@each $index, $size in $spacer {
.m-#{$index} {
margin: #{$size};
margin: #{$size} !important;
}
.mt-#{$index} {
margin-top: #{$size};
margin-top: #{$size} !important;
}
.mb-#{$index} {
margin-bottom: #{$size};
margin-bottom: #{$size} !important;
}
.ml-#{$index} {
margin-left: #{$size};
margin-left: #{$size} !important;
}
.mr-#{$index} {
margin-right: #{$size};
margin-right: #{$size} !important;
}
.mx-#{$index} {
margin-left: #{$size};
margin-right: #{$size};
margin-left: #{$size} !important;
margin-right: #{$size} !important;
}
.my-#{$index} {
margin-top: #{$size};
margin-bottom: #{$size};
margin-top: #{$size} !important;
margin-bottom: #{$size} !important;
}
}

View File

@@ -1,4 +1,4 @@
import _ from 'lodash';
import _ from "lodash";
window._ = _;
/**
@@ -7,10 +7,10 @@ window._ = _;
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
// import axios from 'axios';
// window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening

View File

@@ -11,7 +11,7 @@
]"
:type="buttonType">
{{ label }}
<font-awesome-icon v-if="icon" :icon="icon" />
<ion-icon v-if="icon" :icon="icon" />
</a>
<button
v-else-if="to == null"
@@ -24,7 +24,7 @@
]"
:type="buttonType">
{{ label }}
<font-awesome-icon v-if="icon" :icon="icon" />
<ion-icon v-if="icon" :icon="icon" />
</button>
<router-link
v-else
@@ -37,7 +37,7 @@
{ 'button-block': block },
]">
{{ label }}
<font-awesome-icon v-if="icon" :icon="icon" />
<ion-icon v-if="icon" :icon="icon" />
</router-link>
</template>
@@ -83,5 +83,11 @@ const classType = props.type == "submit" ? "primary" : props.type;
display: block;
width: 100%;
}
ion-icon {
height: 1.2rem;
width: 1.2rem;
vertical-align: middle;
}
}
</style>

View File

@@ -7,22 +7,20 @@
<slot></slot>
</div>
<div class="carousel-slide-prev" @click="handleSlidePrev">
<font-awesome-icon icon="fa-solid fa-chevron-left" />
<ion-icon name="chevron-back-outline" />
</div>
<div class="carousel-slide-next" @click="handleSlideNext">
<font-awesome-icon icon="fa-solid fa-chevron-right" />
<ion-icon name="chevron-forward-outline" />
</div>
<div class="carousel-slide-indicators">
<div
v-for="(indicator, index) in slideElements"
:key="index"
class="carousel-slide-indicator-dot">
<font-awesome-icon
v-if="currentSlide != index"
icon="fa-regular fa-circle"
@click="handleIndicator(index)" />
<font-awesome-icon v-else icon="fa-solid fa-circle" />
</div>
:class="[
'carousel-slide-indicator-item',
{ highlighted: currentSlide == index },
]"
@click="handleIndicator(index)"></div>
</div>
</div>
</template>
@@ -184,12 +182,20 @@ const disconnectMutationObserver = () => {
opacity: 0.75;
transition: opacity 0.2s ease-in-out;
svg {
.carousel-slide-indicator-item {
height: map-get($spacer, 1);
width: map-get($spacer, 1);
border: 1px solid white;
border-radius: 50%;
cursor: pointer;
font-size: 80%;
padding: 0 0.25rem;
margin: 0 calc(#{map-get($spacer, 1)} / 3);
color: #fff;
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
&.highlighted {
background-color: white;
}
}
}
}

View File

@@ -3,7 +3,7 @@
class="carousel-slide"
:style="{ backgroundImage: `url('${imageUrl}')` }">
<div v-if="imageUrl == null" class="carousel-slide-loading">
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
</div>
<div v-else class="carousel-slide-body">
<div class="carousel-slide-content">
@@ -20,9 +20,10 @@
</template>
<script setup lang="ts">
import axios from "axios";
import { ref } from "vue";
import { api } from "../helpers/api";
import SMButton from "./SMButton.vue";
import SMLoadingIcon from "./SMLoadingIcon.vue";
const props = defineProps({
title: {
@@ -56,9 +57,9 @@ let imageUrl = ref(null);
const handleLoad = async () => {
try {
let result = await axios.get(`media/${props.image}`);
if (result.data.medium) {
imageUrl.value = result.data.medium.url;
let result = await api.get(`/media/${props.image}`);
if (result.json.medium) {
imageUrl.value = result.json.medium.url;
}
} catch (error) {
imageUrl.value = "";

View File

@@ -1,5 +1,5 @@
<template>
<div :class="['container', { full: isFull }]">
<div :class="['container', { full: isFull }]" :style="styleObject">
<SMLoader :loading="loading">
<d-error-forbidden
v-if="pageError == 403 || !hasPermission()"></d-error-forbidden>
@@ -50,9 +50,19 @@ const props = defineProps({
default: false,
required: false,
},
background: {
type: String,
default: "",
required: false,
},
});
const slots = useSlots();
const userStore = useUserStore();
let styleObject = {};
if (props.background != "") {
styleObject["backgroundImage"] = `url('${props.background}')`;
}
const hasPermission = () => {
return (
@@ -76,6 +86,9 @@ const isFull = computed(() => {
width: 100%;
max-width: 1200px;
margin: 0 auto;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
&.full {
padding-left: 0;

View File

@@ -20,7 +20,7 @@
<slot></slot>
</div>
<div v-if="help" class="form-group-help">
<font-awesome-icon v-if="helpIcon" :icon="helpIcon" />
<!-- <font-awesome-icon v-if="helpIcon" :icon="helpIcon" /> -->
{{ help }}
</div>
</div>

View File

@@ -8,7 +8,7 @@
<transition name="fade">
<div v-if="loading" class="dialog-loading-cover">
<div class="dialog-loading">
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
<span>{{ loadingMessage }}</span>
</div>
</div>
@@ -18,6 +18,8 @@
</template>
<script setup lang="ts">
import SMLoadingIcon from "./SMLoadingIcon.vue";
defineProps({
loading: {
type: Boolean,
@@ -50,6 +52,7 @@ defineProps({
border-radius: 24px;
overflow: hidden;
min-width: map-get($spacer, 5) * 12;
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
& > h1 {
margin-top: 0;

View File

@@ -5,33 +5,33 @@
<ul>
<li>
<a href="https://facebook.com/stemmechanics"
><font-awesome-icon icon="fa-brands fa-facebook"
/></a>
><ion-icon name="logo-facebook"></ion-icon
></a>
</li>
<li>
<a href="https://mastodon.au/@stemmechanics"
><font-awesome-icon icon="fa-brands fa-mastodon"
/></a>
><ion-icon name="logo-mastodon"></ion-icon
></a>
</li>
<li>
<a href="https://www.youtube.com/@stemmechanics"
><font-awesome-icon icon="fa-brands fa-youtube"
/></a>
><ion-icon name="logo-youtube"></ion-icon
></a>
</li>
<li>
<a href="https://twitter.com/stemmechanics"
><font-awesome-icon icon="fa-brands fa-twitter"
/></a>
><ion-icon name="logo-twitter"></ion-icon
></a>
</li>
<li>
<a href="https://github.com/stemmechanics"
><font-awesome-icon icon="fa-brands fa-github"
/></a>
><ion-icon name="logo-github"></ion-icon
></a>
</li>
<li>
<a href="https://discord.gg/yNzk4x7mpD"
><font-awesome-icon icon="fa-brands fa-discord"
/></a>
><ion-icon name="logo-discord"></ion-icon
></a>
</li>
</ul>
</SMColumn>

View File

@@ -0,0 +1,36 @@
<template>
<form @submit.prevent="handleSubmit">
<SMLoader :loading="props.modelValue._loading"></SMLoader>
<SMMessage
v-if="props.modelValue._message.length > 0"
:message="props.modelValue._message"
:type="props.modelValue._messageType"
:icon="props.modelValue._messageIcon" />
<slot></slot>
</form>
</template>
<script setup lang="ts">
import { provide } from "vue";
import SMLoader from "../components/SMLoader.vue";
import SMMessage from "./SMMessage.vue";
const props = defineProps({
modelValue: {
type: Object,
required: true,
},
});
const emits = defineEmits(["submit"]);
const handleSubmit = function () {
if (props.modelValue.validate()) {
emits("submit");
}
};
provide("form", props.modelValue);
</script>
<style lang="scss"></style>

View File

@@ -1,26 +1,25 @@
<template>
<div class="form-footer">
<div class="form-footer-column form-footer-column-left">
<div class="sm-form-footer">
<div class="sm-form-footer-column sm-form-footer-column-left">
<slot name="left"></slot>
</div>
<div class="form-footer-column form-footer-column-right">
<div class="sm-form-footer-column sm-form-footer-column-right">
<slot name="right"></slot>
</div>
</div>
</template>
<style lang="scss">
.form-footer {
.sm-form-footer {
display: flex;
flex: 1;
// margin-bottom: map-get($spacer, 3);
.form-footer-column {
.sm-form-footer-column {
display: flex;
align-items: center;
&.form-footer-column-left,
&.form-footer-column-right {
&.sm-form-footer-column-left,
&.sm-form-footer-column-right {
a,
button {
margin-left: map-get($spacer, 1);
@@ -36,7 +35,7 @@
}
}
&.form-footer-column-right {
&.sm-form-footer-column-right {
flex: 1;
justify-content: flex-end;
}
@@ -44,12 +43,12 @@
}
@media screen and (max-width: 768px) {
.form-footer {
.sm-form-footer {
flex-direction: column-reverse;
.form-footer-column {
&.form-footer-column-left,
&.form-footer-column-right {
.sm-form-footer-column {
&.sm-form-footer-column-left,
&.sm-form-footer-column-right {
display: flex;
flex-direction: column-reverse;
justify-content: center;
@@ -66,11 +65,11 @@
}
}
&.form-footer-column-left {
&.sm-form-footer-column-left {
margin-bottom: -#{calc(map-get($spacer, 1) / 2)};
}
&.form-footer-column-right {
&.sm-form-footer-column-right {
margin-top: -#{calc(map-get($spacer, 1) / 2)};
}
}

View File

@@ -4,13 +4,13 @@
v-if="back != ''"
:to="{ name: back }"
class="heading-back">
<font-awesome-icon icon="fa-solid fa-arrow-left" />{{ backLabel }}
<ion-icon name="arrow-back-outline" />{{ backLabel }}
</router-link>
<router-link v-if="close != ''" :to="{ name: close }" class="close">
<font-awesome-icon icon="fa-solid fa-close" />
<ion-icon name="close-outline" />
</router-link>
<span v-if="closeBack" class="close" @click="handleBack">
<font-awesome-icon icon="fa-solid fa-close" />
<ion-icon name="close-outline" />
</span>
<h1>{{ heading }}</h1>
</div>

View File

@@ -1,29 +1,39 @@
<template>
<div :class="['form-group', { 'has-error': error }]">
<label v-if="label" :class="{ required: required, inline: inline }">{{
label
}}</label>
<div
:class="[
'sm-input-group',
{
'sm-input-active': inputActive,
'sm-feedback-invalid': feedbackInvalid,
},
]">
<label v-if="label">{{ label }}</label>
<ion-icon
class="sm-invalid-icon"
name="alert-circle-outline"></ion-icon>
<input
v-if="
type == 'text' ||
type == 'email' ||
type == 'password' ||
type == 'email' ||
type == 'url'
"
:type="type"
:value="modelValue"
:placeholder="placeholder"
@input="input"
:value="value"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleBlur" />
@keydown="handleKeydown" />
<textarea
v-if="type == 'textarea'"
rows="5"
:value="modelValue"
:placeholder="placeholder"
@input="input"
:value="value"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleBlur"></textarea>
@keydown="handleKeydown"></textarea>
<div v-if="type == 'file'" class="input-file-group">
<input
id="file"
@@ -40,37 +50,31 @@
props.modelValue
}}</a>
<span v-if="type == 'static'">{{ props.modelValue }}</span>
<div v-if="type == 'media'" class="input-media-group">
<div class="input-media-display">
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
<font-awesome-icon v-else icon="fa-regular fa-image" />
</div>
<div v-if="type == 'media'" class="form-group-error">
{{ error }}
</div>
<a class="button" @click.prevent="handleMediaSelect">Select file</a>
</div>
<div v-if="type != 'media'" class="form-group-error">{{ error }}</div>
<div v-if="slots.default" class="form-group-info">
<slot></slot>
<div v-if="slots.default || feedbackInvalid" class="sm-input-help">
<span v-if="feedbackInvalid" class="sm-input-invalid">{{
feedbackInvalid
}}</span>
<span v-if="slots.default" class="sm-input-info">
<slot></slot>
</span>
</div>
<div v-if="help" class="form-group-help">
<font-awesome-icon v-if="helpIcon" :icon="helpIcon" />
<ion-icon v-if="helpIcon" name="information-circle-outline" />
{{ help }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots, ref, watch } from "vue";
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
import { openDialog } from "vue3-promise-dialog";
import axios from "axios";
import { watch, computed, useSlots, ref, inject } from "vue";
import { toTitleCase } from "../helpers/string";
import { isEmpty } from "../helpers/utils";
const props = defineProps({
modelValue: {
type: String,
default: "",
required: false,
},
label: {
type: String,
@@ -90,7 +94,7 @@ const props = defineProps({
type: String,
default: "text",
},
error: {
feedbackInvalid: {
type: String,
default: "",
},
@@ -106,100 +110,215 @@ const props = defineProps({
type: String,
default: "",
},
href: {
control: {
type: String,
default: "",
},
form: {
type: Object,
default: () => {
return {};
},
required: false,
},
});
const emits = defineEmits(["update:modelValue", "blur"]);
const emits = defineEmits(["update:modelValue", "focus", "blur", "keydown"]);
const slots = useSlots();
const mediaUrl = ref("");
const objForm = inject("form", props.form);
const objControl =
!isEmpty(objForm) && props.control != "" ? objForm[props.control] : null;
const label = ref("");
const feedbackInvalid = ref("");
watch(
() => props.label,
(newValue) => {
label.value = newValue;
}
);
const value = ref(props.modelValue);
if (objControl) {
if (value.value.length > 0) {
objControl.value = value.value;
} else {
value.value = objControl.value;
}
if (label.value.length == 0) {
label.value = toTitleCase(props.control);
}
watch(
() => objControl.validation.result.valid,
(newValue) => {
feedbackInvalid.value = newValue
? ""
: objControl.validation.result.invalidMessages[0];
},
{ deep: true }
);
}
watch(
() => props.modelValue,
(newValue) => {
value.value = newValue;
}
);
watch(
() => props.feedbackInvalid,
(newValue) => {
feedbackInvalid.value = newValue;
}
);
const inputActive = ref(value.value.length > 0);
const handleChange = (event) => {
emits("update:modelValue", event.target.files[0]);
};
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
value.value = target.value;
emits("update:modelValue", target.value);
if (objControl) {
objControl.value = target.value;
feedbackInvalid.value = "";
}
};
const handleFocus = (event: Event) => {
inputActive.value = true;
if (event instanceof KeyboardEvent) {
if (event.key === undefined || event.key === "Tab") {
emits("blur", event);
}
}
emits("focus", event);
};
const handleBlur = (event: Event) => {
if (objControl) {
objForm.validate(props.control);
feedbackInvalid.value = objForm[props.control].validation.result.valid
? ""
: objForm[props.control].validation.result.invalidMessages[0];
}
const target = event.target as HTMLInputElement;
if (target.value.length == 0) {
inputActive.value = false;
}
emits("blur", event);
};
const input = (event) => {
emits("update:modelValue", event.target.value);
};
const handleBlur = (event) => {
if (event.keyCode == undefined || event.keyCode == 9) {
emits("blur", event);
}
};
const handleMediaSelect = async (event) => {
let result = await openDialog(SMDialogMedia);
console.log(result);
if (result) {
mediaUrl.value = result.url;
emits("update:modelValue", result.id);
}
const handleKeydown = (event: Event) => {
emits("keydown", event);
};
const inline = computed(() => {
return ["static", "link"].includes(props.type);
});
const handleLoad = async () => {
if (props.type == "media" && props.modelValue.length > 0) {
try {
let result = await axios.get(`media/${props.modelValue}`);
mediaUrl.value = result.data.medium.url;
} catch (error) {
/* empty */
}
}
};
watch(
() => props.modelValue,
() => {
handleLoad();
}
);
</script>
<style lang="scss">
.input-media-group {
.sm-input-group {
position: relative;
display: flex;
margin: 0 auto;
max-width: 26rem;
flex-direction: column;
align-items: center;
margin-bottom: map-get($spacer, 4);
.input-media-display {
display: flex;
margin-bottom: 1rem;
&.sm-input-active {
label {
transform: translate(8px, -3px) scale(0.7);
color: $secondary-color-dark;
}
input {
padding: calc(#{map-get($spacer, 2)} * 1.5) map-get($spacer, 3)
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
}
textarea {
padding: calc(#{map-get($spacer, 2)} * 2) map-get($spacer, 3)
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
}
}
&.sm-feedback-invalid {
input,
select,
textarea {
border: 2px solid $danger-color;
}
.sm-invalid-icon {
display: block;
}
}
label {
position: absolute;
display: block;
padding: map-get($spacer, 2) map-get($spacer, 3);
line-height: 1.5;
transform-origin: top left;
transform: translate(0, 1px) scale(1);
transition: all 0.1s ease-in-out;
color: $font-color;
pointer-events: none;
}
.sm-invalid-icon {
position: absolute;
display: none;
right: 0;
top: 2px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $danger-color;
font-size: 120%;
}
input,
select,
textarea {
box-sizing: border-box;
display: block;
width: 100%;
border: 1px solid $border-color;
background-color: #fff;
border-radius: 12px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $font-color;
margin-bottom: map-get($spacer, 1);
img {
max-width: 100%;
max-height: 100%;
}
svg {
padding: 4rem;
}
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.button {
max-width: 13rem;
textarea {
resize: none;
}
}
.input-media-group + .form-group-error {
text-align: center;
}
.sm-input-help {
font-size: 75%;
margin: 0 map-get($spacer, 1);
@media screen and (max-width: 768px) {
.input-media-group {
max-width: 13rem;
.sm-input-invalid {
color: $danger-color;
padding-right: map-get($spacer, 1);
}
}
}
</style>

View File

@@ -1,10 +1,8 @@
<template>
<template v-if="loading">
<transition name="fade">
<div v-if="loading" class="loader-cover">
<div class="loader">
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
</div>
<div v-if="loading" class="sm-loader">
<SMLoadingIcon />
</div>
</transition>
</template>
@@ -12,6 +10,8 @@
</template>
<script setup lang="ts">
import SMLoadingIcon from "./SMLoadingIcon.vue";
defineProps({
loading: {
type: Boolean,
@@ -19,3 +19,20 @@ defineProps({
},
});
</script>
<style lang="scss">
.sm-loader {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
bottom: 0;
right: 0;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(4px);
background-color: rgba(255, 255, 255, 0.5);
z-index: 10000;
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="sm-loading-icon">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</template>
<style lang="scss">
.sm-loading-icon {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.sm-loading-icon div {
position: absolute;
top: 33px;
width: 13px;
height: 13px;
border-radius: 50%;
background: #000;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.sm-loading-icon div:nth-child(1) {
left: 8px;
animation: sm-loading-icon1 0.6s infinite;
}
.sm-loading-icon div:nth-child(2) {
left: 8px;
animation: sm-loading-icon2 0.6s infinite;
}
.sm-loading-icon div:nth-child(3) {
left: 32px;
animation: sm-loading-icon2 0.6s infinite;
}
.sm-loading-icon div:nth-child(4) {
left: 56px;
animation: sm-loading-icon3 0.6s infinite;
}
@keyframes sm-loading-icon1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes sm-loading-icon3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes sm-loading-icon2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
</style>

View File

@@ -0,0 +1,360 @@
<template>
<div
:class="[
'sm-input-group',
{ 'sm-input-active': inputActive, 'sm-has-error': error },
]">
<label v-if="label" :class="{ required: required, inline: inline }">{{
label
}}</label>
<ion-icon class="sm-error-icon" name="alert-circle-outline"></ion-icon>
<input
v-if="
type == 'text' ||
type == 'email' ||
type == 'password' ||
type == 'email' ||
type == 'url'
"
:type="type"
:value="modelValue"
:placeholder="placeholder"
@input="input"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeydown" />
<textarea
v-if="type == 'textarea'"
rows="5"
:value="modelValue"
:placeholder="placeholder"
@input="input"
@blur="handleBlur"
@keydown="handleBlur"></textarea>
<div v-if="type == 'file'" class="input-file-group">
<input
id="file"
type="file"
class="file"
:accept="props.accept"
@change="handleChange" />
<label class="button" for="file">Select file</label>
<div class="file-name">
{{ modelValue?.name ? modelValue.name : modelValue }}
</div>
</div>
<a v-if="type == 'link'" :href="href" target="_blank">{{
props.modelValue
}}</a>
<span v-if="type == 'static'">{{ props.modelValue }}</span>
<div v-if="type == 'media'" class="input-media-group">
<div class="input-media-display">
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
<ion-icon v-else name="image-outline" />
</div>
<div v-if="type == 'media'" class="form-group-error">
{{ error }}
</div>
<a class="button" @click.prevent="handleMediaSelect">Select file</a>
</div>
<div v-if="slots.default || error" class="sm-input-help">
<span v-if="type != 'media'" class="sm-input-error">{{
error
}}</span>
<span v-if="slots.default" class="sm-input-info">
<slot></slot>
</span>
</div>
<div v-if="help" class="form-group-help">
<ion-icon v-if="helpIcon" name="information-circle-outline" />
{{ help }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots, ref, watch } from "vue";
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
import { openDialog } from "vue3-promise-dialog";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
label: {
type: String,
default: "",
required: false,
},
placeholder: {
type: String,
default: "",
required: false,
},
required: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "text",
},
error: {
type: String,
default: "",
},
help: {
type: String,
default: "",
},
helpIcon: {
type: String,
default: "",
},
accept: {
type: String,
default: "",
},
href: {
type: String,
default: "",
},
});
const emits = defineEmits(["update:modelValue", "blur"]);
const slots = useSlots();
const mediaUrl = ref("");
let inputActive = ref(false);
const handleChange = (event) => {
emits("update:modelValue", event.target.files[0]);
emits("blur", event);
};
const input = (event) => {
emits("update:modelValue", event.target.value);
};
const handleBlur = (event) => {
if (props.modelValue.length == 0) {
inputActive.value = false;
}
if (event.keyCode == undefined || event.keyCode == 9) {
emits("blur", event);
}
};
const handleFocus = (event) => {
inputActive.value = true;
if (event.keyCode == undefined || event.keyCode == 9) {
emits("blur", event);
}
};
const handleKeydown = (event) => {};
const handleMediaSelect = async (event) => {
let result = await openDialog(SMDialogMedia);
if (result) {
mediaUrl.value = result.url;
emits("update:modelValue", result.id);
}
};
const inline = computed(() => {
return ["static", "link"].includes(props.type);
});
const handleLoad = async () => {
if (props.type == "media" && props.modelValue.length > 0) {
try {
let result = await api.get(`/media/${props.modelValue}`);
mediaUrl.value = result.json.medium.url;
} catch (error) {
/* empty */
}
}
};
watch(
() => props.modelValue,
() => {
handleLoad();
}
);
</script>
<style lang="scss">
.sm-input-group {
position: relative;
display: flex;
flex-direction: column;
margin-bottom: map-get($spacer, 4);
&.sm-input-active {
label {
transform: translate(8px, -3px) scale(0.7);
color: $secondary-color-dark;
}
input {
padding: calc(#{map-get($spacer, 2)} * 1.5) map-get($spacer, 3)
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
}
}
&.sm-has-error {
input,
select,
textarea {
border: 2px solid $danger-color;
}
.sm-error-icon {
display: block;
}
}
label {
position: absolute;
display: block;
padding: map-get($spacer, 2) map-get($spacer, 3);
line-height: 1.5;
transform-origin: top left;
transform: translate(0, 1px) scale(1);
transition: all 0.1s ease-in-out;
color: $font-color;
}
.sm-error-icon {
position: absolute;
display: none;
right: 0;
top: 2px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $danger-color;
font-size: 120%;
}
input,
select,
textarea {
box-sizing: border-box;
display: block;
width: 100%;
border: 1px solid $border-color;
border-radius: 12px;
padding: map-get($spacer, 2) map-get($spacer, 3);
color: $font-color;
margin-bottom: map-get($spacer, 1);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
textarea {
resize: none;
}
.sm-input-help {
font-size: 75%;
margin: 0 map-get($spacer, 1);
.sm-input-error {
color: $danger-color;
padding-right: map-get($spacer, 1);
}
}
}
// .form-group {
// margin-bottom: map-get($spacer, 3);
// padding: 0 4px;
// flex: 1;
// input,
// textarea {
// margin-bottom: map-get($spacer, 1);
// }
// label {
// position: absolute;
// }
// .form-group-info {
// font-size: 85%;
// margin-bottom: map-get($spacer, 1);
// }
// .form-group-error {
// // display: none;
// font-size: 85%;
// margin-bottom: map-get($spacer, 1);
// color: $danger-color;
// }
// .form-group-help {
// font-size: 85%;
// margin-bottom: map-get($spacer, 1);
// color: $secondary-color;
// svg {
// vertical-align: middle !important;
// }
// }
// &.has-error {
// input,
// textarea,
// .input-file-group,
// .input-media-group .input-media-display {
// border: 2px solid $danger-color;
// }
// .form-group-error {
// display: block;
// }
// }
// }
// .input-media-group {
// display: flex;
// margin: 0 auto;
// max-width: 26rem;
// flex-direction: column;
// align-items: center;
// .input-media-display {
// display: flex;
// margin-bottom: 1rem;
// border: 1px solid $border-color;
// background-color: #fff;
// img {
// max-width: 100%;
// max-height: 100%;
// }
// svg {
// padding: 4rem;
// }
// }
// .button {
// max-width: 13rem;
// }
// }
// .input-media-group + .form-group-error {
// text-align: center;
// }
// @media screen and (max-width: 768px) {
// .input-media-group {
// max-width: 13rem;
// }
// }
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div :class="['message', type]">
<font-awesome-icon v-if="icon" :icon="icon" />{{ message }}
<ion-icon v-if="icon" :name="icon"></ion-icon>
<p>{{ message }}</p>
</div>
</template>
@@ -23,16 +24,13 @@ defineProps({
<style lang="scss">
.message {
display: flex;
padding: map-get($spacer, 2) map-get($spacer, 3);
margin-bottom: map-get($spacer, 4);
text-align: center;
font-size: 90%;
word-break: break-word;
svg {
padding-right: map-get($spacer, 1);
}
&.primary {
background-color: $primary-color-lighter;
color: $primary-color-darker;
@@ -53,5 +51,20 @@ defineProps({
border: 1px solid $danger-color-lighter;
border-radius: 12px;
}
ion-icon {
height: 1.3em;
width: 1.3em;
justify-content: center;
align-self: center;
}
p {
flex: 1;
margin-bottom: 0;
justify-content: center;
align-self: center;
white-space: pre-wrap;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<SMContainer
:full="true"
:class="['navbar', { showDropdown: showToggle }]"
:class="['sm-navbar', { showDropdown: showToggle }]"
@click="handleHideMenu">
<template #inner>
<div class="navbar-container">
@@ -24,12 +24,12 @@
:to="{ name: 'workshop-list' }"
class="navbar-cta"
label="Find a workshop"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
<div class="menuButton" @click.stop="handleToggleMenu">
<span>Menu</span
><font-awesome-icon
icon="fa-solid fa-bars"
class="menuButtonIcon" />
><ion-icon
class="menuButtonIcon"
name="reorder-three-outline"></ion-icon>
</div>
</div>
</template>
@@ -37,9 +37,7 @@
<ul class="navbar-dropdown">
<li class="ml-auto">
<div class="menuClose" @click.stop="handleToggleMenu">
<font-awesome-icon
icon="fa-solid fa-xmark"
class="menuCloseIcon" />
<ion-icon name="close-outline"></ion-icon>
</div>
</li>
<template v-for="item in menuItems">
@@ -47,7 +45,7 @@
v-if="item.show == undefined || item.show()"
:key="item.name">
<router-link :to="item.to"
><font-awesome-icon :icon="item.icon" />{{
><ion-icon :name="item.icon" />{{
item.label
}}</router-link
>
@@ -69,13 +67,13 @@ const menuItems = [
name: "news",
label: "News",
to: "/news",
icon: "fa-regular fa-newspaper",
icon: "newspaper-outline",
},
{
name: "workshops",
label: "Workshops",
to: "/workshops",
icon: "fa-solid fa-pen-ruler",
icon: "shapes-outline",
},
// {
// name: "courses",
@@ -87,13 +85,13 @@ const menuItems = [
name: "contact",
label: "Contact us",
to: "/contact",
icon: "fa-regular fa-envelope",
icon: "mail-outline",
},
{
name: "register",
label: "Register",
to: "/register",
icon: "fa-solid fa-pen-to-square",
icon: "person-add-outline",
show: () => !userStore.id,
inNav: false,
},
@@ -101,7 +99,7 @@ const menuItems = [
name: "login",
label: "Log in",
to: "/login",
icon: "fa-solid fa-right-to-bracket",
icon: "log-in-outline",
show: () => !userStore.id,
inNav: false,
},
@@ -109,7 +107,7 @@ const menuItems = [
name: "dashboard",
label: "Dashboard",
to: "/dashboard",
icon: "fa-regular fa-circle-user",
icon: "apps-outline",
show: () => userStore.id,
inNav: false,
},
@@ -117,7 +115,7 @@ const menuItems = [
name: "logout",
label: "Log out",
to: "/logout",
icon: "fa-solid fa-right-from-bracket",
icon: "log-out-outline",
show: () => userStore.id,
inNav: false,
},
@@ -135,7 +133,7 @@ const handleHideMenu = () => {
</script>
<style lang="scss">
.navbar {
.sm-navbar {
height: 4.5rem;
align-items: center;
justify-content: center;
@@ -143,6 +141,7 @@ const handleHideMenu = () => {
position: relative;
flex: 0 0 auto !important;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
z-index: 1000;
&.showDropdown {
.navbar-dropdown-cover {
@@ -195,8 +194,10 @@ const handleHideMenu = () => {
display: inline-block;
width: map-get($spacer, 5) * 3;
svg {
ion-icon {
padding-right: map-get($spacer, 1);
font-size: map-get($spacer, 4);
vertical-align: middle;
}
}
}
@@ -224,7 +225,7 @@ const handleHideMenu = () => {
}
}
.menuClose svg {
.menuClose ion-icon {
cursor: pointer;
font-size: map-get($spacer, 4);
padding-left: map-get($spacer, 1);
@@ -271,7 +272,7 @@ const handleHideMenu = () => {
.menuButtonIcon {
margin-left: 0.5rem;
font-size: 1.4rem;
font-size: map-get($spacer, 4);
}
}
}

View File

@@ -0,0 +1,97 @@
<template>
<div :class="['sm-page-outer', { 'sm-no-breadcrumbs': noBreadcrumbs }]">
<SMBreadcrumbs v-if="!noBreadcrumbs" />
<SMLoader :loading="loading">
<SMErrorForbidden
v-if="pageError == 403 || !hasPermission()"></SMErrorForbidden>
<SMErrorInternal
v-if="pageError >= 500 && hasPermission()"></SMErrorInternal>
<SMErrorNotFound
v-if="pageError == 404 && hasPermission()"></SMErrorNotFound>
<div
v-if="pageError < 300 && hasPermission()"
class="sm-page"
:style="styleObject">
<slot></slot>
</div>
</SMLoader>
</div>
</template>
<script setup lang="ts">
import SMLoader from "./SMLoader.vue";
import SMErrorForbidden from "./errors/Forbidden.vue";
import SMErrorInternal from "./errors/Internal.vue";
import SMErrorNotFound from "./errors/NotFound.vue";
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
import { useUserStore } from "../store/UserStore";
const props = defineProps({
pageError: {
type: Number,
default: 200,
required: false,
},
permission: {
type: String,
default: "",
required: false,
},
loading: {
type: Boolean,
default: false,
required: false,
},
background: {
type: String,
default: "",
required: false,
},
noBreadcrumbs: {
type: Boolean,
default: false,
required: false,
},
});
const userStore = useUserStore();
let styleObject = {};
if (props.background != "") {
styleObject["backgroundImage"] = `url('${props.background}')`;
}
const hasPermission = () => {
return (
props.permission.length == 0 ||
userStore.permissions.includes(props.permission)
);
};
</script>
<style lang="scss">
.sm-page-outer {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
margin-bottom: calc(map-get($spacer, 5) * 2);
&.sm-no-breadcrumbs {
margin-bottom: 0;
.sm-page {
// padding-top: calc(map-get($spacer, 5) * 2);
padding-bottom: calc(map-get($spacer, 5) * 2);
}
}
.sm-page {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
display: flex;
flex-direction: column;
flex: 1;
}
}
</style>

View File

@@ -2,7 +2,7 @@
<d-error-forbidden v-if="error == 403"></d-error-forbidden>
<d-error-internal v-if="error >= 500"></d-error-internal>
<d-error-not-found v-if="error == 404"></d-error-not-found>
<template v-if="slots.default">
<template v-if="slots.default && error < 300">
<slot></slot>
</template>
</template>

View File

@@ -11,23 +11,22 @@
{{ format(new Date(date), "MMM") }}
</div>
</div>
<font-awesome-icon
<ion-icon
v-if="hideImageLoader == false"
class="panel-image-loader"
icon="fa-regular fa-image" />
name="image-outline" />
</div>
<div class="panel-body">
<h3 class="panel-title">{{ title }}</h3>
<div v-if="showDate" class="panel-date">
<font-awesome-icon
<ion-icon
v-if="showTime == false && endDate.length == 0"
icon="fa-regular fa-calendar" /><font-awesome-icon
v-else
icon="fa-regular fa-clock" />
name="calendar-outline" />
<ion-icon v-else name="time-outline" />
<p>{{ panelDate }}</p>
</div>
<div v-if="location" class="panel-location">
<font-awesome-icon icon="fa-solid fa-location-dot" />
<ion-icon name="location-outline" />
<p>{{ location }}</p>
</div>
<div v-if="content" class="panel-content">{{ panelContent }}</div>
@@ -39,7 +38,6 @@
</template>
<script setup lang="ts">
import axios from "axios";
import { onMounted, computed, ref } from "vue";
import {
excerpt,
@@ -49,6 +47,7 @@ import {
} from "../helpers/common";
import { format } from "date-fns";
import SMButton from "./SMButton.vue";
import { api } from "../helpers/api";
const props = defineProps({
title: {
@@ -151,10 +150,10 @@ const hideImageLoader = computed(() => {
onMounted(async () => {
if (imageUrl.value && imageUrl.value.length > 0 && isUUID(imageUrl.value)) {
try {
let result = await axios.get(`media/${props.image}`);
let result = await api.get(`/media/${props.image}`);
if (result.data.medium) {
imageUrl.value = result.data.medium.url;
if (result.json.medium) {
imageUrl.value = result.json.medium.url;
}
} catch (error) {
/* empty */

View File

@@ -1,10 +1,10 @@
<template>
<div class="panel-list">
<div v-if="loading" class="panel-list-loading">
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
</div>
<div v-else-if="notFound" class="panel-list-not-found">
<font-awesome-icon icon="fa-regular fa-face-frown-open" />
<ion-icon name="alert-circle-outline" />
<p>{{ notFoundText }}</p>
</div>
<slot></slot>
@@ -12,6 +12,8 @@
</template>
<script setup lang="ts">
import SMLoadingIcon from "./SMLoadingIcon.vue";
defineProps({
loading: {
type: Boolean,

View File

@@ -19,7 +19,7 @@
<slot></slot>
</div>
<div v-if="help" class="form-group-help">
<font-awesome-icon v-if="helpIcon" :icon="helpIcon" />
<ion-icon v-if="helpIcon" name="information-circle-outline" />
{{ help }}
</div>
</div>

View File

@@ -33,7 +33,6 @@
</template>
<script setup lang="ts">
import axios from "axios";
import { useUserStore } from "../../store/UserStore";
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
import { closeDialog } from "vue3-promise-dialog";
@@ -82,15 +81,17 @@ const handleConfirm = async () => {
if (isValidated(formData)) {
try {
formLoading.value = true;
await axios.put(`users/${userStore.id}`, {
password: formData.password.value,
await api.put({
url: `/users/${userStore.id}`,
body: {
password: formData.password.value,
},
});
isSuccessful.value = true;
} catch (err) {
formData.password.error =
err.response?.data?.message ||
"An unexpected error occurred";
err.json?.message || "An unexpected error occurred";
}
}
}

View File

@@ -26,19 +26,19 @@
>Page {{ page }} of {{ totalPages }}</span
>
<span class="media-browser-page-changer">
<font-awesome-icon
<ion-icon
name="chevron-back-outline"
:class="[
'changer-button',
{ disabled: prevDisabled },
]"
icon="fa-solid fa-angle-left"
@click="handlePrev" />
<font-awesome-icon
<ion-icon
name="chevron-forward-outline"
:class="[
'changer-button',
{ disabled: nextDisabled },
]"
icon="fa-solid fa-angle-right"
@click="handleNext" />
</span>
</div>
@@ -73,7 +73,6 @@
</template>
<script setup lang="ts">
import axios from "axios";
import { computed, watch, ref, reactive, onMounted, onUnmounted } from "vue";
import { closeDialog } from "vue3-promise-dialog";
import SMButton from "../SMButton.vue";
@@ -82,6 +81,7 @@ import SMDialog from "../SMDialog.vue";
import SMMessage from "../SMMessage.vue";
import SMModal from "../SMModal.vue";
import { toParamString } from "../../helpers/common";
import { api } from "../../helpers/api";
const uploader = ref(null);
const formLoading = ref(false);
@@ -134,18 +134,18 @@ const handleLoad = async () => {
params.limit = perPage.value;
// params.fields = "url";
let res = await axios.get(`media${toParamString(params)}`);
let res = await api.get(`/media${toParamString(params)}`);
totalItems.value = res.data.total;
mediaItems.value = res.data.media;
totalItems.value = res.json.total;
mediaItems.value = res.json.media;
} catch (error) {
if (error.response.status == 404) {
if (error.status == 404) {
formMessage.type = "primary";
formMessage.icon = "fa-regular fa-folder-open";
formMessage.message = "No media items found";
} else {
formMessage.message =
error.response?.data?.message || "An unexpected error occurred";
error?.json?.message || "An unexpected error occurred";
}
}
};
@@ -165,18 +165,20 @@ const handleUpload = async () => {
if (uploader.value.files[0] instanceof File) {
submitFormData.append("file", uploader.value.files[0]);
let res = await axios.post("media", submitFormData, {
let res = await api.post({
url: "/media",
body: submitFormData,
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent) =>
(formLoadingMessage.value = `Uploading Files ${Math.floor(
(progressEvent.loaded / progressEvent.total) * 100
)}%`),
// onUploadProgress: (progressEvent) =>
// (formLoadingMessage.value = `Uploading Files ${Math.floor(
// (progressEvent.loaded / progressEvent.total) * 100
// )}%`),
});
if (res.data.medium) {
closeDialog(res.data.medium);
if (res.json.medium) {
closeDialog(res.json.medium);
} else {
formMessage.message =
"An unexpected response was received from the server";

View File

@@ -1,15 +1,21 @@
<template>
<div class="page-error forbidden">
<div class="image"></div>
<div class="content">
<h1>The cat says no!</h1>
<p>You do not have the needed access to see this page</p>
<SMPage no-breadcrumbs>
<div class="page-error forbidden">
<div class="image"></div>
<div class="content">
<h1>The cat says no!</h1>
<p>You do not have the needed access to see this page</p>
</div>
</div>
</div>
</SMPage>
</template>
<script setup lang="ts">
import SMPage from "../SMPage.vue";
</script>
<style lang="scss">
.page-error.forbidden .image {
background-image: url('/img/403.jpg');
background-image: url("/img/403.jpg");
}
</style>
</style>

View File

@@ -1,15 +1,24 @@
<template>
<div class="page-error internal">
<div class="image"></div>
<div class="content">
<h1>The cat has broken something</h1>
<p>We are working to fix that what was broken. Please try again later.</p>
<SMPage no-breadcrumbs>
<div class="page-error internal">
<div class="image"></div>
<div class="content">
<h1>The cat has broken something</h1>
<p>
We are working to fix that what was broken. Please try again
later.
</p>
</div>
</div>
</div>
</SMPage>
</template>
<script setup lang="ts">
import SMPage from "../SMPage.vue";
</script>
<style lang="scss">
.page-error.internal .image {
background-image: url('/img/500.jpg');
background-image: url("/img/500.jpg");
}
</style>
</style>

View File

@@ -1,15 +1,21 @@
<template>
<div class="page-error not-found">
<div class="image"></div>
<div class="content">
<h1>Opps</h1>
<p>The page you asked for was not found</p>
<SMPage no-breadcrumbs>
<div class="page-error not-found">
<div class="image"></div>
<div class="content">
<h1>Opps</h1>
<p>The page you asked for was not found</p>
</div>
</div>
</div>
</SMPage>
</template>
<script setup lang="ts">
import SMPage from "../SMPage.vue";
</script>
<style lang="scss">
.page-error.not-found .image {
background-image: url('/img/404.jpg');
background-image: url("/img/404.jpg");
}
</style>
</style>

190
resources/js/helpers/api.ts Normal file
View File

@@ -0,0 +1,190 @@
/* https://blog.logrocket.com/axios-vs-fetch-best-http-requests/ */
import { useUserStore } from "../store/UserStore";
interface ApiProgressData {
loaded: number;
total: number;
}
type ApiProgressCallback = (progress: ApiProgressData) => void;
interface ApiOptions {
url: string;
params?: object;
method?: string;
headers?: HeadersInit;
body?: string | object;
progress?: ApiProgressCallback;
}
interface ApiResponse {
status: number;
message: string;
data: object;
}
const apiDefaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
};
export const api = {
timeout: 8000,
baseUrl: "https://www.stemmechanics.com.au/api",
send: function (options: ApiOptions) {
return new Promise((resolve, reject) => {
let url = this.baseUrl + options.url;
if (options.params) {
url =
url +
"?" +
Object.keys(options.params)
.map((key) => key + "=" + options.params[key])
.join("&");
}
options.headers = {
...apiDefaultHeaders,
...(options.headers || {}),
};
const userStore = useUserStore();
if (userStore.id) {
options.headers["Authorization"] = `Bearer ${userStore.token}`;
}
if (options.body && typeof options.body === "object") {
options.body = JSON.stringify(options.body);
}
const fetchOptions: RequestInit = {
method: options.method || "GET",
headers: options.headers,
body: options.body,
};
let receivedData = false;
fetch(url, fetchOptions)
.then((response) => {
receivedData = true;
if (options.progress) {
if (!response.ok) {
return response;
}
if (!response.body) {
return response;
// return {
// status: 0,
// message:
// "ReadableStream not yet supported in this browser.",
// data: null,
// };
}
let contentLength =
response.headers.get("content-length");
if (!contentLength) {
contentLength = -1;
}
// parse the integer into a base-10 number
const total = parseInt(contentLength, 10);
let loaded = 0;
return new Response(
// create and return a readable stream
new ReadableStream({
start(controller) {
const reader = response.body.getReader();
read();
/**
*
*/
function read() {
reader
.read()
.then(({ done, value }) => {
if (done) {
controller.close();
return;
}
loaded += value.byteLength;
options.progress({
loaded,
total,
});
controller.enqueue(value);
read();
})
.catch((error) => {
controller.error(error);
reject({
status: 0,
message: "controller error",
data: null,
});
});
}
},
})
);
}
return response;
})
.then(async (response) => {
const data = response.json ? await response.json() : {};
const result = {
status: response.status,
statusText: response.statusText,
url: response.url,
headers: response.headers,
data: data,
};
if (response.status >= 300) {
reject(result);
}
resolve(result);
})
.catch((error) => {
console.log(error);
// Handle any errors thrown during the fetch process
const { response, ...rest } = error;
reject({
...rest,
response: response && response.json(),
});
});
});
},
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): Promise<ApiResponse> {
options.method = "POST";
return await this.send(options);
},
delete: async function (options: ApiOptions): Promise<ApiResponse> {
options.method = "DELETE";
return await this.send(options);
},
};

View File

@@ -1,5 +1,3 @@
import { format } from "date-fns";
const transitionEndEventName = () => {
var i,
undefined,
@@ -173,41 +171,6 @@ export function parseErrorType(
return def;
}
export const relativeDate = (d) => {
if (isString(d)) {
d = new Date(d);
}
// const d = new Date(0);
// // d.setUTCSeconds(parseInt(epoch));
// d.setUTCSeconds(epoch);
const now = new Date();
const dif = Math.round((now.getTime() - d.getTime()) / 1000);
if (dif < 60) {
// let v = dif;
// return v + " sec" + (v != 1 ? "s" : "") + " ago";
return "Just now";
} else if (dif < 3600) {
const v = Math.round(dif / 60);
return v + " min" + (v != 1 ? "s" : "") + " ago";
} else if (dif < 86400) {
const v = Math.round(dif / 3600);
return v + " hour" + (v != 1 ? "s" : "") + " ago";
} else if (dif < 604800) {
const v = Math.round(dif / 86400);
return v + " day" + (v != 1 ? "s" : "") + " ago";
} else if (dif < 2419200) {
const v = Math.round(dif / 604800);
return v + " week" + (v != 1 ? "s" : "") + " ago";
}
return (
monthString[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear()
);
};
export const buildUrlQuery = (url, query) => {
let s = "";
@@ -347,90 +310,6 @@ export const isUUID = (uuid) => {
);
};
export const timestampUtcToLocal = (utc) => {
try {
let iso = new Date(
utc.replace(
/([0-9]{4}-[0-9]{2}-[0-9]{2}),? ([0-9]{2}:[0-9]{2}:[0-9]{2})/,
"$1T$2.000Z"
)
);
return format(iso, "yyyy/MM/dd HH:mm:ss");
} catch (error) {
/* empty */
}
return "";
};
export const timestampLocalToUtc = (local) => {
try {
let d = new Date(local);
return d
.toISOString()
.replace(
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
"$1 $2"
);
} catch (error) {
/* empty */
}
return "";
};
export const timestampNowLocal = () => {
let d = new Date();
return (
d.getFullYear() +
"-" +
("0" + (d.getMonth() + 1)).slice(-2) +
"-"("0" + d.getDate()).slice(-2) +
" " +
("0" + d.getHours()).slice(-2) +
":" +
("0" + d.getMinutes()).slice(-2) +
":" +
("0" + d.getSeconds()).slice(-2)
);
};
export const timestampNowUtc = () => {
try {
let d = new Date();
return d
.toISOString()
.replace(
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
"$1 $2"
);
} catch (error) {
/* empty */
}
return "";
};
export const timestampBeforeNow = (timestamp) => {
try {
return new Date(timestamp) < new Date();
} catch (error) {
/* empty */
}
return false;
};
export const timestampAfterNow = (timestamp) => {
try {
return new Date(timestamp) > new Date();
} catch (error) {
/* empty */
}
return false;
};
export {
transitionEndEventName,
waitForElementRender,

View File

@@ -0,0 +1,221 @@
import { isString } from "../helpers/common";
export const dayString = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
export const fullDayString = [
"Sunday",
"Monday",
"Tueday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
export const monthString = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const fullMonthString = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export const format = (objDate: Date, format: string): string => {
const result = format;
const year = objDate.getFullYear().toString();
const month = (objDate.getMonth() + 1).toString();
const date = objDate.getDate().toString();
const day = objDate.getDay().toString();
const hour = objDate.getHours().toString();
const min = objDate.getMinutes().toString();
const sec = objDate.getSeconds().toString();
const apm = objDate.getHours() >= 12 ? "am" : "pm";
/* eslint-disable indent */
const apmhours = (
objDate.getHours() > 12
? objDate.getHours() - 12
: objDate.getHours() == 0
? 12
: objDate.getHours()
).toString();
/* eslint-enable indent */
// year
result.replace(/\byy\b/g, year.slice(-2));
result.replace(/\byyyy\b/g, year);
// month
result.replace(/\bM\b/g, month);
result.replace(/\bMM\b/g, (0 + month).slice(-2));
result.replace(/\bMMM\b/g, monthString[month]);
result.replace(/\bMMMM\b/g, fullMonthString[month]);
// day
result.replace(/\bd\b/g, date);
result.replace(/\bdd\b/g, (0 + date).slice(-2));
result.replace(/\bddd\b/g, dayString[day]);
result.replace(/\bdddd\b/g, fullDayString[day]);
// hour
result.replace(/\bH\b/g, hour);
result.replace(/\bHH\b/g, (0 + hour).slice(-2));
result.replace(/\bh\b/g, apmhours);
result.replace(/\bhh\b/g, (0 + apmhours).slice(-2));
// min
result.replace(/\bm\b/g, min);
result.replace(/\bmm\b/g, (0 + min).slice(-2));
// sec
result.replace(/\bs\b/g, sec);
result.replace(/\bss\b/g, (0 + sec).slice(-2));
// am/pm
result.replace(/\baa\b/g, apm);
return result;
};
export const timestampUtcToLocal = (utc: string): string => {
try {
const iso = new Date(
utc.replace(
/([0-9]{4}-[0-9]{2}-[0-9]{2}),? ([0-9]{2}:[0-9]{2}:[0-9]{2})/,
"$1T$2.000Z"
)
);
return format(iso, "yyyy/MM/dd HH:mm:ss");
} catch (error) {
/* empty */
}
return "";
};
export const timestampLocalToUtc = (local) => {
try {
const d = new Date(local);
return d
.toISOString()
.replace(
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
"$1 $2"
);
} catch (error) {
/* empty */
}
return "";
};
export const timestampNowLocal = () => {
const d = new Date();
return (
d.getFullYear() +
"-" +
("0" + (d.getMonth() + 1)).slice(-2) +
"-" +
("0" + d.getDate()).slice(-2) +
" " +
("0" + d.getHours()).slice(-2) +
":" +
("0" + d.getMinutes()).slice(-2) +
":" +
("0" + d.getSeconds()).slice(-2)
);
};
export const timestampNowUtc = () => {
try {
const d = new Date();
return d
.toISOString()
.replace(
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
"$1 $2"
);
} catch (error) {
/* empty */
}
return "";
};
export const timestampBeforeNow = (timestamp) => {
try {
return new Date(timestamp) < new Date();
} catch (error) {
/* empty */
}
return false;
};
export const timestampAfterNow = (timestamp) => {
try {
return new Date(timestamp) > new Date();
} catch (error) {
/* empty */
}
return false;
};
export const relativeDate = (d) => {
if (isString(d)) {
d = new Date(d);
}
// const d = new Date(0);
// // d.setUTCSeconds(parseInt(epoch));
// d.setUTCSeconds(epoch);
const now = new Date();
const dif = Math.round((now.getTime() - d.getTime()) / 1000);
if (dif < 60) {
// let v = dif;
// return v + " sec" + (v != 1 ? "s" : "") + " ago";
return "Just now";
} else if (dif < 3600) {
const v = Math.round(dif / 60);
return v + " min" + (v != 1 ? "s" : "") + " ago";
} else if (dif < 86400) {
const v = Math.round(dif / 3600);
return v + " hour" + (v != 1 ? "s" : "") + " ago";
} else if (dif < 604800) {
const v = Math.round(dif / 86400);
return v + " day" + (v != 1 ? "s" : "") + " ago";
} else if (dif < 2419200) {
const v = Math.round(dif / 604800);
return v + " week" + (v != 1 ? "s" : "") + " ago";
}
return (
monthString[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear()
);
};

View File

@@ -0,0 +1,137 @@
import {
ValidationObject,
ValidationResult,
defaultValidationResult,
createValidationResult,
} from "./validate";
export const FormObject = (controls) => {
controls.validate = function (item = null) {
const keys = item ? [item] : Object.keys(this);
let valid = true;
keys.every((key) => {
if (
typeof this[key] == "object" &&
Object.keys(this[key]).includes("validation")
) {
this[key].validation.result = this[
key
].validation.validator.validate(this[key].value);
if (!this[key].validation.result.valid) {
valid = false;
}
}
return true;
});
return valid;
};
controls._loading = false;
controls.loading = function (state = true) {
this._loading = state;
};
controls._message = "";
controls._messageType = "primary";
controls._messageIcon = "";
controls.message = function (message = "", type = "", icon = "") {
this._message = message;
if (type.length > 0) {
this._messageType = type;
}
if (icon.length > 0) {
this._messageIcon = icon;
}
};
controls.error = function (message = "") {
if (message == "") {
this.message("");
} else {
this.message(message, "error", "alert-circle-outline");
}
};
controls.apiErrors = function (apiResponse) {
let foundKeys = false;
if (apiResponse?.json?.errors) {
Object.keys(apiResponse.json.errors).forEach((key) => {
if (
typeof this[key] == "object" &&
Object.keys(this[key]).includes("validation")
) {
foundKeys = true;
this[key].validation.result = createValidationResult(
false,
apiResponse.json.errors[key]
);
}
});
}
if (foundKeys == false) {
this.error(
apiResponse?.json?.message ||
"An unknown server error occurred.\nPlease try again later."
);
}
};
return controls;
};
interface FormControlValidation {
validator: ValidationObject;
result: ValidationResult;
}
const defaultFormControlValidation: FormControlValidation = {
validator: {
validate: (): ValidationResult => {
return defaultValidationResult;
},
},
result: defaultValidationResult,
};
type FormClearValidations = () => void;
type FormSetValidation = (
valid: boolean,
message?: string | Array<string>
) => ValidationResult;
interface FormControlObject {
value: string;
validation: FormControlValidation;
clearValidations: FormClearValidations;
setValidationResult: FormSetValidation;
}
/* eslint-disable indent */
export const FormControl = (
value = "",
validator: ValidationObject | null = null
): FormControlObject => {
return {
value: value,
validation:
validator == null
? defaultFormControlValidation
: {
validator: validator,
result: defaultValidationResult,
},
clearValidations: function () {
this.validation.result = defaultValidationResult;
},
setValidationResult: createValidationResult,
};
};
/* eslint-enable indent */

View File

@@ -0,0 +1,5 @@
export const toTitleCase = (str) => {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};

View File

@@ -0,0 +1,11 @@
export const isEmpty = (obj: object | string) => {
if (obj) {
if (typeof obj === "string") {
return obj.length == 0;
} else if (typeof obj == "object" && Object.keys(obj).length === 0) {
return true;
}
}
return false;
};

View File

@@ -0,0 +1,440 @@
export interface ValidationObject {
validate: (value: string) => 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) => 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): ValidationResult {
return {
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) => 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): ValidationResult {
return {
valid:
this.type == "String"
? value.toString().length <= this.max
: parseInt(value) <= this.max,
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) => 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): ValidationResult {
return {
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) => 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): ValidationResult {
return {
valid: /^\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) => 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): ValidationResult {
return {
valid: /^(\+|00)?[0-9][0-9 \-().]{7,32}$/.test(value),
invalidMessages: [
typeof this.invalidMessage === "string"
? this.invalidMessage
: this.invalidMessage(this),
],
};
},
};
}
/**
* CUSTOM
*/
type ValidationCustomCallback = (value: string) => boolean | string;
interface ValidationCustomOptions {
callback: ValidationCustomCallback;
invalidMessage?: string | ((options: ValidationCustomOptions) => string);
}
interface ValidationCustomObject extends ValidationCustomOptions {
validate: (value: string) => ValidationResult;
}
const defaultValidationCustomOptions: ValidationCustomOptions = {
callback: () => {
return true;
},
invalidMessage: "Your Custom number is not in a supported format.",
};
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: function (value: string): ValidationResult {
const validateResult = {
valid: true,
invalidMessages: [
typeof this.invalidMessage === "string"
? this.invalidMessage
: this.invalidMessage(this),
],
};
const callbackResult =
typeof this.callback === "function"
? 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: function (value: string) {
const validationResult: ValidationResult = {
valid: true,
invalidMessages: [],
};
this.list.every((item: ValidationObject) => {
const validationItemResult = item.validate(value);
if (validationItemResult.valid == false) {
validationResult.valid = false;
validationResult.invalidMessages =
validationResult.invalidMessages.concat(
validationItemResult.invalidMessages
);
}
return true;
});
return validationResult;
},
};
};
/**
* Required
*/
interface ValidationRequiredOptions {
invalidMessage?: string | ((options: ValidationRequiredOptions) => string);
}
interface ValidationRequiredObject extends ValidationRequiredOptions {
validate: (value: string) => 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: string): ValidationResult {
return {
valid: value.length > 0,
invalidMessages:
typeof this.invalidMessage === "string"
? this.invalidMessage
: this.invalidMessage(this),
};
},
};
}

View File

@@ -3,11 +3,11 @@ import { createApp } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import Router from "@/router";
import "./axios.js";
// import "./axios.js";
import "normalize.css";
import "../css/app.scss";
import App from "./views/App.vue";
import FontAwesomeIcon from "@/helpers/fontawesome";
// import FontAwesomeIcon from "@/helpers/fontawesome";
import SMContainer from "./components/SMContainer.vue";
import SMRow from "./components/SMRow.vue";
import SMColumn from "./components/SMColumn.vue";
@@ -19,7 +19,7 @@ const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
createApp(App)
.component("FontAwesomeIcon", FontAwesomeIcon)
// .component("FontAwesomeIcon", FontAwesomeIcon)
.use(pinia)
.use(Router)
.use(PromiseDialog)

View File

@@ -1,7 +1,7 @@
import axios from "axios";
import { createWebHistory, createRouter } from "vue-router";
import { useUserStore } from "@/store/UserStore";
import { useApplicationStore } from "../store/ApplicationStore";
import { api } from "../helpers/api";
export const routes = [
{
@@ -386,10 +386,10 @@ router.beforeEach(async (to, from, next) => {
let redirect = false;
try {
let res = await axios.get("me");
userStore.setUserDetails(res.data.user);
let res = await api.get("/me");
userStore.setUserDetails(res.json.user);
} catch (err) {
if (err.response.status == 401) {
if (err.status == 401) {
userStore.clearUser();
redirect = true;
}

View File

@@ -1,4 +1,4 @@
import axios from "axios";
import { api } from "../helpers/api";
import { defineStore } from "pinia";
export interface UserDetails {
@@ -51,16 +51,16 @@ export const useUserStore = defineStore({
},
async fetchUser() {
const res = await axios.get("users/" + this.$state.id);
const res = await api.get("/users/" + this.$state.id);
this.$state.id = res.data.user.id;
this.$state.token = res.data.token;
this.$state.username = res.data.user.username;
this.$state.firstName = res.data.user.first_name;
this.$state.lastName = res.data.user.last_name;
this.$state.email = res.data.user.email;
this.$state.phone = res.data.user.phone;
this.$state.permissions = res.data.user.permissions || [];
this.$state.id = res.json.user.id;
this.$state.token = res.json.token;
this.$state.username = res.json.user.username;
this.$state.firstName = res.json.user.first_name;
this.$state.lastName = res.json.user.last_name;
this.$state.email = res.json.user.email;
this.$state.phone = res.json.user.phone;
this.$state.permissions = res.json.user.permissions || [];
},
clearUser() {

View File

@@ -1,6 +1,5 @@
<template>
<SMNavbar />
<SMBreadcrumbs />
<main>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
@@ -14,7 +13,6 @@
<script setup lang="ts">
import SMNavbar from "../components/SMNavbar.vue";
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
import SMFooter from "../components/SMFooter.vue";
import { DialogWrapper } from "vue3-promise-dialog";
</script>

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer class="page-contact">
<SMPage class="page-contact">
<SMRow break-large>
<SMColumn>
<h1 class="text-left">Contact Us</h1>
@@ -42,43 +42,20 @@
</SMColumn>
<SMColumn>
<div>
<SMDialog narrow :loading="formLoading">
<template v-if="!formDone">
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMDialog narrow>
<template v-if="!formSubmitted">
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="name" />
<SMInput control="email" type="email" />
<SMInput
v-model="formData.name.value"
name="name"
label="Name"
required
:error="formData.name.error"
@blur="fieldValidate(formData.name)" />
<SMInput
v-model="formData.email.value"
name="email"
label="Email"
required
:error="formData.email.error"
@blur="fieldValidate(formData.email)" />
<SMInput
v-model="formData.content.value"
name="content"
type="textarea"
control="content"
label="Message"
required
:error="formData.content.error"
@blur="fieldValidate(formData.content)" />
<SMCaptchaNotice />
type="textarea" />
<SMButton
type="submit"
block
label="Send Message"
icon="fa-regular fa-paper-plane" />
</form>
label="Send Message" />
</SMForm>
</template>
<template v-else>
<h1>Message Sent!</h1>
@@ -86,98 +63,65 @@
Your message as been sent to us. We will respond
as soon as we can.
</p>
<SMButton block to="/" label="Home" />
<SMButton
block
:to="{ name: 'home' }"
label="Home" />
</template>
</SMDialog>
</div>
</SMColumn>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMForm from "../components/SMForm.vue";
import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Email, Min, Required } from "../helpers/validate";
import { ref, reactive } from "vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
name: {
value: "",
error: "",
rules: {
required: true,
required_message: "A name is needed",
min: 4,
min_message: "A name needs to be is at least 4 characters",
},
},
email: {
value: "",
error: "",
rules: {
required: true,
required_message: "A email address is needed",
email: true,
email_message: "That email address does not look right",
},
},
content: {
value: "",
error: "",
rules: {
required: true,
required_message: "A message is required",
min: 8,
min_message: "The message needs to be at least %d characters",
},
},
});
const form = reactive(
FormObject({
name: FormControl("", And([Required(), Min(4)])),
email: FormControl("", And([Required(), Email()])),
content: FormControl("", And([Required(), Min(8)])),
})
);
const formSubmitted = ref(false);
useValidation(formData);
const submit = async () => {
formLoading.value = true;
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await axios.post("contact", {
name: formData.name.value,
email: formData.email.value,
await api.post({
url: "/contact",
body: {
name: form.name.value,
email: form.email.value,
captcha_token: captcha,
content: formData.content.value,
});
content: form.content.value,
},
});
formDone.value = true;
}
formSubmitted.value = true;
} catch (err) {
formLoading.value = false;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
restParseErrors(formData, [formMessage, "message"], err);
console.log(err);
form.apiErrors(err);
}
formLoading.value = false;
form.loading(false);
};
</script>

View File

@@ -1,26 +1,14 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow :loading="formLoading">
<SMDialog class="mt-5" narrow>
<template v-if="!formDone">
<h1>Email Verify</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model="formData.code.value"
name="code"
label="Code"
required
:error="formData.code.error"
@blur="fieldValidate(formData.code)" />
<SMCaptchaNotice />
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="code" />
<SMFormFooter>
<template #left>
<div>
<div class="small">
<router-link to="/resend-verify-email"
>Resend Code</router-link
>
@@ -30,10 +18,10 @@
<SMButton
type="submit"
label="Verify Code"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Email Verified!</h1>
@@ -48,7 +36,7 @@
</template>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
@@ -57,70 +45,47 @@ import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import SMPage from "../components/SMPage.vue";
import SMForm from "../components/SMForm.vue";
import { useRoute } from "vue-router";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import { And, Max, Min, Required } from "../helpers/validate";
import { useReCaptcha } from "vue-recaptcha-v3";
import { FormControl, FormObject } from "../helpers/form";
import { api } from "../helpers/api";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
code: {
value: "",
error: "",
rules: {
required: true,
required_message: "The code is needed",
min: 6,
min_message: "The code should be 6 characters",
max: 6,
max_message: "The code should be 6 characters",
},
},
});
const form = reactive(
FormObject({
code: FormControl("", And([Required(), Min(6), Max(6)])),
})
);
useValidation(formData);
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await axios.post("users/verifyEmail", {
code: formData.code.value,
await api.post({
url: "/users/verifyEmail",
body: {
code: form.code.value,
captcha_token: captcha,
});
},
});
formDone.value = true;
}
formDone.value = true;
} catch (err) {
restParseErrors(formData, [formMessage, "message"], err);
form.apiErrors(err);
}
formLoading.value = false;
form.loading(false);
};
if (useRoute().query.code !== undefined) {
formData.code.value = useRoute().query.code;
submit();
form.code.value = useRoute().query.code;
handleSubmit();
}
</script>

View File

@@ -23,7 +23,7 @@ import SMMessage from "../components/SMMessage.vue";
import SMPanelList from "../components/SMPanelList.vue";
import SMPanel from "../components/SMPanel.vue";
import { reactive } from "vue";
import axios from "axios";
import { api } from "../helpers/api";
const events = reactive([]);
@@ -39,12 +39,16 @@ const handleLoad = async () => {
formMessage.message = "";
try {
let result = await axios.get("events?limit=10");
events.value = result.data.events;
let result = await api.get({
url: "/events",
params: {
limit: 10,
},
});
events.value = result.json.events;
} catch (error) {
formMessage.message =
error.response?.data?.message ||
"Could not load any events from the server.";
error.json?.message || "Could not load any events from the server.";
}
};

View File

@@ -1,26 +1,14 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow :loading="formLoading">
<SMDialog narrow class="mt-5">
<template v-if="!formDone">
<h1>Forgot Password</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model="formData.username.value"
name="username"
label="Username"
required
:error="formData.username.error"
@blur="fieldValidate(formData.username)" />
<SMCaptchaNotice />
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="username" />
<SMFormFooter>
<template #left>
<div>
<div class="small">
<span class="pr-1">Remember?</span
><router-link :to="{ name: 'login' }"
>Log in</router-link
@@ -31,10 +19,10 @@
<SMButton
type="submit"
label="Send"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Email Sent!</h1>
@@ -51,78 +39,54 @@
</template>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Required, Min } from "../helpers/validate";
import { ref, reactive } from "vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import { useRoute } from "vue-router";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
username: {
value: "",
error: "",
rules: {
required: true,
required_message: "Your username is needed",
min: 4,
min_message: "Your username is at least %d characters",
},
},
});
const form = reactive(
FormObject({
username: FormControl("", And([Required(), Min(4)])),
})
);
useValidation(formData);
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
let res = await axios.post("users/forgotPassword", {
username: formData.username.value,
await api.post({
url: "/users/forgotPassword",
body: {
username: form.username.value,
captcha_token: captcha,
});
},
});
formDone.value = true;
}
} catch (err) {
if (err.response.status == 422) {
formDone.value = true;
} catch (error) {
if (error.status == 422) {
formDone.value = true;
} else {
restParseErrors(formData, [formMessage, "message"], err);
form.apiErrors(error);
}
}
formLoading.value = false;
form.loading(false);
};
</script>
<style lang="scss"></style>

View File

@@ -1,26 +1,14 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow :loading="formLoading">
<SMDialog narrow class="mt-5">
<template v-if="!formDone">
<h1>Forgot Username</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model:error="formData.email.error"
v-model="formData.email.value"
name="email"
label="Email"
required
@blur="fieldValidate(formData.email)" />
<SMCaptchaNotice />
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="email" />
<SMFormFooter>
<template #left>
<div>
<div class="small">
<span class="pr-1">Remember?</span
><router-link :to="{ name: 'login' }"
>Log in</router-link
@@ -31,10 +19,10 @@
<SMButton
type="submit"
label="Send"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Email Sent!</h1>
@@ -50,72 +38,52 @@
</template>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Required, Email } from "../helpers/validate";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
email: {
value: "",
error: "",
rules: {
required: true,
required_message: "An email address is required",
email: true,
},
},
});
const form = reactive(
FormObject({
email: FormControl("", And([Required(), Email()])),
})
);
useValidation(formData);
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
let res = await axios.post("users/forgotUsername", {
email: formData.email.value,
await api.post({
url: "/users/forgotUsername",
body: {
email: form.email.value,
captcha_token: captcha,
});
},
});
formDone.value = true;
}
} catch (err) {
restParseErrors(formData, [formMessage, "message"], err);
formDone.value = true;
} catch (error) {
form.apiErrors(error);
}
formLoading.value = false;
form.loading(false);
};
</script>
<style lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer full class="home">
<SMPage full class="home">
<SMCarousel>
<SMCarouselSlide
v-for="(slide, index) in slides"
@@ -110,100 +110,85 @@
Sign up for our mailing list to receive expert tips and tricks,
as well as updates on upcoming workshops.
</p>
<SMDialog :loading="formLoading" class="p-0">
<form @submit.prevent="handleSubscribe">
<SMDialog class="p-0">
<SMForm v-model="form" @submit.prevent="handleSubscribe">
<div class="form-row">
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<SMInput
v-model="subscribeFormData.email.value"
placeholder="Email address"
:error="subscribeFormData.email.error"
@blur="fieldValidate(subscribeFormData.email)" />
<SMCaptchaNotice />
<SMInput control="email" />
<SMButton type="submit" label="Subscribe" />
</div>
</form>
</SMForm>
</SMDialog>
</SMContainer>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import axios from "axios";
import { reactive, ref } from "vue";
import { buildUrlQuery, excerpt, timestampNowUtc } from "../helpers/common";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
clearFormData,
} from "../helpers/validation";
import { excerpt } from "../helpers/common";
import { timestampNowUtc } from "../helpers/datetime";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMCarousel from "../components/SMCarousel.vue";
import SMCarouselSlide from "../components/SMCarouselSlide.vue";
import SMMessage from "../components/SMMessage.vue";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import SMForm from "../components/SMForm.vue";
import SMDialog from "../components/SMDialog.vue";
import SMPage from "../components/SMPage.vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import { FormObject, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
import { api } from "../helpers/api";
const slides = ref([]);
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const subscribeFormData = reactive({
email: {
value: "",
error: "",
rules: {
required: true,
required_message: "An email address is needed.",
email: true,
email_message: "That does not appear to be an email address.",
},
},
});
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formLoading = ref(false);
const form = reactive(
FormObject({
email: FormControl("", And([Required(), Email()])),
})
);
const handleLoad = async () => {
slides.value = [];
let posts = [];
let events = [];
try {
let result = await axios.get(buildUrlQuery("posts", { limit: 3 }));
if (result.data.posts) {
result.data.posts.forEach((post) => {
posts.push({
title: post.title,
content: excerpt(post.content, 200),
image: post.hero,
url: { name: "post-view", params: { slug: post.slug } },
cta: "Read More...",
});
});
}
} catch (error) {
/* empty */
}
try {
let query = {
api.get({
url: "/posts",
params: {
limit: 3,
end_at: ">" + timestampNowUtc(),
};
},
progress: ({ loaded, total }) => {
console.log("progress", `${loaded} - ${total}`);
},
})
.then((response) => {
if (response.data.posts) {
response.data.posts.forEach((post) => {
posts.push({
title: post.title,
content: excerpt(post.content, 200),
image: post.hero,
url: { name: "post-view", params: { slug: post.slug } },
cta: "Read More...",
});
});
}
})
.catch((error) => {
console.log("error", error);
/* empty */
});
let result = await axios.get(buildUrlQuery("events", query));
if (result.data.events) {
result.data.events.forEach((event) => {
try {
let result = await api.get({
url: "/events",
params: {
limit: 3,
end_at: ">" + timestampNowUtc(),
},
});
if (result.json.events) {
result.json.events.forEach((event) => {
events.push({
title: event.title,
content: excerpt(event.content, 200),
@@ -228,34 +213,30 @@ const handleLoad = async () => {
};
const handleSubscribe = async () => {
formLoading.value = true;
formMessage.icon = "";
formMessage.type = "error";
formMessage.message = "";
form.loading(true);
form.message();
try {
if (isValidated(subscribeFormData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await axios.post("subscriptions", {
email: subscribeFormData.email.value,
await api.post({
url: "/subscriptions",
body: {
email: form.email.value,
captcha_token: captcha,
});
},
});
clearFormData(subscribeFormData);
formMessage.type = "success";
formMessage.message = "Your email address has been subscribed.";
}
form.email.value = "";
form.message("Your email address has been subscribed.", "success");
} catch (err) {
restParseErrors(subscribeFormData, [formMessage, "message"], err);
form.apiErrors(err);
}
formLoading.value = false;
form.loading(false);
};
useValidation(subscribeFormData);
handleLoad();
</script>

View File

@@ -1,157 +1,96 @@
<template>
<SMContainer>
<SMRow>
<SMColumn>
<SMDialog narrow>
<h1>Log in</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model:error="formData.username.error"
v-model="formData.username.value"
name="username"
label="Username"
required
@blur="fieldValidate(formData.username)">
<router-link to="/forgot-username"
>Forgot username?</router-link
>
</SMInput>
<SMInput
v-model="formData.password.value"
name="password"
type="password"
label="Password"
required
:error="formData.password.error"
@blur="fieldValidate(formData.password)">
<router-link to="/forgot-password"
>Forgot password?</router-link
>
</SMInput>
<SMCaptchaNotice />
<SMFormFooter>
<template #left>
<div>
<span class="pr-1">Need an account?</span
><router-link to="/register"
>Register</router-link
>
</div>
</template>
<template #right>
<SMButton
type="submit"
label="Log in"
icon="fa-solid fa-arrow-right" />
</template>
</SMFormFooter>
</form>
</SMDialog>
</SMColumn>
</SMRow>
</SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMDialog narrow class="mt-5">
<h1>Log in</h1>
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="username">
<router-link to="/forgot-username"
>Forgot username?</router-link
>
</SMInput>
<SMInput control="password" type="password">
<router-link to="/forgot-password"
>Forgot password?</router-link
>
</SMInput>
<SMFormFooter>
<template #left>
<div class="small">
<span class="pr-1">Need an account?</span
><router-link to="/register">Register</router-link>
</div>
</template>
<template #right>
<SMButton
type="submit"
label="Log in"
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</SMForm>
</SMDialog>
</SMPage>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { reactive } from "vue";
import { useUserStore } from "../store/UserStore";
import { useRoute, useRouter } from "vue-router";
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Min, Required, Password } from "../helpers/validate";
import SMPage from "../components/SMPage.vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import { useUserStore } from "../store/UserStore";
import { useRoute, useRouter } from "vue-router";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import SMForm from "../components/SMForm.vue";
const router = useRouter();
const userStore = useUserStore();
const formLoading = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
username: {
value: "",
error: "",
rules: {
required: true,
required_message: "Your username is needed",
min: 4,
min_message: "Your username is at least 6 characters",
},
},
password: {
value: "",
error: "",
rules: {
required: true,
required_message: "A password is required",
min: 8,
min_message: "Your password needs to be at least %d characters",
password: "special",
password_message:
"Your password needs to have at least a letter, a number and a special character",
},
},
});
useValidation(formData);
const form = reactive(
FormObject({
username: FormControl("", And([Required(), Min(4)])),
password: FormControl("", Password()),
})
);
const redirect = useRoute().query.redirect;
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.message();
form.loading(true);
try {
if (isValidated(formData)) {
let res = await axios.post("login", {
username: formData.username.value,
password: formData.password.value,
});
let res = await api.post({
url: "/login",
body: {
username: form.username.value,
password: form.password.value,
},
});
if (res.data.token !== undefined) {
userStore.setUserDetails(res.data.user);
userStore.setUserToken(res.data.token);
if (redirect !== undefined) {
if (redirect.startsWith("api/")) {
window.location.href =
redirect +
"?token=" +
encodeURIComponent(res.data.token);
} else {
router.push({ path: redirect });
}
} else {
router.push({ name: "dashboard" });
}
userStore.setUserDetails(res.json.user);
userStore.setUserToken(res.json.token);
if (redirect !== undefined) {
if (redirect.startsWith("api/")) {
window.location.href =
redirect + "?token=" + encodeURIComponent(res.json.token);
} else {
formMessage.message =
"An unexpected error occurred on the server. Please try again later";
router.push({ path: redirect });
}
} else {
router.push({ name: "dashboard" });
}
} catch (err) {
restParseErrors(formData, [formMessage, "message"], err);
console.log(err);
form.apiErrors(err);
}
formLoading.value = false;
form.loading(false);
};
</script>
<style lang="scss"></style>
if (userStore.token) {
userStore.clearUser();
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow>
<SMDialog narrow class="mt-5" :loading="formLoading">
<h1>Logged out</h1>
<SMRow>
<SMColumn class="justify-content-center">
@@ -17,33 +17,28 @@
</SMRow>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import SMButton from "@/components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import { api } from "../helpers/api";
import { ref } from "vue";
import { useUserStore } from "../store/UserStore";
import { useRouter } from "vue-router";
const router = useRouter();
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMPage from "../components/SMPage.vue";
const userStore = useUserStore();
const formLoading = ref(false);
const formMessage = reactive({
type: "info",
message: "Logging you out...",
icon: "",
});
formLoading.value = true;
const logout = async () => {
formLoading.value = true;
try {
await axios.post("logout");
await api.post({
url: "/logout",
});
} catch (err) {
console.log(err);
}

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer class="news-list">
<SMPage class="news-list">
<SMMessage
v-if="formMessage.message"
:icon="formMessage.icon"
@@ -22,15 +22,16 @@
button="Read More"
button-type="outline" />
</SMPanelList>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
import axios from "axios";
import { api } from "../helpers/api";
import SMMessage from "../components/SMMessage.vue";
import SMPanelList from "../components/SMPanelList.vue";
import SMPanel from "../components/SMPanel.vue";
import SMPage from "../components/SMPage.vue";
import { timestampUtcToLocal } from "../helpers/common";
const formMessage = reactive({
@@ -48,8 +49,13 @@ const handleLoad = async () => {
formMessage.message = "";
try {
let result = await axios.get("posts?limit=5");
posts.value = result.data.posts;
let result = await api.get({
url: "/posts",
params: {
limit: 5,
},
});
posts.value = result.json.posts;
posts.value.forEach((post) => {
post.publish_at = timestampUtcToLocal(post.publish_at);
@@ -65,5 +71,3 @@ const handleLoad = async () => {
handleLoad();
</script>
<style lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer :loading="pageLoading" full class="page-post-view">
<SMPage :loading="pageLoading" full class="page-post-view">
<SMPageError :error="error">
<div
class="heading-image"
@@ -10,7 +10,7 @@
<div class="heading-info">
<h1>{{ post.title }}</h1>
<div class="date-author">
<font-awesome-icon icon="fa-solid fa-calendar" />
<ion-icon name="calendar-outline" />
{{ formattedPublishAt(post.publish_at) }}, by
{{ post.user_username }}
</div>
@@ -18,16 +18,17 @@
<component :is="formattedContent" ref="content"></component>
</SMContainer>
</SMPageError>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import axios from "axios";
import { useRoute } from "vue-router";
import SMPageError from "../components/SMPageError.vue";
import { fullMonthString, timestampUtcToLocal } from "../helpers/common";
import { useApplicationStore } from "../store/ApplicationStore";
import { api } from "../helpers/api";
import SMPage from "../components/SMPage.vue";
const applicationStore = useApplicationStore();
const route = useRoute();
@@ -39,16 +40,20 @@ let pageLoading = ref(true);
const loadData = async () => {
if (route.params.slug) {
try {
let res = await axios.get(
`posts?slug==${route.params.slug}&limit=1`
);
if (!res.data.posts) {
let res = await api.get({
url: "/posts",
params: {
slug: `=${route.params.slug}`,
limit: 1,
},
});
if (!res.json.posts) {
error.value = 500;
} else {
if (res.data.total == 0) {
if (res.json.total == 0) {
error.value = 404;
} else {
post.value = res.data.posts[0];
post.value = res.json.posts[0];
post.value.publish_at = timestampUtcToLocal(
post.value.publish_at
@@ -57,19 +62,19 @@ const loadData = async () => {
applicationStore.setDynamicTitle(post.value.title);
try {
let result = await axios.get(
`media/${post.value.hero}`
);
post.value.hero_url = result.data.medium.url;
let result = await api.get({
url: `/media/${post.value.hero}`,
});
post.value.hero_url = result.json.medium.url;
} catch (error) {
/* empty */
}
try {
let result = await axios.get(
`users/${post.value.user_id}`
);
post.value.user_username = result.data.user.username;
let result = await api.get({
url: `/users/${post.value.user_id}`,
});
post.value.user_username = result.json.user.username;
} catch (error) {
/* empty */
}
@@ -144,12 +149,15 @@ loadData();
.content {
margin-top: map-get($spacer, 4);
line-height: 1.5rem;
padding: 0 map-get($spacer, 3);
a span {
color: $primary-color !important;
}
p {
line-height: 1.5rem;
}
}
}

View File

@@ -1,76 +1,38 @@
<template>
<SMContainer>
<SMRow>
<SMDialog :narrow="formDone" :loading="formLoading">
<SMDialog :narrow="formDone">
<template v-if="!formDone">
<h1>Register</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMForm v-model="form" @submit="handleSubmit">
<SMRow>
<SMColumn>
<SMInput
v-model="formData.username.value"
label="Username"
required
:error="formData.username.error"
@blur="
fieldValidate(formData.username)
"></SMInput>
<SMInput control="username" />
</SMColumn>
<SMColumn>
<SMInput
v-model="formData.password.value"
type="password"
label="Password"
required
:error="formData.password.error"
@blur="
fieldValidate(formData.password)
"></SMInput>
control="password"
type="password"></SMInput>
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
v-model="formData.first_name.value"
label="First Name"
required
:error="formData.first_name.error"
@blur="
fieldValidate(formData.first_name)
" />
<SMInput control="first_name" />
</SMColumn>
<SMColumn>
<SMInput
v-model="formData.last_name.value"
label="Last Name"
required
:error="formData.last_name.error"
@blur="fieldValidate(formData.last_name)" />
<SMInput control="last_name" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
v-model="formData.email.value"
label="Email"
required
:error="formData.email.error"
@blur="fieldValidate(formData.email)" />
<SMInput control="email" />
</SMColumn>
<SMColumn>
<SMInput
v-model="formData.phone.value"
label="Phone Number"
:error="formData.phone.error"
@blur="fieldValidate(formData.phone)" />
<SMInput control="phone">
This field is optional.
</SMInput>
</SMColumn>
</SMRow>
<SMCaptchaNotice />
<SMFormFooter>
<template #left>
<div>
@@ -85,10 +47,10 @@
<SMButton
type="submit"
label="Register"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Email Sent!</h1>
@@ -113,154 +75,95 @@ import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import SMForm from "../components/SMForm.vue";
import { api } from "../helpers/api";
import { FormControl, FormObject } from "../helpers/form";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
And,
Custom,
Email,
Min,
Password,
Phone,
Required,
} from "../helpers/validate";
import { debounce } from "../helpers/common";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import { useReCaptcha } from "vue-recaptcha-v3";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const lastUsernameCheck = ref("");
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
first_name: {
value: "",
error: "",
rules: {
required: true,
required_message: "A first name is needed",
min: 2,
min_message: "Your first name should be at least 2 letters long",
},
},
last_name: {
value: "",
error: "",
rules: {
required: true,
required_message: "A last name is needed",
min: 2,
min_message: "Your last name should be at least 2 letters long",
},
},
email: {
value: "",
error: "",
rules: {
required: true,
required_message: "A email address is needed",
email: true,
email_message: "Your email address is not correct",
},
},
phone: {
value: "",
error: "",
rules: {
phone: true,
phone_message: "Your phone number does not look correct",
},
},
username: {
value: "",
error: "",
rules: {
required: true,
required_message: "A username is needed",
min: 4,
min_message: "Your username needs to be at least %d characters",
custom: () => {
checkUsername();
const checkUsername = (value: string): boolean | string => {
if (lastUsernameCheck.value != form.username.value) {
lastUsernameCheck.value = form.username.value;
api.get({
url: "/users",
params: {
username: form.username.value,
},
},
},
password: {
value: "",
error: "",
rules: {
required: true,
required_message: "A password is needed",
min: 8,
min_message: "Your password needs to be at least %d characters",
password: "special",
password_message:
"Your password needs to have at least a letter, a number and a special character",
},
},
});
useValidation(formData);
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
let res = await axios.post("register", {
first_name: formData.first_name.value,
last_name: formData.last_name.value,
email: formData.email.value,
phone: formData.phone.value,
username: formData.username.value,
password: formData.password.value,
captcha_token: captcha,
})
.then((response) => {
return "The username has already been taken.";
})
.catch((error) => {
if (error.status != 404) {
return (
error.json?.message ||
"An unexpected server error occurred."
);
}
});
formDone.value = true;
}
} catch (err) {
restParseErrors(formData, [formMessage, "message"], err);
}
formLoading.value = false;
return true;
};
const checkUsername = async () => {
const formDone = ref(false);
const form = reactive(
FormObject({
first_name: FormControl("", Required()),
last_name: FormControl("", Required()),
email: FormControl("", And([Required(), Email()])),
phone: FormControl("", Phone()),
username: FormControl("", And([Min(4), Custom(checkUsername)])),
password: FormControl("", And([Required(), Password()])),
})
);
const handleSubmit = async () => {
form.loading(true);
try {
if (
formData.username.value.length >= 4 &&
lastUsernameCheck.value != formData.username.value
) {
lastUsernameCheck.value = formData.username.value;
await axios.get(`users?username=${formData.username.value}`);
formData.username.error = "The username has already been taken.";
}
} catch (error) {
if (error.response.status == 404) {
formData.username.error = "";
} else {
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message =
error.response.message ||
"An unexpected server error occurred.";
}
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await api.post({
url: "/register",
body: {
first_name: form.first_name.value,
last_name: form.last_name.value,
email: form.email.value,
phone: form.phone.value,
username: form.username.value,
password: form.password.value,
captcha_token: captcha,
},
});
formDone.value = true;
} catch (err) {
form.apiErrors(err);
}
form.loading(false);
};
const lastUsernameCheck = ref("");
const debouncedFilter = debounce(checkUsername, 1000);
let oldUsernameValue = "";
watch(
formData,
form,
(value) => {
if (value.username.value !== oldUsernameValue) {
oldUsernameValue = value.username.value;

View File

@@ -1,26 +1,14 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow :loading="formLoading">
<SMDialog narrow>
<template v-if="!formDone">
<h1>Resend Verify Email</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model="formData.username.value"
name="username"
label="Username"
required
:error="formData.username.error"
@blur="fieldValidate(formData.username)" />
<SMCaptchaNotice />
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="username" />
<SMFormFooter>
<template #left>
<div>
<div class="small">
<span class="pr-1">Stuck?</span
><router-link to="/contact"
>Contact Us</router-link
@@ -31,10 +19,10 @@
<SMButton
type="submit"
label="Send"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Email Sent!</h1>
@@ -51,7 +39,7 @@
</template>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
@@ -60,69 +48,45 @@ import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import { useRoute } from "vue-router";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import SMForm from "../components/SMForm.vue";
import SMPage from "../components/SMPage.vue";
import { api } from "../helpers/api";
import { Required } from "../helpers/validate";
import { useReCaptcha } from "vue-recaptcha-v3";
import { FormObject, FormControl } from "../helpers/form";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
username: {
value: "",
error: "",
rules: {
required: true,
required_message: "Your username is needed",
min: 4,
min_message: "Your username is at least %d characters",
},
},
});
const form = reactive(
FormObject({
username: FormControl("", Required()),
})
);
useValidation(formData);
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
let res = await axios.post("users/resendVerifyEmailCode", {
username: formData.username.value,
await api.post({
url: "/users/resendVerifyEmailCode",
body: {
username: form.username.value,
captcha_token: captcha,
});
},
});
formDone.value = true;
}
} catch (err) {
if (err.response.status == 422) {
formDone.value = true;
} catch (error) {
if (error.status == 422) {
formDone.value = true;
} else {
restParseErrors(formData, [formMessage, "message"], err);
form.apiErrors(error);
}
}
formLoading.value = false;
form.loading(false);
};
</script>
<style lang="scss"></style>

View File

@@ -1,34 +1,15 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow :loading="formLoading">
<SMDialog narrow>
<template v-if="!formDone">
<h1>Reset Password</h1>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model="formData.code.value"
name="code"
label="Reset Code"
required
:error="formData.code.error"
@blur="fieldValidate(formData.code)" />
<SMInput
v-model="formData.password.value"
type="password"
name="password"
label="New Password"
required
:error="formData.password.error"
@blur="fieldValidate(formData.password)" />
<SMCaptchaNotice />
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="code" />
<SMInput control="password" type="password" />
<SMFormFooter>
<template #left>
<div>
<div class="small">
<router-link
:to="{ name: 'forgot-password' }"
>Resend Code</router-link
@@ -39,10 +20,10 @@
<SMButton
type="submit"
label="Reset Password"
icon="fa-solid fa-arrow-right" />
icon="arrow-forward-outline" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Password Reset!</h1>
@@ -57,94 +38,57 @@
</template>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Required, Min, Max, Password } from "../helpers/validate";
import { ref, reactive } from "vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import { useRoute } from "vue-router";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
code: {
value: "",
error: "",
rules: {
required: true,
required_message: "The code is needed",
min: 6,
min_message: "The code should be 6 characters",
max: 6,
max_message: "The code should be 6 characters",
},
},
password: {
value: "",
error: "",
rules: {
required: true,
required_message: "A new password is required",
min: 8,
min_message: "Your password needs to be at least %d characters",
password: "special",
password_message:
"Your password needs to have at least a letter, a number and a special character",
},
},
});
useValidation(formData);
const form = reactive(
FormObject({
code: FormControl("", And([Required(), Min(6), Max(6)])),
password: FormControl("", And([Required(), Password()])),
})
);
if (useRoute().query.code !== undefined) {
formData.code.value = useRoute().query.code;
form.code.value = useRoute().query.code;
}
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
let res = await axios.post("users/resetPassword", {
code: formData.code.value,
password: formData.password.value,
await api.post({
url: "/users/resetPassword",
body: {
code: form.code.value,
password: form.password.value,
captcha_token: captcha,
});
},
});
formDone.value = true;
}
} catch (err) {
restParseErrors(formData, [formMessage, "message"], err);
formDone.value = true;
} catch (error) {
form.apiError(error);
}
formLoading.value = false;
form.loading(false);
};
</script>
<style lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer class="rules">
<SMPage class="rules">
<h1>Rules</h1>
<p>
Oh gosh, no body likes rules but to ensure that we have a fun,
@@ -72,9 +72,13 @@
grief other players builds outside of the Survival game-mode.
</li>
</ul>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import SMPage from "../components/SMPage.vue";
</script>
<style lang="scss">
.rules {
h2 {

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer class="terms">
<SMPage class="terms">
<h1>Terms and Conditions</h1>
<p>
Please read these terms carefully. By accessing or using our website
@@ -560,5 +560,9 @@
be responsible for warranty and after sales service but this is
allowed under the Law.
</p>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import SMPage from "../components/SMPage.vue";
</script>

View File

@@ -1,33 +1,21 @@
<template>
<SMContainer>
<SMPage no-breadcrumbs background="/img/background.jpg">
<SMRow>
<SMDialog narrow :loading="formLoading">
<SMDialog narrow>
<template v-if="!formDone">
<h1>Unsubscribe</h1>
<p>
If you would like to unsubscribe from our mailing list,
you have come to the right page!
</p>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMInput
v-model="formData.email.value"
name="email"
label="Email"
required
:error="formData.email.error"
@blur="fieldValidate(formData.email)" />
<SMCaptchaNotice />
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="email" />
<SMFormFooter>
<template #right>
<SMButton type="submit" label="Unsubscribe" />
</template>
</SMFormFooter>
</form>
</SMForm>
</template>
<template v-else>
<h1>Unsubscribed</h1>
@@ -42,79 +30,56 @@
</template>
</SMDialog>
</SMRow>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { api } from "../helpers/api";
import { FormObject, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
import { ref, reactive } from "vue";
import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMDialog from "../components/SMDialog.vue";
import SMMessage from "../components/SMMessage.vue";
import axios from "axios";
import { useRoute } from "vue-router";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../helpers/validation";
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import SMButton from "../components/SMButton.vue";
import SMDialog from "../components/SMDialog.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../components/SMInput.vue";
import SMPage from "../components/SMPage.vue";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formLoading = ref(false);
const formDone = ref(false);
const formMessage = reactive({
message: "",
type: "error",
icon: "",
});
const formData = reactive({
email: {
value: "",
error: "",
rules: {
required: true,
required_message: "An email address is required.",
email: true,
email_message: "That does not look like an email address.",
},
},
});
const form = reactive(
FormObject({
email: FormControl("", And([Required(), Email()])),
})
);
useValidation(formData);
const submit = async () => {
formLoading.value = true;
formMessage.type = "error";
formMessage.icon = "fa-solid fa-circle-exclamation";
formMessage.message = "";
const handleSubmit = async () => {
form.loading(true);
try {
if (isValidated(formData)) {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await axios.delete("subscriptions", {
data: {
email: formData.email.value,
captcha_token: captcha,
},
});
await api.delete({
url: "/subscriptions",
body: {
email: form.email.value,
captcha_token: captcha,
},
});
formDone.value = true;
}
} catch (err) {
restParseErrors(formData, [formMessage, "message"], err);
formDone.value = true;
} catch (error) {
form.apiErrors(error);
}
formLoading.value = false;
form.loading(false);
};
if (useRoute().query.email !== undefined) {
formData.email.value = useRoute().query.email;
submit();
form.email.value = useRoute().query.email;
handleSubmit();
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<SMContainer class="mx-auto workshop-list">
<SMPage class="mx-auto workshop-list">
<h1>Workshops</h1>
<div class="toolbar">
<SMInput
@@ -40,7 +40,7 @@
event.location == 'online' ? 'Online Event' : event.address
"></SMPanel>
</SMPanelList>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
@@ -49,15 +49,14 @@ import SMInput from "../components/SMInput.vue";
import SMMessage from "../components/SMMessage.vue";
import SMPanelList from "../components/SMPanelList.vue";
import SMPanel from "../components/SMPanel.vue";
import SMPage from "../components/SMPage.vue";
import { reactive, ref } from "vue";
import axios from "axios";
import { api } from "../helpers/api";
import {
buildUrlQuery,
timestampLocalToUtc,
timestampNowUtc,
timestampUtcToLocal,
} from "../helpers/common";
import { format, parse, parseISO } from "date-fns";
} from "../helpers/datetime";
const loading = ref(true);
const events = reactive([]);
@@ -103,11 +102,13 @@ const handleLoad = async () => {
query["end_at"] = ">" + timestampNowUtc();
}
const url = buildUrlQuery("events", query);
let result = await axios.get(url);
let result = await api.get({
url: "/events",
params: query,
});
if (result.data.events) {
events.value = result.data.events;
if (result.json.events) {
events.value = result.json.events;
events.value.forEach((item) => {
item.start_at = timestampUtcToLocal(item.start_at);

View File

@@ -3,10 +3,10 @@
<div
class="workshop-image"
:style="{ backgroundImage: `url('${imageUrl}')` }">
<font-awesome-icon
<ion-icon
v-if="imageUrl.length == 0"
class="workshop-image-loader"
icon="fa-regular fa-image" />
name="image-outline" />
</div>
<template #inner>
<SMMessage
@@ -57,10 +57,7 @@
label="Register for Event"></SMButton>
</div>
<div class="workshop-date">
<h4>
<font-awesome-icon
icon="fa-regular fa-calendar" />Date / Time
</h4>
<h4><ion-icon name="calendar-outline" />Date / Time</h4>
<p
v-for="(line, index) in workshopDate"
:key="index"
@@ -69,10 +66,7 @@
</p>
</div>
<div class="workshop-location">
<h4>
<font-awesome-icon
icon="fa-solid fa-location-dot" />Location
</h4>
<h4><ion-icon name="location-outline" />Location</h4>
<p>
{{
event.location == "online"
@@ -88,19 +82,19 @@
</template>
<script setup lang="ts">
import axios from "axios";
import { api } from "../helpers/api";
import { computed, ref, reactive } from "vue";
import { useRoute } from "vue-router";
import { useApplicationStore } from "../store/ApplicationStore";
import { format } from "date-fns";
import SMButton from "../components/SMButton.vue";
import SMHTML from "../components/SMHTML.vue";
import SMMessage from "../components/SMMessage.vue";
import {
format,
timestampUtcToLocal,
timestampBeforeNow,
timestampAfterNow,
} from "../helpers/common";
} from "../helpers/datetime";
const applicationStore = useApplicationStore();
const event = ref({});
@@ -162,8 +156,8 @@ const handleLoad = async () => {
formMessage.message = "";
try {
const result = await axios.get(`events/${route.params.id}`);
event.value = result.data.event;
const result = await api.get(`events/${route.params.id}`);
event.value = result.json.event;
event.value.start_at = timestampUtcToLocal(event.value.start_at);
event.value.end_at = timestampUtcToLocal(event.value.end_at);
@@ -172,16 +166,16 @@ const handleLoad = async () => {
handleLoadImage();
} catch (error) {
formMessage.message =
error.response?.data?.message ||
error.json?.message ||
"Could not load event information from the server.";
}
};
const handleLoadImage = async () => {
try {
const result = await axios.get(`media/${event.value.hero}`);
if (result.data.medium) {
imageUrl.value = result.data.medium.url;
const result = await api.get(`media/${event.value.hero}`);
if (result.json.medium) {
imageUrl.value = result.json.medium.url;
}
} catch (error) {
/* empty */

View File

@@ -1,118 +1,72 @@
<template>
<SMContainer>
<SMMessage
v-if="formMessage.message"
:type="formMessage.type"
:message="formMessage.message"
:icon="formMessage.icon" />
<form @submit.prevent="submit">
<SMPage>
<SMForm v-model="form" @submit="handleSubmit">
<SMRow>
<SMInput
v-model="formData.title.value"
label="Title"
required
:error="formData.title.error"
@blur="fieldValidate(formData.title)" />
<SMInput control="title" />
</SMRow>
<SMRow>
<SMEditor
id="content"
v-model="formData.content.value"
v-model="form.content.value"
@file-accept="fileAccept"
@attachment-add="attachmentAdd" />
</SMRow>
<SMRow>
<SMButton type="submit" label="Save" />
</SMRow>
</form>
</SMContainer>
</SMForm>
</SMPage>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import DEditor from "../../components/SMEditor.vue";
import { api } from "../../helpers/api";
import { FormObject, FormControl } from "../../helpers/form";
import { And, Required, Min } from "../../helpers/validate";
import { reactive } from "vue";
import { useRoute } from "vue-router";
import SMInput from "../../components/SMInput.vue";
import SMButton from "../../components/SMButton.vue";
import SMDialog from "../../components/SMDialog.vue";
import SMMessage from "../../components/SMMessage.vue";
import axios from "axios";
import {
useValidation,
isValidated,
fieldValidate,
restParseErrors,
} from "../../helpers/validation";
import { useUserStore } from "@/store/UserStore";
import { useRoute } from "vue-router";
import { createTemplateLiteral } from "@vue/compiler-core";
import SMPage from "../../components/SMPage.vue";
import SMForm from "../../components/SMForm.vue";
const route = useRoute();
const userStore = useUserStore();
const formMessage = reactive({
icon: "",
type: "",
message: "",
});
const formData = reactive({
title: {
value: "",
error: "",
rules: {
required: true,
required_message: "A first name is needed",
min: 2,
min_message: "Your first name should be at least 2 letters long",
},
},
content: {
value: "<div>Hello <strong>People</strong> persons!</div>",
error: "",
rules: {
required: true,
required_message: "A last name is needed",
min: 2,
min_message: "Your last name should be at least 2 letters long",
},
},
});
const form = reactive(
FormObject({
title: FormControl("", And([Required(), Min(2)])),
content: FormControl("", Required()),
})
);
useValidation(formData);
// const getPostById = async () => {
// try {
// if (isValidated(formData)) {
// let res = await axios.get("posts/" + route.params.id);
const getPostById = async () => {
// formData.title.value = res.data.title;
// formData.content.value = res.data.content;
// }
// } catch (err) {
// console.log(err);
// formMessage.icon = "";
// formMessage.type = "error";
// formMessage.message = "";
// restParseErrors(formData, [formMessage, "message"], err);
// }
// };
const handleSubmit = async () => {
try {
if (isValidated(formData)) {
let res = await axios.get("posts/" + route.params.id);
await api.post({
url: "/posts",
body: {
title: form.title.value,
content: form.content.value,
},
});
formData.title.value = res.data.title;
formData.content.value = res.data.content;
}
} catch (err) {
console.log(err);
formMessage.icon = "";
formMessage.type = "error";
formMessage.message = "";
restParseErrors(formData, [formMessage, "message"], err);
}
};
const submit = async () => {
try {
if (isValidated(formData)) {
let res = await axios.post("posts", {
title: formData.title.value,
content: formData.content.value,
});
console.log(ref);
formMessage.type = "success";
formMessage.message = "Your details have been updated";
}
} catch (err) {
console.log(err);
formMessage.icon = "";
formMessage.type = "error";
formMessage.message = "";
restParseErrors(formData, [formMessage, "message"], err);
form.message("The post has been saved", "success");
} catch (error) {
form.apiError(error);
}
};
@@ -162,21 +116,3 @@ const attachmentAdd = async (event) => {
}
};
</script>
<style lang="scss">
// .dialog {
// flex-direction: column;
// margin: 0 auto;
// max-width: 600px;
// }
// .buttonFooter {
// flex-direction: row;
// }
// @media screen and (max-width: 768px) {
// .buttonFooter {
// flex-direction: column-reverse;
// }
// }
</style>

View File

@@ -1,58 +1,59 @@
<template>
<SMContainer class="dashboard mx-auto">
<SMPage class="dashboard mx-auto">
<h1>Dashboard</h1>
<div class="boxes">
<router-link to="/dashboard/details" class="box">
<font-awesome-icon icon="fa-solid fa-user-pen" />
<ion-icon name="location-outline" />
<h2>My Details</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/posts')"
to="/dashboard/posts"
class="box">
<font-awesome-icon icon="fa-regular fa-newspaper" />
<ion-icon name="newspaper-outline" />
<h2>Posts</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/users')"
:to="{ name: 'user-list' }"
class="box">
<font-awesome-icon icon="fa-solid fa-users" />
<ion-icon name="people-outline" />
<h2>Users</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/events')"
to="/dashboard/events"
class="box">
<font-awesome-icon icon="fa-regular fa-calendar" />
<ion-icon name="calendar-outline" />
<h2>Events</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/courses')"
to="/dashboard/courses"
class="box">
<font-awesome-icon icon="fa-solid fa-graduation-cap" />
<ion-icon name="school-outline" />
<h2>{{ courseBoxTitle }}</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('admin/media')"
to="/dashboard/media"
class="box">
<font-awesome-icon icon="fa-solid fa-photo-film" />
<ion-icon name="film-outline" />
<h2>Media</h2>
</router-link>
<router-link
v-if="userStore.permissions.includes('logs/discord')"
:to="{ name: 'discord-bot-logs' }"
class="box">
<font-awesome-icon icon="fa-brands fa-discord" />
<ion-icon name="logo-discord" />
<h2>Discord Bot Logs</h2>
</router-link>
</div>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import SMPage from "../../components/SMPage.vue";
import { computed } from "vue";
import { useUserStore } from "../../store/UserStore";
@@ -90,6 +91,7 @@ const courseBoxTitle = computed(() => {
font-size: map-get($spacer, 3);
color: $font-color;
transition: background-color 0.3s, border 0.3s;
align-items: center;
text-align: center;
h2 {
@@ -97,7 +99,7 @@ const courseBoxTitle = computed(() => {
margin-bottom: 0;
}
svg {
ion-icon {
font-size: map-get($spacer, 5);
}

View File

@@ -166,7 +166,6 @@ import {
} from "../../helpers/validation";
import { useRoute } from "vue-router";
import { timestampLocalToUtc, timestampUtcToLocal } from "../../helpers/common";
import { parseISO } from "date-fns";
const route = useRoute();
const formLoading = ref(false);

View File

@@ -26,7 +26,7 @@
:items="items"
:search-value="search">
<template #loading>
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
</template>
<template #item-title="item">
<router-link
@@ -36,12 +36,12 @@
</template>
<template #item-actions="item">
<div class="action-wrapper">
<font-awesome-icon
<!-- <font-awesome-icon
icon="fa-solid fa-pen-to-square"
@click="handleEdit(item)" />
<font-awesome-icon
icon="fa-regular fa-trash-can"
@click="handleDelete(item)" />
@click="handleDelete(item)" /> -->
</div>
</template>
</EasyDataTable>
@@ -66,6 +66,7 @@ import { debounce } from "../../helpers/common";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import { restParseErrors } from "../../helpers/validation";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter();
const search = ref("");

View File

@@ -29,19 +29,19 @@
:items="items"
:search-value="search">
<template #loading>
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
</template>
<template #item-size="item">
{{ bytesReadable(item.size) }}
</template>
<template #item-actions="item">
<div class="action-wrapper">
<font-awesome-icon
<!-- <font-awesome-icon
icon="fa-solid fa-pen-to-square"
@click.stop="handleEdit(item)" />
<font-awesome-icon
icon="fa-regular fa-trash-can"
@click.stop="handleDelete(item)" />
@click.stop="handleDelete(item)" /> -->
<d-file-link :href="item.url" target="_blank" @click.stop=""
><font-awesome-icon icon="fa-solid fa-download"
/></d-file-link>
@@ -65,6 +65,7 @@ import { debounce, parseErrorType, bytesReadable } from "../../helpers/common";
import SMMessage from "../../components/SMMessage.vue";
import DFileLink from "../../components/DFileLink.vue";
import { useUserStore } from "../../store/UserStore";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter();
const search = ref("");

View File

@@ -26,7 +26,7 @@
:items="items"
:search-value="search">
<template #loading>
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
</template>
<template #item-title="item">
<router-link
@@ -36,12 +36,12 @@
</template>
<template #item-actions="item">
<div class="action-wrapper">
<font-awesome-icon
<!-- <font-awesome-icon
icon="fa-solid fa-pen-to-square"
@click="handleEdit(item)" />
<font-awesome-icon
icon="fa-regular fa-trash-can"
@click="handleDelete(item)" />
@click="handleDelete(item)" /> -->
</div>
</template>
</EasyDataTable>
@@ -62,6 +62,7 @@ import SMButton from "../../components/SMButton.vue";
import { debounce } from "../../helpers/common";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter();
const search = ref("");

View File

@@ -16,16 +16,16 @@
:header-item-class-name="headerItemClassNameFunction"
:body-item-class-name="bodyItemClassNameFunction">
<template #loading>
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
<SMLoadingIcon />
</template>
<template #item-actions="item">
<div class="action-wrapper">
<font-awesome-icon
<!-- <font-awesome-icon
icon="fa-solid fa-pen-to-square"
@click="handleEdit(item)" />
<font-awesome-icon
icon="fa-regular fa-trash-can"
@click="handleDelete(item)" />
@click="handleDelete(item)" /> -->
</div>
</template>
</EasyDataTable>
@@ -42,6 +42,7 @@ import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { openDialog } from "vue3-promise-dialog";
import SMHeading from "../../components/SMHeading.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
const router = useRouter();
const searchValue = ref("");

View File

@@ -8,5 +8,7 @@
<body>
<div id="app"></div>
@vite('resources/js/main.js')
<script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
</body>
</html>

View File

@@ -7,7 +7,8 @@ export default defineConfig({
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => ["trix-editor"].includes(tag),
isCustomElement: (tag) =>
["trix-editor", "ion-icon"].includes(tag),
},
transformAssetUrls: {
base: null,