drop axios/date-fns/fontawesome
This commit is contained in:
@@ -240,7 +240,7 @@ class UserController extends ApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this->respondError([
|
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
|
}//end if
|
||||||
|
|
||||||
return $this->respondWithErrors([
|
return $this->respondWithErrors([
|
||||||
'code' => 'The code was not found or has expired'
|
'code' => 'The code was not found or has expired.'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
698
app/Services/AnimatedGifService.php
Normal file
698
app/Services/AnimatedGifService.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use ImageIntervention;
|
|
||||||
|
|
||||||
class ImageService
|
|
||||||
{
|
|
||||||
}
|
|
||||||
314
package-lock.json
generated
314
package-lock.json
generated
@@ -1,20 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "Website",
|
"name": "website",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"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",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"@vuepic/vue-datepicker": "^3.6.4",
|
"@vuepic/vue-datepicker": "^3.6.4",
|
||||||
"date-fns": "^2.29.3",
|
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"element-plus": "^2.2.27",
|
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
"pinia-plugin-persistedstate": "^3.0.1",
|
"pinia-plugin-persistedstate": "^3.0.1",
|
||||||
@@ -32,7 +25,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"axios": "^1.1.2",
|
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-plugin-jsdoc": "^39.6.4",
|
"eslint-plugin-jsdoc": "^39.6.4",
|
||||||
@@ -56,22 +48,6 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/@es-joy/jsdoccomment": {
|
||||||
"version": "0.36.1",
|
"version": "0.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz",
|
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz",
|
||||||
@@ -439,85 +415,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.8",
|
"version": "0.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||||
@@ -644,16 +541,6 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.21.0",
|
"version": "8.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.11.18",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
@@ -710,11 +584,6 @@
|
|||||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.50.0",
|
"version": "5.50.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz",
|
||||||
@@ -1037,39 +906,6 @@
|
|||||||
"vue": ">=3.2.0"
|
"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": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||||
@@ -1331,28 +1167,6 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
@@ -1622,11 +1424,6 @@
|
|||||||
"date-fns": ">=2.0.0"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"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==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/dir-glob": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||||
@@ -1702,31 +1490,6 @@
|
|||||||
"integrity": "sha512-47o4PPgxfU1KMNejz+Dgaodf7YTcg48uOfV1oM6cs3adrl2+7R+dHkt3Jpxqo0LRCbGJEzTKMUt0RdvByb/leg==",
|
"integrity": "sha512-47o4PPgxfU1KMNejz+Dgaodf7YTcg48uOfV1oM6cs3adrl2+7R+dHkt3Jpxqo0LRCbGJEzTKMUt0RdvByb/leg==",
|
||||||
"peer": true
|
"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": {
|
"node_modules/emojis-list": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||||
@@ -1799,11 +1562,6 @@
|
|||||||
"node": ">=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": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"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==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -2638,22 +2362,8 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
},
|
"dev": true
|
||||||
"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": "*"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
@@ -2681,11 +2391,6 @@
|
|||||||
"sourcemap-codec": "^1.4.8"
|
"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": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
@@ -2718,6 +2423,7 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -2726,6 +2432,7 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
},
|
},
|
||||||
@@ -2794,11 +2501,6 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/normalize.css": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"axios": "^1.1.2",
|
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-plugin-jsdoc": "^39.6.4",
|
"eslint-plugin-jsdoc": "^39.6.4",
|
||||||
@@ -22,16 +21,9 @@
|
|||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"@vuepic/vue-datepicker": "^3.6.4",
|
"@vuepic/vue-datepicker": "^3.6.4",
|
||||||
"date-fns": "^2.29.3",
|
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"element-plus": "^2.2.27",
|
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.28",
|
||||||
"pinia-plugin-persistedstate": "^3.0.1",
|
"pinia-plugin-persistedstate": "^3.0.1",
|
||||||
|
|||||||
BIN
public/img/background.jpg
Normal file
BIN
public/img/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -43,50 +43,31 @@ h1 {
|
|||||||
font-weight: 800;
|
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 {
|
p {
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
// input,
|
||||||
select,
|
// select,
|
||||||
textarea {
|
// textarea {
|
||||||
box-sizing: border-box;
|
// box-sizing: border-box;
|
||||||
display: block;
|
// display: block;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
border: 1px solid $border-color;
|
// border: 1px solid $border-color;
|
||||||
border-radius: 12px;
|
// border-radius: 12px;
|
||||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
// padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||||
color: $font-color;
|
// color: $font-color;
|
||||||
margin-bottom: map-get($spacer, 4);
|
// margin-bottom: map-get($spacer, 4);
|
||||||
|
|
||||||
-webkit-appearance: none;
|
// -webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
// -moz-appearance: none;
|
||||||
appearance: none;
|
// appearance: none;
|
||||||
}
|
// }
|
||||||
|
|
||||||
textarea {
|
// textarea {
|
||||||
resize: none;
|
// resize: none;
|
||||||
}
|
// }
|
||||||
|
|
||||||
select {
|
select {
|
||||||
padding-right: 2.5rem;
|
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.button,
|
button.button,
|
||||||
a.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 Errors */
|
||||||
.page-error {
|
.page-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -125,33 +125,33 @@
|
|||||||
/* Margin */
|
/* Margin */
|
||||||
@each $index, $size in $spacer {
|
@each $index, $size in $spacer {
|
||||||
.m-#{$index} {
|
.m-#{$index} {
|
||||||
margin: #{$size};
|
margin: #{$size} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-#{$index} {
|
.mt-#{$index} {
|
||||||
margin-top: #{$size};
|
margin-top: #{$size} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb-#{$index} {
|
.mb-#{$index} {
|
||||||
margin-bottom: #{$size};
|
margin-bottom: #{$size} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-#{$index} {
|
.ml-#{$index} {
|
||||||
margin-left: #{$size};
|
margin-left: #{$size} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-#{$index} {
|
.mr-#{$index} {
|
||||||
margin-right: #{$size};
|
margin-right: #{$size} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-#{$index} {
|
.mx-#{$index} {
|
||||||
margin-left: #{$size};
|
margin-left: #{$size} !important;
|
||||||
margin-right: #{$size};
|
margin-right: #{$size} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-#{$index} {
|
.my-#{$index} {
|
||||||
margin-top: #{$size};
|
margin-top: #{$size} !important;
|
||||||
margin-bottom: #{$size};
|
margin-bottom: #{$size} !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
resources/js/bootstrap.js
vendored
8
resources/js/bootstrap.js
vendored
@@ -1,4 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
window._ = _;
|
window._ = _;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,10 +7,10 @@ window._ = _;
|
|||||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
// import axios from 'axios';
|
||||||
window.axios = 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
|
* Echo exposes an expressive API for subscribing to channels and listening
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
]"
|
]"
|
||||||
:type="buttonType">
|
:type="buttonType">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<font-awesome-icon v-if="icon" :icon="icon" />
|
<ion-icon v-if="icon" :icon="icon" />
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-else-if="to == null"
|
v-else-if="to == null"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
]"
|
]"
|
||||||
:type="buttonType">
|
:type="buttonType">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<font-awesome-icon v-if="icon" :icon="icon" />
|
<ion-icon v-if="icon" :icon="icon" />
|
||||||
</button>
|
</button>
|
||||||
<router-link
|
<router-link
|
||||||
v-else
|
v-else
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
{ 'button-block': block },
|
{ 'button-block': block },
|
||||||
]">
|
]">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<font-awesome-icon v-if="icon" :icon="icon" />
|
<ion-icon v-if="icon" :icon="icon" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,5 +83,11 @@ const classType = props.type == "submit" ? "primary" : props.type;
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
height: 1.2rem;
|
||||||
|
width: 1.2rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,22 +7,20 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-slide-prev" @click="handleSlidePrev">
|
<div class="carousel-slide-prev" @click="handleSlidePrev">
|
||||||
<font-awesome-icon icon="fa-solid fa-chevron-left" />
|
<ion-icon name="chevron-back-outline" />
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-slide-next" @click="handleSlideNext">
|
<div class="carousel-slide-next" @click="handleSlideNext">
|
||||||
<font-awesome-icon icon="fa-solid fa-chevron-right" />
|
<ion-icon name="chevron-forward-outline" />
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-slide-indicators">
|
<div class="carousel-slide-indicators">
|
||||||
<div
|
<div
|
||||||
v-for="(indicator, index) in slideElements"
|
v-for="(indicator, index) in slideElements"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="carousel-slide-indicator-dot">
|
:class="[
|
||||||
<font-awesome-icon
|
'carousel-slide-indicator-item',
|
||||||
v-if="currentSlide != index"
|
{ highlighted: currentSlide == index },
|
||||||
icon="fa-regular fa-circle"
|
]"
|
||||||
@click="handleIndicator(index)" />
|
@click="handleIndicator(index)"></div>
|
||||||
<font-awesome-icon v-else icon="fa-solid fa-circle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -184,12 +182,20 @@ const disconnectMutationObserver = () => {
|
|||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
transition: opacity 0.2s ease-in-out;
|
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;
|
cursor: pointer;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
padding: 0 0.25rem;
|
margin: 0 calc(#{map-get($spacer, 1)} / 3);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||||
|
|
||||||
|
&.highlighted {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class="carousel-slide"
|
class="carousel-slide"
|
||||||
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
||||||
<div v-if="imageUrl == null" class="carousel-slide-loading">
|
<div v-if="imageUrl == null" class="carousel-slide-loading">
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="carousel-slide-body">
|
<div v-else class="carousel-slide-body">
|
||||||
<div class="carousel-slide-content">
|
<div class="carousel-slide-content">
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios";
|
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { api } from "../helpers/api";
|
||||||
import SMButton from "./SMButton.vue";
|
import SMButton from "./SMButton.vue";
|
||||||
|
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -56,9 +57,9 @@ let imageUrl = ref(null);
|
|||||||
|
|
||||||
const handleLoad = async () => {
|
const handleLoad = async () => {
|
||||||
try {
|
try {
|
||||||
let result = await axios.get(`media/${props.image}`);
|
let result = await api.get(`/media/${props.image}`);
|
||||||
if (result.data.medium) {
|
if (result.json.medium) {
|
||||||
imageUrl.value = result.data.medium.url;
|
imageUrl.value = result.json.medium.url;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
imageUrl.value = "";
|
imageUrl.value = "";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['container', { full: isFull }]">
|
<div :class="['container', { full: isFull }]" :style="styleObject">
|
||||||
<SMLoader :loading="loading">
|
<SMLoader :loading="loading">
|
||||||
<d-error-forbidden
|
<d-error-forbidden
|
||||||
v-if="pageError == 403 || !hasPermission()"></d-error-forbidden>
|
v-if="pageError == 403 || !hasPermission()"></d-error-forbidden>
|
||||||
@@ -50,9 +50,19 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
background: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
let styleObject = {};
|
||||||
|
|
||||||
|
if (props.background != "") {
|
||||||
|
styleObject["backgroundImage"] = `url('${props.background}')`;
|
||||||
|
}
|
||||||
|
|
||||||
const hasPermission = () => {
|
const hasPermission = () => {
|
||||||
return (
|
return (
|
||||||
@@ -76,6 +86,9 @@ const isFull = computed(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
&.full {
|
&.full {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="help" class="form-group-help">
|
<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 }}
|
{{ help }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="loading" class="dialog-loading-cover">
|
<div v-if="loading" class="dialog-loading-cover">
|
||||||
<div class="dialog-loading">
|
<div class="dialog-loading">
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
<span>{{ loadingMessage }}</span>
|
<span>{{ loadingMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -50,6 +52,7 @@ defineProps({
|
|||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: map-get($spacer, 5) * 12;
|
min-width: map-get($spacer, 5) * 12;
|
||||||
|
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
& > h1 {
|
& > h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|||||||
@@ -5,33 +5,33 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://facebook.com/stemmechanics"
|
<a href="https://facebook.com/stemmechanics"
|
||||||
><font-awesome-icon icon="fa-brands fa-facebook"
|
><ion-icon name="logo-facebook"></ion-icon
|
||||||
/></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://mastodon.au/@stemmechanics"
|
<a href="https://mastodon.au/@stemmechanics"
|
||||||
><font-awesome-icon icon="fa-brands fa-mastodon"
|
><ion-icon name="logo-mastodon"></ion-icon
|
||||||
/></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.youtube.com/@stemmechanics"
|
<a href="https://www.youtube.com/@stemmechanics"
|
||||||
><font-awesome-icon icon="fa-brands fa-youtube"
|
><ion-icon name="logo-youtube"></ion-icon
|
||||||
/></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://twitter.com/stemmechanics"
|
<a href="https://twitter.com/stemmechanics"
|
||||||
><font-awesome-icon icon="fa-brands fa-twitter"
|
><ion-icon name="logo-twitter"></ion-icon
|
||||||
/></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/stemmechanics"
|
<a href="https://github.com/stemmechanics"
|
||||||
><font-awesome-icon icon="fa-brands fa-github"
|
><ion-icon name="logo-github"></ion-icon
|
||||||
/></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/yNzk4x7mpD"
|
<a href="https://discord.gg/yNzk4x7mpD"
|
||||||
><font-awesome-icon icon="fa-brands fa-discord"
|
><ion-icon name="logo-discord"></ion-icon
|
||||||
/></a>
|
></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
|
|||||||
36
resources/js/components/SMForm.vue
Normal file
36
resources/js/components/SMForm.vue
Normal 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>
|
||||||
@@ -1,26 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-footer">
|
<div class="sm-form-footer">
|
||||||
<div class="form-footer-column form-footer-column-left">
|
<div class="sm-form-footer-column sm-form-footer-column-left">
|
||||||
<slot name="left"></slot>
|
<slot name="left"></slot>
|
||||||
</div>
|
</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>
|
<slot name="right"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.form-footer {
|
.sm-form-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
// margin-bottom: map-get($spacer, 3);
|
|
||||||
|
|
||||||
.form-footer-column {
|
.sm-form-footer-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&.form-footer-column-left,
|
&.sm-form-footer-column-left,
|
||||||
&.form-footer-column-right {
|
&.sm-form-footer-column-right {
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
margin-left: map-get($spacer, 1);
|
margin-left: map-get($spacer, 1);
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.form-footer-column-right {
|
&.sm-form-footer-column-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
@@ -44,12 +43,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.form-footer {
|
.sm-form-footer {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
|
||||||
.form-footer-column {
|
.sm-form-footer-column {
|
||||||
&.form-footer-column-left,
|
&.sm-form-footer-column-left,
|
||||||
&.form-footer-column-right {
|
&.sm-form-footer-column-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -66,11 +65,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.form-footer-column-left {
|
&.sm-form-footer-column-left {
|
||||||
margin-bottom: -#{calc(map-get($spacer, 1) / 2)};
|
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)};
|
margin-top: -#{calc(map-get($spacer, 1) / 2)};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
v-if="back != ''"
|
v-if="back != ''"
|
||||||
:to="{ name: back }"
|
:to="{ name: back }"
|
||||||
class="heading-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>
|
||||||
<router-link v-if="close != ''" :to="{ name: close }" class="close">
|
<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>
|
</router-link>
|
||||||
<span v-if="closeBack" class="close" @click="handleBack">
|
<span v-if="closeBack" class="close" @click="handleBack">
|
||||||
<font-awesome-icon icon="fa-solid fa-close" />
|
<ion-icon name="close-outline" />
|
||||||
</span>
|
</span>
|
||||||
<h1>{{ heading }}</h1>
|
<h1>{{ heading }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['form-group', { 'has-error': error }]">
|
<div
|
||||||
<label v-if="label" :class="{ required: required, inline: inline }">{{
|
:class="[
|
||||||
label
|
'sm-input-group',
|
||||||
}}</label>
|
{
|
||||||
|
'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
|
<input
|
||||||
v-if="
|
v-if="
|
||||||
type == 'text' ||
|
type == 'text' ||
|
||||||
|
type == 'email' ||
|
||||||
type == 'password' ||
|
type == 'password' ||
|
||||||
type == 'email' ||
|
type == 'email' ||
|
||||||
type == 'url'
|
type == 'url'
|
||||||
"
|
"
|
||||||
:type="type"
|
:type="type"
|
||||||
:value="modelValue"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@input="input"
|
:value="value"
|
||||||
|
@input="handleInput"
|
||||||
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
@keydown="handleBlur" />
|
@keydown="handleKeydown" />
|
||||||
<textarea
|
<textarea
|
||||||
v-if="type == 'textarea'"
|
v-if="type == 'textarea'"
|
||||||
rows="5"
|
rows="5"
|
||||||
:value="modelValue"
|
:value="value"
|
||||||
:placeholder="placeholder"
|
@input="handleInput"
|
||||||
@input="input"
|
@focus="handleFocus"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
@keydown="handleBlur"></textarea>
|
@keydown="handleKeydown"></textarea>
|
||||||
<div v-if="type == 'file'" class="input-file-group">
|
<div v-if="type == 'file'" class="input-file-group">
|
||||||
<input
|
<input
|
||||||
id="file"
|
id="file"
|
||||||
@@ -40,37 +50,31 @@
|
|||||||
props.modelValue
|
props.modelValue
|
||||||
}}</a>
|
}}</a>
|
||||||
<span v-if="type == 'static'">{{ props.modelValue }}</span>
|
<span v-if="type == 'static'">{{ props.modelValue }}</span>
|
||||||
<div v-if="type == 'media'" class="input-media-group">
|
<div v-if="slots.default || feedbackInvalid" class="sm-input-help">
|
||||||
<div class="input-media-display">
|
<span v-if="feedbackInvalid" class="sm-input-invalid">{{
|
||||||
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
|
feedbackInvalid
|
||||||
<font-awesome-icon v-else icon="fa-regular fa-image" />
|
}}</span>
|
||||||
</div>
|
<span v-if="slots.default" class="sm-input-info">
|
||||||
<div v-if="type == 'media'" class="form-group-error">
|
<slot></slot>
|
||||||
{{ error }}
|
</span>
|
||||||
</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>
|
</div>
|
||||||
<div v-if="help" class="form-group-help">
|
<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 }}
|
{{ help }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, useSlots, ref, watch } from "vue";
|
import { watch, computed, useSlots, ref, inject } from "vue";
|
||||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
import { toTitleCase } from "../helpers/string";
|
||||||
import { openDialog } from "vue3-promise-dialog";
|
import { isEmpty } from "../helpers/utils";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -90,7 +94,7 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "text",
|
default: "text",
|
||||||
},
|
},
|
||||||
error: {
|
feedbackInvalid: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
@@ -106,100 +110,215 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
href: {
|
control: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
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 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) => {
|
const handleChange = (event) => {
|
||||||
emits("update:modelValue", event.target.files[0]);
|
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);
|
emits("blur", event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const input = (event) => {
|
const handleKeydown = (event: Event) => {
|
||||||
emits("update:modelValue", event.target.value);
|
emits("keydown", event);
|
||||||
};
|
|
||||||
|
|
||||||
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 inline = computed(() => {
|
const inline = computed(() => {
|
||||||
return ["static", "link"].includes(props.type);
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.input-media-group {
|
.sm-input-group {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 26rem;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
margin-bottom: map-get($spacer, 4);
|
||||||
|
|
||||||
.input-media-display {
|
&.sm-input-active {
|
||||||
display: flex;
|
label {
|
||||||
margin-bottom: 1rem;
|
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;
|
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 {
|
-webkit-appearance: none;
|
||||||
max-width: 100%;
|
-moz-appearance: none;
|
||||||
max-height: 100%;
|
appearance: none;
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
padding: 4rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
textarea {
|
||||||
max-width: 13rem;
|
resize: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.input-media-group + .form-group-error {
|
.sm-input-help {
|
||||||
text-align: center;
|
font-size: 75%;
|
||||||
}
|
margin: 0 map-get($spacer, 1);
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
.sm-input-invalid {
|
||||||
.input-media-group {
|
color: $danger-color;
|
||||||
max-width: 13rem;
|
padding-right: map-get($spacer, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="loading" class="loader-cover">
|
<div v-if="loading" class="sm-loader">
|
||||||
<div class="loader">
|
<SMLoadingIcon />
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
@@ -12,6 +10,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -19,3 +19,20 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
66
resources/js/components/SMLoadingIcon.vue
Normal file
66
resources/js/components/SMLoadingIcon.vue
Normal 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>
|
||||||
360
resources/js/components/SMMediaInput.vue
Normal file
360
resources/js/components/SMMediaInput.vue
Normal 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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['message', type]">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,16 +24,13 @@ defineProps({
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.message {
|
.message {
|
||||||
|
display: flex;
|
||||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||||
margin-bottom: map-get($spacer, 4);
|
margin-bottom: map-get($spacer, 4);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
||||||
svg {
|
|
||||||
padding-right: map-get($spacer, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: $primary-color-lighter;
|
background-color: $primary-color-lighter;
|
||||||
color: $primary-color-darker;
|
color: $primary-color-darker;
|
||||||
@@ -53,5 +51,20 @@ defineProps({
|
|||||||
border: 1px solid $danger-color-lighter;
|
border: 1px solid $danger-color-lighter;
|
||||||
border-radius: 12px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer
|
<SMContainer
|
||||||
:full="true"
|
:full="true"
|
||||||
:class="['navbar', { showDropdown: showToggle }]"
|
:class="['sm-navbar', { showDropdown: showToggle }]"
|
||||||
@click="handleHideMenu">
|
@click="handleHideMenu">
|
||||||
<template #inner>
|
<template #inner>
|
||||||
<div class="navbar-container">
|
<div class="navbar-container">
|
||||||
@@ -24,12 +24,12 @@
|
|||||||
:to="{ name: 'workshop-list' }"
|
:to="{ name: 'workshop-list' }"
|
||||||
class="navbar-cta"
|
class="navbar-cta"
|
||||||
label="Find a workshop"
|
label="Find a workshop"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
<div class="menuButton" @click.stop="handleToggleMenu">
|
<div class="menuButton" @click.stop="handleToggleMenu">
|
||||||
<span>Menu</span
|
<span>Menu</span
|
||||||
><font-awesome-icon
|
><ion-icon
|
||||||
icon="fa-solid fa-bars"
|
class="menuButtonIcon"
|
||||||
class="menuButtonIcon" />
|
name="reorder-three-outline"></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -37,9 +37,7 @@
|
|||||||
<ul class="navbar-dropdown">
|
<ul class="navbar-dropdown">
|
||||||
<li class="ml-auto">
|
<li class="ml-auto">
|
||||||
<div class="menuClose" @click.stop="handleToggleMenu">
|
<div class="menuClose" @click.stop="handleToggleMenu">
|
||||||
<font-awesome-icon
|
<ion-icon name="close-outline"></ion-icon>
|
||||||
icon="fa-solid fa-xmark"
|
|
||||||
class="menuCloseIcon" />
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-for="item in menuItems">
|
<template v-for="item in menuItems">
|
||||||
@@ -47,7 +45,7 @@
|
|||||||
v-if="item.show == undefined || item.show()"
|
v-if="item.show == undefined || item.show()"
|
||||||
:key="item.name">
|
:key="item.name">
|
||||||
<router-link :to="item.to"
|
<router-link :to="item.to"
|
||||||
><font-awesome-icon :icon="item.icon" />{{
|
><ion-icon :name="item.icon" />{{
|
||||||
item.label
|
item.label
|
||||||
}}</router-link
|
}}</router-link
|
||||||
>
|
>
|
||||||
@@ -69,13 +67,13 @@ const menuItems = [
|
|||||||
name: "news",
|
name: "news",
|
||||||
label: "News",
|
label: "News",
|
||||||
to: "/news",
|
to: "/news",
|
||||||
icon: "fa-regular fa-newspaper",
|
icon: "newspaper-outline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "workshops",
|
name: "workshops",
|
||||||
label: "Workshops",
|
label: "Workshops",
|
||||||
to: "/workshops",
|
to: "/workshops",
|
||||||
icon: "fa-solid fa-pen-ruler",
|
icon: "shapes-outline",
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// name: "courses",
|
// name: "courses",
|
||||||
@@ -87,13 +85,13 @@ const menuItems = [
|
|||||||
name: "contact",
|
name: "contact",
|
||||||
label: "Contact us",
|
label: "Contact us",
|
||||||
to: "/contact",
|
to: "/contact",
|
||||||
icon: "fa-regular fa-envelope",
|
icon: "mail-outline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "register",
|
name: "register",
|
||||||
label: "Register",
|
label: "Register",
|
||||||
to: "/register",
|
to: "/register",
|
||||||
icon: "fa-solid fa-pen-to-square",
|
icon: "person-add-outline",
|
||||||
show: () => !userStore.id,
|
show: () => !userStore.id,
|
||||||
inNav: false,
|
inNav: false,
|
||||||
},
|
},
|
||||||
@@ -101,7 +99,7 @@ const menuItems = [
|
|||||||
name: "login",
|
name: "login",
|
||||||
label: "Log in",
|
label: "Log in",
|
||||||
to: "/login",
|
to: "/login",
|
||||||
icon: "fa-solid fa-right-to-bracket",
|
icon: "log-in-outline",
|
||||||
show: () => !userStore.id,
|
show: () => !userStore.id,
|
||||||
inNav: false,
|
inNav: false,
|
||||||
},
|
},
|
||||||
@@ -109,7 +107,7 @@ const menuItems = [
|
|||||||
name: "dashboard",
|
name: "dashboard",
|
||||||
label: "Dashboard",
|
label: "Dashboard",
|
||||||
to: "/dashboard",
|
to: "/dashboard",
|
||||||
icon: "fa-regular fa-circle-user",
|
icon: "apps-outline",
|
||||||
show: () => userStore.id,
|
show: () => userStore.id,
|
||||||
inNav: false,
|
inNav: false,
|
||||||
},
|
},
|
||||||
@@ -117,7 +115,7 @@ const menuItems = [
|
|||||||
name: "logout",
|
name: "logout",
|
||||||
label: "Log out",
|
label: "Log out",
|
||||||
to: "/logout",
|
to: "/logout",
|
||||||
icon: "fa-solid fa-right-from-bracket",
|
icon: "log-out-outline",
|
||||||
show: () => userStore.id,
|
show: () => userStore.id,
|
||||||
inNav: false,
|
inNav: false,
|
||||||
},
|
},
|
||||||
@@ -135,7 +133,7 @@ const handleHideMenu = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.navbar {
|
.sm-navbar {
|
||||||
height: 4.5rem;
|
height: 4.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -143,6 +141,7 @@ const handleHideMenu = () => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 auto !important;
|
flex: 0 0 auto !important;
|
||||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
&.showDropdown {
|
&.showDropdown {
|
||||||
.navbar-dropdown-cover {
|
.navbar-dropdown-cover {
|
||||||
@@ -195,8 +194,10 @@ const handleHideMenu = () => {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: map-get($spacer, 5) * 3;
|
width: map-get($spacer, 5) * 3;
|
||||||
|
|
||||||
svg {
|
ion-icon {
|
||||||
padding-right: map-get($spacer, 1);
|
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;
|
cursor: pointer;
|
||||||
font-size: map-get($spacer, 4);
|
font-size: map-get($spacer, 4);
|
||||||
padding-left: map-get($spacer, 1);
|
padding-left: map-get($spacer, 1);
|
||||||
@@ -271,7 +272,7 @@ const handleHideMenu = () => {
|
|||||||
|
|
||||||
.menuButtonIcon {
|
.menuButtonIcon {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
font-size: 1.4rem;
|
font-size: map-get($spacer, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
resources/js/components/SMPage.vue
Normal file
97
resources/js/components/SMPage.vue
Normal 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>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<d-error-forbidden v-if="error == 403"></d-error-forbidden>
|
<d-error-forbidden v-if="error == 403"></d-error-forbidden>
|
||||||
<d-error-internal v-if="error >= 500"></d-error-internal>
|
<d-error-internal v-if="error >= 500"></d-error-internal>
|
||||||
<d-error-not-found v-if="error == 404"></d-error-not-found>
|
<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>
|
<slot></slot>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,23 +11,22 @@
|
|||||||
{{ format(new Date(date), "MMM") }}
|
{{ format(new Date(date), "MMM") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<font-awesome-icon
|
<ion-icon
|
||||||
v-if="hideImageLoader == false"
|
v-if="hideImageLoader == false"
|
||||||
class="panel-image-loader"
|
class="panel-image-loader"
|
||||||
icon="fa-regular fa-image" />
|
name="image-outline" />
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<h3 class="panel-title">{{ title }}</h3>
|
<h3 class="panel-title">{{ title }}</h3>
|
||||||
<div v-if="showDate" class="panel-date">
|
<div v-if="showDate" class="panel-date">
|
||||||
<font-awesome-icon
|
<ion-icon
|
||||||
v-if="showTime == false && endDate.length == 0"
|
v-if="showTime == false && endDate.length == 0"
|
||||||
icon="fa-regular fa-calendar" /><font-awesome-icon
|
name="calendar-outline" />
|
||||||
v-else
|
<ion-icon v-else name="time-outline" />
|
||||||
icon="fa-regular fa-clock" />
|
|
||||||
<p>{{ panelDate }}</p>
|
<p>{{ panelDate }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="location" class="panel-location">
|
<div v-if="location" class="panel-location">
|
||||||
<font-awesome-icon icon="fa-solid fa-location-dot" />
|
<ion-icon name="location-outline" />
|
||||||
<p>{{ location }}</p>
|
<p>{{ location }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="content" class="panel-content">{{ panelContent }}</div>
|
<div v-if="content" class="panel-content">{{ panelContent }}</div>
|
||||||
@@ -39,7 +38,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios";
|
|
||||||
import { onMounted, computed, ref } from "vue";
|
import { onMounted, computed, ref } from "vue";
|
||||||
import {
|
import {
|
||||||
excerpt,
|
excerpt,
|
||||||
@@ -49,6 +47,7 @@ import {
|
|||||||
} from "../helpers/common";
|
} from "../helpers/common";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import SMButton from "./SMButton.vue";
|
import SMButton from "./SMButton.vue";
|
||||||
|
import { api } from "../helpers/api";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -151,10 +150,10 @@ const hideImageLoader = computed(() => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (imageUrl.value && imageUrl.value.length > 0 && isUUID(imageUrl.value)) {
|
if (imageUrl.value && imageUrl.value.length > 0 && isUUID(imageUrl.value)) {
|
||||||
try {
|
try {
|
||||||
let result = await axios.get(`media/${props.image}`);
|
let result = await api.get(`/media/${props.image}`);
|
||||||
|
|
||||||
if (result.data.medium) {
|
if (result.json.medium) {
|
||||||
imageUrl.value = result.data.medium.url;
|
imageUrl.value = result.json.medium.url;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* empty */
|
/* empty */
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="panel-list">
|
<div class="panel-list">
|
||||||
<div v-if="loading" class="panel-list-loading">
|
<div v-if="loading" class="panel-list-loading">
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="notFound" class="panel-list-not-found">
|
<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>
|
<p>{{ notFoundText }}</p>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="help" class="form-group-help">
|
<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 }}
|
{{ help }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios";
|
|
||||||
import { useUserStore } from "../../store/UserStore";
|
import { useUserStore } from "../../store/UserStore";
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { closeDialog } from "vue3-promise-dialog";
|
import { closeDialog } from "vue3-promise-dialog";
|
||||||
@@ -82,15 +81,17 @@ const handleConfirm = async () => {
|
|||||||
if (isValidated(formData)) {
|
if (isValidated(formData)) {
|
||||||
try {
|
try {
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
await axios.put(`users/${userStore.id}`, {
|
await api.put({
|
||||||
password: formData.password.value,
|
url: `/users/${userStore.id}`,
|
||||||
|
body: {
|
||||||
|
password: formData.password.value,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
isSuccessful.value = true;
|
isSuccessful.value = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
formData.password.error =
|
formData.password.error =
|
||||||
err.response?.data?.message ||
|
err.json?.message || "An unexpected error occurred";
|
||||||
"An unexpected error occurred";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,19 +26,19 @@
|
|||||||
>Page {{ page }} of {{ totalPages }}</span
|
>Page {{ page }} of {{ totalPages }}</span
|
||||||
>
|
>
|
||||||
<span class="media-browser-page-changer">
|
<span class="media-browser-page-changer">
|
||||||
<font-awesome-icon
|
<ion-icon
|
||||||
|
name="chevron-back-outline"
|
||||||
:class="[
|
:class="[
|
||||||
'changer-button',
|
'changer-button',
|
||||||
{ disabled: prevDisabled },
|
{ disabled: prevDisabled },
|
||||||
]"
|
]"
|
||||||
icon="fa-solid fa-angle-left"
|
|
||||||
@click="handlePrev" />
|
@click="handlePrev" />
|
||||||
<font-awesome-icon
|
<ion-icon
|
||||||
|
name="chevron-forward-outline"
|
||||||
:class="[
|
:class="[
|
||||||
'changer-button',
|
'changer-button',
|
||||||
{ disabled: nextDisabled },
|
{ disabled: nextDisabled },
|
||||||
]"
|
]"
|
||||||
icon="fa-solid fa-angle-right"
|
|
||||||
@click="handleNext" />
|
@click="handleNext" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios";
|
|
||||||
import { computed, watch, ref, reactive, onMounted, onUnmounted } from "vue";
|
import { computed, watch, ref, reactive, onMounted, onUnmounted } from "vue";
|
||||||
import { closeDialog } from "vue3-promise-dialog";
|
import { closeDialog } from "vue3-promise-dialog";
|
||||||
import SMButton from "../SMButton.vue";
|
import SMButton from "../SMButton.vue";
|
||||||
@@ -82,6 +81,7 @@ import SMDialog from "../SMDialog.vue";
|
|||||||
import SMMessage from "../SMMessage.vue";
|
import SMMessage from "../SMMessage.vue";
|
||||||
import SMModal from "../SMModal.vue";
|
import SMModal from "../SMModal.vue";
|
||||||
import { toParamString } from "../../helpers/common";
|
import { toParamString } from "../../helpers/common";
|
||||||
|
import { api } from "../../helpers/api";
|
||||||
|
|
||||||
const uploader = ref(null);
|
const uploader = ref(null);
|
||||||
const formLoading = ref(false);
|
const formLoading = ref(false);
|
||||||
@@ -134,18 +134,18 @@ const handleLoad = async () => {
|
|||||||
params.limit = perPage.value;
|
params.limit = perPage.value;
|
||||||
// params.fields = "url";
|
// params.fields = "url";
|
||||||
|
|
||||||
let res = await axios.get(`media${toParamString(params)}`);
|
let res = await api.get(`/media${toParamString(params)}`);
|
||||||
|
|
||||||
totalItems.value = res.data.total;
|
totalItems.value = res.json.total;
|
||||||
mediaItems.value = res.data.media;
|
mediaItems.value = res.json.media;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response.status == 404) {
|
if (error.status == 404) {
|
||||||
formMessage.type = "primary";
|
formMessage.type = "primary";
|
||||||
formMessage.icon = "fa-regular fa-folder-open";
|
formMessage.icon = "fa-regular fa-folder-open";
|
||||||
formMessage.message = "No media items found";
|
formMessage.message = "No media items found";
|
||||||
} else {
|
} else {
|
||||||
formMessage.message =
|
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) {
|
if (uploader.value.files[0] instanceof File) {
|
||||||
submitFormData.append("file", uploader.value.files[0]);
|
submitFormData.append("file", uploader.value.files[0]);
|
||||||
|
|
||||||
let res = await axios.post("media", submitFormData, {
|
let res = await api.post({
|
||||||
|
url: "/media",
|
||||||
|
body: submitFormData,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
onUploadProgress: (progressEvent) =>
|
// onUploadProgress: (progressEvent) =>
|
||||||
(formLoadingMessage.value = `Uploading Files ${Math.floor(
|
// (formLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||||
(progressEvent.loaded / progressEvent.total) * 100
|
// (progressEvent.loaded / progressEvent.total) * 100
|
||||||
)}%`),
|
// )}%`),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.medium) {
|
if (res.json.medium) {
|
||||||
closeDialog(res.data.medium);
|
closeDialog(res.json.medium);
|
||||||
} else {
|
} else {
|
||||||
formMessage.message =
|
formMessage.message =
|
||||||
"An unexpected response was received from the server";
|
"An unexpected response was received from the server";
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-error forbidden">
|
<SMPage no-breadcrumbs>
|
||||||
<div class="image"></div>
|
<div class="page-error forbidden">
|
||||||
<div class="content">
|
<div class="image"></div>
|
||||||
<h1>The cat says no!</h1>
|
<div class="content">
|
||||||
<p>You do not have the needed access to see this page</p>
|
<h1>The cat says no!</h1>
|
||||||
|
<p>You do not have the needed access to see this page</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SMPage from "../SMPage.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.page-error.forbidden .image {
|
.page-error.forbidden .image {
|
||||||
background-image: url('/img/403.jpg');
|
background-image: url("/img/403.jpg");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-error internal">
|
<SMPage no-breadcrumbs>
|
||||||
<div class="image"></div>
|
<div class="page-error internal">
|
||||||
<div class="content">
|
<div class="image"></div>
|
||||||
<h1>The cat has broken something</h1>
|
<div class="content">
|
||||||
<p>We are working to fix that what was broken. Please try again later.</p>
|
<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>
|
||||||
</div>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SMPage from "../SMPage.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.page-error.internal .image {
|
.page-error.internal .image {
|
||||||
background-image: url('/img/500.jpg');
|
background-image: url("/img/500.jpg");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-error not-found">
|
<SMPage no-breadcrumbs>
|
||||||
<div class="image"></div>
|
<div class="page-error not-found">
|
||||||
<div class="content">
|
<div class="image"></div>
|
||||||
<h1>Opps</h1>
|
<div class="content">
|
||||||
<p>The page you asked for was not found</p>
|
<h1>Opps</h1>
|
||||||
|
<p>The page you asked for was not found</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SMPage from "../SMPage.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.page-error.not-found .image {
|
.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
190
resources/js/helpers/api.ts
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
const transitionEndEventName = () => {
|
const transitionEndEventName = () => {
|
||||||
var i,
|
var i,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -173,41 +171,6 @@ export function parseErrorType(
|
|||||||
return def;
|
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) => {
|
export const buildUrlQuery = (url, query) => {
|
||||||
let s = "";
|
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 {
|
export {
|
||||||
transitionEndEventName,
|
transitionEndEventName,
|
||||||
waitForElementRender,
|
waitForElementRender,
|
||||||
|
|||||||
221
resources/js/helpers/datetime.ts
Normal file
221
resources/js/helpers/datetime.ts
Normal 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()
|
||||||
|
);
|
||||||
|
};
|
||||||
137
resources/js/helpers/form.ts
Normal file
137
resources/js/helpers/form.ts
Normal 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 */
|
||||||
5
resources/js/helpers/string.js
Normal file
5
resources/js/helpers/string.js
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
||||||
11
resources/js/helpers/utils.ts
Normal file
11
resources/js/helpers/utils.ts
Normal 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;
|
||||||
|
};
|
||||||
440
resources/js/helpers/validate.ts
Normal file
440
resources/js/helpers/validate.ts
Normal 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),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ import { createApp } from "vue";
|
|||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
import Router from "@/router";
|
import Router from "@/router";
|
||||||
import "./axios.js";
|
// import "./axios.js";
|
||||||
import "normalize.css";
|
import "normalize.css";
|
||||||
import "../css/app.scss";
|
import "../css/app.scss";
|
||||||
import App from "./views/App.vue";
|
import App from "./views/App.vue";
|
||||||
import FontAwesomeIcon from "@/helpers/fontawesome";
|
// import FontAwesomeIcon from "@/helpers/fontawesome";
|
||||||
import SMContainer from "./components/SMContainer.vue";
|
import SMContainer from "./components/SMContainer.vue";
|
||||||
import SMRow from "./components/SMRow.vue";
|
import SMRow from "./components/SMRow.vue";
|
||||||
import SMColumn from "./components/SMColumn.vue";
|
import SMColumn from "./components/SMColumn.vue";
|
||||||
@@ -19,7 +19,7 @@ const pinia = createPinia();
|
|||||||
pinia.use(piniaPluginPersistedstate);
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
createApp(App)
|
createApp(App)
|
||||||
.component("FontAwesomeIcon", FontAwesomeIcon)
|
// .component("FontAwesomeIcon", FontAwesomeIcon)
|
||||||
.use(pinia)
|
.use(pinia)
|
||||||
.use(Router)
|
.use(Router)
|
||||||
.use(PromiseDialog)
|
.use(PromiseDialog)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { createWebHistory, createRouter } from "vue-router";
|
import { createWebHistory, createRouter } from "vue-router";
|
||||||
import { useUserStore } from "@/store/UserStore";
|
import { useUserStore } from "@/store/UserStore";
|
||||||
import { useApplicationStore } from "../store/ApplicationStore";
|
import { useApplicationStore } from "../store/ApplicationStore";
|
||||||
|
import { api } from "../helpers/api";
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
{
|
{
|
||||||
@@ -386,10 +386,10 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
let redirect = false;
|
let redirect = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res = await axios.get("me");
|
let res = await api.get("/me");
|
||||||
userStore.setUserDetails(res.data.user);
|
userStore.setUserDetails(res.json.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response.status == 401) {
|
if (err.status == 401) {
|
||||||
userStore.clearUser();
|
userStore.clearUser();
|
||||||
redirect = true;
|
redirect = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from "axios";
|
import { api } from "../helpers/api";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export interface UserDetails {
|
export interface UserDetails {
|
||||||
@@ -51,16 +51,16 @@ export const useUserStore = defineStore({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetchUser() {
|
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.id = res.json.user.id;
|
||||||
this.$state.token = res.data.token;
|
this.$state.token = res.json.token;
|
||||||
this.$state.username = res.data.user.username;
|
this.$state.username = res.json.user.username;
|
||||||
this.$state.firstName = res.data.user.first_name;
|
this.$state.firstName = res.json.user.first_name;
|
||||||
this.$state.lastName = res.data.user.last_name;
|
this.$state.lastName = res.json.user.last_name;
|
||||||
this.$state.email = res.data.user.email;
|
this.$state.email = res.json.user.email;
|
||||||
this.$state.phone = res.data.user.phone;
|
this.$state.phone = res.json.user.phone;
|
||||||
this.$state.permissions = res.data.user.permissions || [];
|
this.$state.permissions = res.json.user.permissions || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
clearUser() {
|
clearUser() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMNavbar />
|
<SMNavbar />
|
||||||
<SMBreadcrumbs />
|
|
||||||
<main>
|
<main>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
@@ -14,7 +13,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SMNavbar from "../components/SMNavbar.vue";
|
import SMNavbar from "../components/SMNavbar.vue";
|
||||||
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
|
|
||||||
import SMFooter from "../components/SMFooter.vue";
|
import SMFooter from "../components/SMFooter.vue";
|
||||||
import { DialogWrapper } from "vue3-promise-dialog";
|
import { DialogWrapper } from "vue3-promise-dialog";
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer class="page-contact">
|
<SMPage class="page-contact">
|
||||||
<SMRow break-large>
|
<SMRow break-large>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<h1 class="text-left">Contact Us</h1>
|
<h1 class="text-left">Contact Us</h1>
|
||||||
@@ -42,43 +42,20 @@
|
|||||||
</SMColumn>
|
</SMColumn>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<div>
|
<div>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog narrow>
|
||||||
<template v-if="!formDone">
|
<template v-if="!formSubmitted">
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="name" />
|
||||||
:type="formMessage.type"
|
<SMInput control="email" type="email" />
|
||||||
:message="formMessage.message"
|
|
||||||
:icon="formMessage.icon" />
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="formData.name.value"
|
control="content"
|
||||||
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"
|
|
||||||
label="Message"
|
label="Message"
|
||||||
required
|
type="textarea" />
|
||||||
:error="formData.content.error"
|
|
||||||
@blur="fieldValidate(formData.content)" />
|
|
||||||
<SMCaptchaNotice />
|
|
||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
block
|
block
|
||||||
label="Send Message"
|
label="Send Message" />
|
||||||
icon="fa-regular fa-paper-plane" />
|
</SMForm>
|
||||||
</form>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Message Sent!</h1>
|
<h1>Message Sent!</h1>
|
||||||
@@ -86,98 +63,65 @@
|
|||||||
Your message as been sent to us. We will respond
|
Your message as been sent to us. We will respond
|
||||||
as soon as we can.
|
as soon as we can.
|
||||||
</p>
|
</p>
|
||||||
<SMButton block to="/" label="Home" />
|
<SMButton
|
||||||
|
block
|
||||||
|
:to="{ name: 'home' }"
|
||||||
|
label="Home" />
|
||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</div>
|
</div>
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
|
||||||
import SMInput from "../components/SMInput.vue";
|
|
||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMForm from "../components/SMForm.vue";
|
||||||
import axios from "axios";
|
import SMInput from "../components/SMInput.vue";
|
||||||
import {
|
import SMPage from "../components/SMPage.vue";
|
||||||
useValidation,
|
|
||||||
isValidated,
|
import { api } from "../helpers/api";
|
||||||
fieldValidate,
|
import { FormObject, FormControl } from "../helpers/form";
|
||||||
restParseErrors,
|
import { And, Email, Min, Required } from "../helpers/validate";
|
||||||
} from "../helpers/validation";
|
|
||||||
|
import { ref, reactive } from "vue";
|
||||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
|
||||||
|
|
||||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
const form = reactive(
|
||||||
const formDone = ref(false);
|
FormObject({
|
||||||
const formMessage = reactive({
|
name: FormControl("", And([Required(), Min(4)])),
|
||||||
message: "",
|
email: FormControl("", And([Required(), Email()])),
|
||||||
type: "error",
|
content: FormControl("", And([Required(), Min(8)])),
|
||||||
icon: "",
|
})
|
||||||
});
|
);
|
||||||
const formData = reactive({
|
const formSubmitted = ref(false);
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useValidation(formData);
|
const handleSubmit = async () => {
|
||||||
|
form.loading(true);
|
||||||
const submit = async () => {
|
|
||||||
formLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
await axios.post("contact", {
|
await api.post({
|
||||||
name: formData.name.value,
|
url: "/contact",
|
||||||
email: formData.email.value,
|
body: {
|
||||||
|
name: form.name.value,
|
||||||
|
email: form.email.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
content: formData.content.value,
|
content: form.content.value,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formSubmitted.value = true;
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
formLoading.value = false;
|
console.log(err);
|
||||||
formMessage.type = "error";
|
form.apiErrors(err);
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog class="mt-5" narrow>
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Email Verify</h1>
|
<h1>Email Verify</h1>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="code" />
|
||||||
: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 />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div>
|
<div class="small">
|
||||||
<router-link to="/resend-verify-email"
|
<router-link to="/resend-verify-email"
|
||||||
>Resend Code</router-link
|
>Resend Code</router-link
|
||||||
>
|
>
|
||||||
@@ -30,10 +18,10 @@
|
|||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Verify Code"
|
label="Verify Code"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Email Verified!</h1>
|
<h1>Email Verified!</h1>
|
||||||
@@ -48,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -57,70 +45,47 @@ import SMInput from "../components/SMInput.vue";
|
|||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMPage from "../components/SMPage.vue";
|
||||||
import axios from "axios";
|
import SMForm from "../components/SMForm.vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import {
|
import { And, Max, Min, Required } from "../helpers/validate";
|
||||||
useValidation,
|
|
||||||
isValidated,
|
|
||||||
fieldValidate,
|
|
||||||
restParseErrors,
|
|
||||||
} from "../helpers/validation";
|
|
||||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
|
||||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||||
|
import { FormControl, FormObject } from "../helpers/form";
|
||||||
|
import { api } from "../helpers/api";
|
||||||
|
|
||||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
|
||||||
const formDone = ref(false);
|
const formDone = ref(false);
|
||||||
const formMessage = reactive({
|
const form = reactive(
|
||||||
message: "",
|
FormObject({
|
||||||
type: "error",
|
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useValidation(formData);
|
const handleSubmit = async () => {
|
||||||
|
form.loading(true);
|
||||||
const submit = async () => {
|
|
||||||
formLoading.value = true;
|
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
await axios.post("users/verifyEmail", {
|
await api.post({
|
||||||
code: formData.code.value,
|
url: "/users/verifyEmail",
|
||||||
|
body: {
|
||||||
|
code: form.code.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
form.apiErrors(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (useRoute().query.code !== undefined) {
|
if (useRoute().query.code !== undefined) {
|
||||||
formData.code.value = useRoute().query.code;
|
form.code.value = useRoute().query.code;
|
||||||
submit();
|
handleSubmit();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import SMMessage from "../components/SMMessage.vue";
|
|||||||
import SMPanelList from "../components/SMPanelList.vue";
|
import SMPanelList from "../components/SMPanelList.vue";
|
||||||
import SMPanel from "../components/SMPanel.vue";
|
import SMPanel from "../components/SMPanel.vue";
|
||||||
import { reactive } from "vue";
|
import { reactive } from "vue";
|
||||||
import axios from "axios";
|
import { api } from "../helpers/api";
|
||||||
|
|
||||||
const events = reactive([]);
|
const events = reactive([]);
|
||||||
|
|
||||||
@@ -39,12 +39,16 @@ const handleLoad = async () => {
|
|||||||
formMessage.message = "";
|
formMessage.message = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await axios.get("events?limit=10");
|
let result = await api.get({
|
||||||
events.value = result.data.events;
|
url: "/events",
|
||||||
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
events.value = result.json.events;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
formMessage.message =
|
formMessage.message =
|
||||||
error.response?.data?.message ||
|
error.json?.message || "Could not load any events from the server.";
|
||||||
"Could not load any events from the server.";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog narrow class="mt-5">
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Forgot Password</h1>
|
<h1>Forgot Password</h1>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="username" />
|
||||||
: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 />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div>
|
<div class="small">
|
||||||
<span class="pr-1">Remember?</span
|
<span class="pr-1">Remember?</span
|
||||||
><router-link :to="{ name: 'login' }"
|
><router-link :to="{ name: 'login' }"
|
||||||
>Log in</router-link
|
>Log in</router-link
|
||||||
@@ -31,10 +19,10 @@
|
|||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Send"
|
label="Send"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Email Sent!</h1>
|
<h1>Email Sent!</h1>
|
||||||
@@ -51,78 +39,54 @@
|
|||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { 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 { 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 { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
|
||||||
const formDone = ref(false);
|
const formDone = ref(false);
|
||||||
const formMessage = reactive({
|
const form = reactive(
|
||||||
message: "",
|
FormObject({
|
||||||
type: "error",
|
username: FormControl("", And([Required(), Min(4)])),
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useValidation(formData);
|
const handleSubmit = async () => {
|
||||||
|
form.loading(true);
|
||||||
const submit = async () => {
|
|
||||||
formLoading.value = true;
|
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
let res = await axios.post("users/forgotPassword", {
|
await api.post({
|
||||||
username: formData.username.value,
|
url: "/users/forgotPassword",
|
||||||
|
body: {
|
||||||
|
username: form.username.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
}
|
} catch (error) {
|
||||||
} catch (err) {
|
if (error.status == 422) {
|
||||||
if (err.response.status == 422) {
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
} else {
|
} else {
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
form.apiErrors(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog narrow class="mt-5">
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Forgot Username</h1>
|
<h1>Forgot Username</h1>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="email" />
|
||||||
: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 />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div>
|
<div class="small">
|
||||||
<span class="pr-1">Remember?</span
|
<span class="pr-1">Remember?</span
|
||||||
><router-link :to="{ name: 'login' }"
|
><router-link :to="{ name: 'login' }"
|
||||||
>Log in</router-link
|
>Log in</router-link
|
||||||
@@ -31,10 +19,10 @@
|
|||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Send"
|
label="Send"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Email Sent!</h1>
|
<h1>Email Sent!</h1>
|
||||||
@@ -50,72 +38,52 @@
|
|||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
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 SMButton from "../components/SMButton.vue";
|
||||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMForm from "../components/SMForm.vue";
|
||||||
import axios from "axios";
|
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||||
import {
|
import SMInput from "../components/SMInput.vue";
|
||||||
useValidation,
|
import SMPage from "../components/SMPage.vue";
|
||||||
isValidated,
|
|
||||||
fieldValidate,
|
|
||||||
restParseErrors,
|
|
||||||
} from "../helpers/validation";
|
|
||||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
|
||||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||||
|
|
||||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
|
||||||
const formDone = ref(false);
|
const formDone = ref(false);
|
||||||
const formMessage = reactive({
|
const form = reactive(
|
||||||
message: "",
|
FormObject({
|
||||||
type: "error",
|
email: FormControl("", And([Required(), Email()])),
|
||||||
icon: "",
|
})
|
||||||
});
|
);
|
||||||
const formData = reactive({
|
|
||||||
email: {
|
|
||||||
value: "",
|
|
||||||
error: "",
|
|
||||||
rules: {
|
|
||||||
required: true,
|
|
||||||
required_message: "An email address is required",
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useValidation(formData);
|
const handleSubmit = async () => {
|
||||||
|
form.loading(true);
|
||||||
const submit = async () => {
|
|
||||||
formLoading.value = true;
|
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
let res = await axios.post("users/forgotUsername", {
|
await api.post({
|
||||||
email: formData.email.value,
|
url: "/users/forgotUsername",
|
||||||
|
body: {
|
||||||
|
email: form.email.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
}
|
} catch (error) {
|
||||||
} catch (err) {
|
form.apiErrors(error);
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer full class="home">
|
<SMPage full class="home">
|
||||||
<SMCarousel>
|
<SMCarousel>
|
||||||
<SMCarouselSlide
|
<SMCarouselSlide
|
||||||
v-for="(slide, index) in slides"
|
v-for="(slide, index) in slides"
|
||||||
@@ -110,100 +110,85 @@
|
|||||||
Sign up for our mailing list to receive expert tips and tricks,
|
Sign up for our mailing list to receive expert tips and tricks,
|
||||||
as well as updates on upcoming workshops.
|
as well as updates on upcoming workshops.
|
||||||
</p>
|
</p>
|
||||||
<SMDialog :loading="formLoading" class="p-0">
|
<SMDialog class="p-0">
|
||||||
<form @submit.prevent="handleSubscribe">
|
<SMForm v-model="form" @submit.prevent="handleSubscribe">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<SMMessage
|
<SMInput control="email" />
|
||||||
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 />
|
|
||||||
<SMButton type="submit" label="Subscribe" />
|
<SMButton type="submit" label="Subscribe" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</SMForm>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMContainer>
|
</SMContainer>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios";
|
|
||||||
import { reactive, ref } from "vue";
|
import { reactive, ref } from "vue";
|
||||||
import { buildUrlQuery, excerpt, timestampNowUtc } from "../helpers/common";
|
import { excerpt } from "../helpers/common";
|
||||||
import {
|
import { timestampNowUtc } from "../helpers/datetime";
|
||||||
useValidation,
|
|
||||||
isValidated,
|
|
||||||
fieldValidate,
|
|
||||||
restParseErrors,
|
|
||||||
clearFormData,
|
|
||||||
} from "../helpers/validation";
|
|
||||||
import SMInput from "../components/SMInput.vue";
|
import SMInput from "../components/SMInput.vue";
|
||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMCarousel from "../components/SMCarousel.vue";
|
import SMCarousel from "../components/SMCarousel.vue";
|
||||||
import SMCarouselSlide from "../components/SMCarouselSlide.vue";
|
import SMCarouselSlide from "../components/SMCarouselSlide.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMForm from "../components/SMForm.vue";
|
||||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
|
import SMPage from "../components/SMPage.vue";
|
||||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
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 slides = ref([]);
|
||||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const subscribeFormData = reactive({
|
const form = reactive(
|
||||||
email: {
|
FormObject({
|
||||||
value: "",
|
email: FormControl("", And([Required(), Email()])),
|
||||||
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 handleLoad = async () => {
|
const handleLoad = async () => {
|
||||||
slides.value = [];
|
slides.value = [];
|
||||||
let posts = [];
|
let posts = [];
|
||||||
let events = [];
|
let events = [];
|
||||||
|
|
||||||
try {
|
api.get({
|
||||||
let result = await axios.get(buildUrlQuery("posts", { limit: 3 }));
|
url: "/posts",
|
||||||
if (result.data.posts) {
|
params: {
|
||||||
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 = {
|
|
||||||
limit: 3,
|
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));
|
try {
|
||||||
if (result.data.events) {
|
let result = await api.get({
|
||||||
result.data.events.forEach((event) => {
|
url: "/events",
|
||||||
|
params: {
|
||||||
|
limit: 3,
|
||||||
|
end_at: ">" + timestampNowUtc(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.json.events) {
|
||||||
|
result.json.events.forEach((event) => {
|
||||||
events.push({
|
events.push({
|
||||||
title: event.title,
|
title: event.title,
|
||||||
content: excerpt(event.content, 200),
|
content: excerpt(event.content, 200),
|
||||||
@@ -228,34 +213,30 @@ const handleLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
formLoading.value = true;
|
form.loading(true);
|
||||||
formMessage.icon = "";
|
form.message();
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(subscribeFormData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
await axios.post("subscriptions", {
|
await api.post({
|
||||||
email: subscribeFormData.email.value,
|
url: "/subscriptions",
|
||||||
|
body: {
|
||||||
|
email: form.email.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
clearFormData(subscribeFormData);
|
form.email.value = "";
|
||||||
|
form.message("Your email address has been subscribed.", "success");
|
||||||
formMessage.type = "success";
|
|
||||||
formMessage.message = "Your email address has been subscribed.";
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
restParseErrors(subscribeFormData, [formMessage, "message"], err);
|
form.apiErrors(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useValidation(subscribeFormData);
|
|
||||||
handleLoad();
|
handleLoad();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,157 +1,96 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMDialog narrow class="mt-5">
|
||||||
<SMColumn>
|
<h1>Log in</h1>
|
||||||
<SMDialog narrow>
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
<h1>Log in</h1>
|
<SMInput control="username">
|
||||||
<SMMessage
|
<router-link to="/forgot-username"
|
||||||
v-if="formMessage.message"
|
>Forgot username?</router-link
|
||||||
:type="formMessage.type"
|
>
|
||||||
:message="formMessage.message"
|
</SMInput>
|
||||||
:icon="formMessage.icon" />
|
<SMInput control="password" type="password">
|
||||||
<form @submit.prevent="submit">
|
<router-link to="/forgot-password"
|
||||||
<SMInput
|
>Forgot password?</router-link
|
||||||
v-model:error="formData.username.error"
|
>
|
||||||
v-model="formData.username.value"
|
</SMInput>
|
||||||
name="username"
|
<SMFormFooter>
|
||||||
label="Username"
|
<template #left>
|
||||||
required
|
<div class="small">
|
||||||
@blur="fieldValidate(formData.username)">
|
<span class="pr-1">Need an account?</span
|
||||||
<router-link to="/forgot-username"
|
><router-link to="/register">Register</router-link>
|
||||||
>Forgot username?</router-link
|
</div>
|
||||||
>
|
</template>
|
||||||
</SMInput>
|
<template #right>
|
||||||
<SMInput
|
<SMButton
|
||||||
v-model="formData.password.value"
|
type="submit"
|
||||||
name="password"
|
label="Log in"
|
||||||
type="password"
|
icon="arrow-forward-outline" />
|
||||||
label="Password"
|
</template>
|
||||||
required
|
</SMFormFooter>
|
||||||
:error="formData.password.error"
|
</SMForm>
|
||||||
@blur="fieldValidate(formData.password)">
|
</SMDialog>
|
||||||
<router-link to="/forgot-password"
|
</SMPage>
|
||||||
>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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 SMInput from "../components/SMInput.vue";
|
||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMForm from "../components/SMForm.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";
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const formLoading = ref(false);
|
const form = reactive(
|
||||||
const formMessage = reactive({
|
FormObject({
|
||||||
message: "",
|
username: FormControl("", And([Required(), Min(4)])),
|
||||||
type: "error",
|
password: FormControl("", Password()),
|
||||||
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 redirect = useRoute().query.redirect;
|
const redirect = useRoute().query.redirect;
|
||||||
|
|
||||||
const submit = async () => {
|
const handleSubmit = async () => {
|
||||||
formLoading.value = true;
|
form.message();
|
||||||
formMessage.type = "error";
|
form.loading(true);
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
let res = await api.post({
|
||||||
let res = await axios.post("login", {
|
url: "/login",
|
||||||
username: formData.username.value,
|
body: {
|
||||||
password: formData.password.value,
|
username: form.username.value,
|
||||||
});
|
password: form.password.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (res.data.token !== undefined) {
|
userStore.setUserDetails(res.json.user);
|
||||||
userStore.setUserDetails(res.data.user);
|
userStore.setUserToken(res.json.token);
|
||||||
userStore.setUserToken(res.data.token);
|
if (redirect !== undefined) {
|
||||||
if (redirect !== undefined) {
|
if (redirect.startsWith("api/")) {
|
||||||
if (redirect.startsWith("api/")) {
|
window.location.href =
|
||||||
window.location.href =
|
redirect + "?token=" + encodeURIComponent(res.json.token);
|
||||||
redirect +
|
|
||||||
"?token=" +
|
|
||||||
encodeURIComponent(res.data.token);
|
|
||||||
} else {
|
|
||||||
router.push({ path: redirect });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
router.push({ name: "dashboard" });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
formMessage.message =
|
router.push({ path: redirect });
|
||||||
"An unexpected error occurred on the server. Please try again later";
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
router.push({ name: "dashboard" });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow>
|
<SMDialog narrow class="mt-5" :loading="formLoading">
|
||||||
<h1>Logged out</h1>
|
<h1>Logged out</h1>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn class="justify-content-center">
|
<SMColumn class="justify-content-center">
|
||||||
@@ -17,33 +17,28 @@
|
|||||||
</SMRow>
|
</SMRow>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
import { api } from "../helpers/api";
|
||||||
import SMButton from "@/components/SMButton.vue";
|
import { ref } from "vue";
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
|
||||||
import axios from "axios";
|
|
||||||
import { useUserStore } from "../store/UserStore";
|
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 userStore = useUserStore();
|
||||||
const formLoading = ref(false);
|
const formLoading = ref(false);
|
||||||
const formMessage = reactive({
|
|
||||||
type: "info",
|
|
||||||
message: "Logging you out...",
|
|
||||||
icon: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
formLoading.value = true;
|
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post("logout");
|
await api.post({
|
||||||
|
url: "/logout",
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer class="news-list">
|
<SMPage class="news-list">
|
||||||
<SMMessage
|
<SMMessage
|
||||||
v-if="formMessage.message"
|
v-if="formMessage.message"
|
||||||
:icon="formMessage.icon"
|
:icon="formMessage.icon"
|
||||||
@@ -22,15 +22,16 @@
|
|||||||
button="Read More"
|
button="Read More"
|
||||||
button-type="outline" />
|
button-type="outline" />
|
||||||
</SMPanelList>
|
</SMPanelList>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from "vue";
|
import { reactive, ref } from "vue";
|
||||||
import axios from "axios";
|
import { api } from "../helpers/api";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMMessage from "../components/SMMessage.vue";
|
||||||
import SMPanelList from "../components/SMPanelList.vue";
|
import SMPanelList from "../components/SMPanelList.vue";
|
||||||
import SMPanel from "../components/SMPanel.vue";
|
import SMPanel from "../components/SMPanel.vue";
|
||||||
|
import SMPage from "../components/SMPage.vue";
|
||||||
import { timestampUtcToLocal } from "../helpers/common";
|
import { timestampUtcToLocal } from "../helpers/common";
|
||||||
|
|
||||||
const formMessage = reactive({
|
const formMessage = reactive({
|
||||||
@@ -48,8 +49,13 @@ const handleLoad = async () => {
|
|||||||
formMessage.message = "";
|
formMessage.message = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await axios.get("posts?limit=5");
|
let result = await api.get({
|
||||||
posts.value = result.data.posts;
|
url: "/posts",
|
||||||
|
params: {
|
||||||
|
limit: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
posts.value = result.json.posts;
|
||||||
|
|
||||||
posts.value.forEach((post) => {
|
posts.value.forEach((post) => {
|
||||||
post.publish_at = timestampUtcToLocal(post.publish_at);
|
post.publish_at = timestampUtcToLocal(post.publish_at);
|
||||||
@@ -65,5 +71,3 @@ const handleLoad = async () => {
|
|||||||
|
|
||||||
handleLoad();
|
handleLoad();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer :loading="pageLoading" full class="page-post-view">
|
<SMPage :loading="pageLoading" full class="page-post-view">
|
||||||
<SMPageError :error="error">
|
<SMPageError :error="error">
|
||||||
<div
|
<div
|
||||||
class="heading-image"
|
class="heading-image"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="heading-info">
|
<div class="heading-info">
|
||||||
<h1>{{ post.title }}</h1>
|
<h1>{{ post.title }}</h1>
|
||||||
<div class="date-author">
|
<div class="date-author">
|
||||||
<font-awesome-icon icon="fa-solid fa-calendar" />
|
<ion-icon name="calendar-outline" />
|
||||||
{{ formattedPublishAt(post.publish_at) }}, by
|
{{ formattedPublishAt(post.publish_at) }}, by
|
||||||
{{ post.user_username }}
|
{{ post.user_username }}
|
||||||
</div>
|
</div>
|
||||||
@@ -18,16 +18,17 @@
|
|||||||
<component :is="formattedContent" ref="content"></component>
|
<component :is="formattedContent" ref="content"></component>
|
||||||
</SMContainer>
|
</SMContainer>
|
||||||
</SMPageError>
|
</SMPageError>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import axios from "axios";
|
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import SMPageError from "../components/SMPageError.vue";
|
import SMPageError from "../components/SMPageError.vue";
|
||||||
import { fullMonthString, timestampUtcToLocal } from "../helpers/common";
|
import { fullMonthString, timestampUtcToLocal } from "../helpers/common";
|
||||||
import { useApplicationStore } from "../store/ApplicationStore";
|
import { useApplicationStore } from "../store/ApplicationStore";
|
||||||
|
import { api } from "../helpers/api";
|
||||||
|
import SMPage from "../components/SMPage.vue";
|
||||||
|
|
||||||
const applicationStore = useApplicationStore();
|
const applicationStore = useApplicationStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -39,16 +40,20 @@ let pageLoading = ref(true);
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (route.params.slug) {
|
if (route.params.slug) {
|
||||||
try {
|
try {
|
||||||
let res = await axios.get(
|
let res = await api.get({
|
||||||
`posts?slug==${route.params.slug}&limit=1`
|
url: "/posts",
|
||||||
);
|
params: {
|
||||||
if (!res.data.posts) {
|
slug: `=${route.params.slug}`,
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res.json.posts) {
|
||||||
error.value = 500;
|
error.value = 500;
|
||||||
} else {
|
} else {
|
||||||
if (res.data.total == 0) {
|
if (res.json.total == 0) {
|
||||||
error.value = 404;
|
error.value = 404;
|
||||||
} else {
|
} else {
|
||||||
post.value = res.data.posts[0];
|
post.value = res.json.posts[0];
|
||||||
|
|
||||||
post.value.publish_at = timestampUtcToLocal(
|
post.value.publish_at = timestampUtcToLocal(
|
||||||
post.value.publish_at
|
post.value.publish_at
|
||||||
@@ -57,19 +62,19 @@ const loadData = async () => {
|
|||||||
applicationStore.setDynamicTitle(post.value.title);
|
applicationStore.setDynamicTitle(post.value.title);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await axios.get(
|
let result = await api.get({
|
||||||
`media/${post.value.hero}`
|
url: `/media/${post.value.hero}`,
|
||||||
);
|
});
|
||||||
post.value.hero_url = result.data.medium.url;
|
post.value.hero_url = result.json.medium.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await axios.get(
|
let result = await api.get({
|
||||||
`users/${post.value.user_id}`
|
url: `/users/${post.value.user_id}`,
|
||||||
);
|
});
|
||||||
post.value.user_username = result.data.user.username;
|
post.value.user_username = result.json.user.username;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
@@ -144,12 +149,15 @@ loadData();
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin-top: map-get($spacer, 4);
|
margin-top: map-get($spacer, 4);
|
||||||
line-height: 1.5rem;
|
|
||||||
padding: 0 map-get($spacer, 3);
|
padding: 0 map-get($spacer, 3);
|
||||||
|
|
||||||
a span {
|
a span {
|
||||||
color: $primary-color !important;
|
color: $primary-color !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMContainer>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog :narrow="formDone" :loading="formLoading">
|
<SMDialog :narrow="formDone">
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Register</h1>
|
<h1>Register</h1>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
|
||||||
:type="formMessage.type"
|
|
||||||
:message="formMessage.message"
|
|
||||||
:icon="formMessage.icon" />
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput control="username" />
|
||||||
v-model="formData.username.value"
|
|
||||||
label="Username"
|
|
||||||
required
|
|
||||||
:error="formData.username.error"
|
|
||||||
@blur="
|
|
||||||
fieldValidate(formData.username)
|
|
||||||
"></SMInput>
|
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="formData.password.value"
|
control="password"
|
||||||
type="password"
|
type="password"></SMInput>
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
:error="formData.password.error"
|
|
||||||
@blur="
|
|
||||||
fieldValidate(formData.password)
|
|
||||||
"></SMInput>
|
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput control="first_name" />
|
||||||
v-model="formData.first_name.value"
|
|
||||||
label="First Name"
|
|
||||||
required
|
|
||||||
:error="formData.first_name.error"
|
|
||||||
@blur="
|
|
||||||
fieldValidate(formData.first_name)
|
|
||||||
" />
|
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput control="last_name" />
|
||||||
v-model="formData.last_name.value"
|
|
||||||
label="Last Name"
|
|
||||||
required
|
|
||||||
:error="formData.last_name.error"
|
|
||||||
@blur="fieldValidate(formData.last_name)" />
|
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput control="email" />
|
||||||
v-model="formData.email.value"
|
|
||||||
label="Email"
|
|
||||||
required
|
|
||||||
:error="formData.email.error"
|
|
||||||
@blur="fieldValidate(formData.email)" />
|
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput control="phone">
|
||||||
v-model="formData.phone.value"
|
This field is optional.
|
||||||
label="Phone Number"
|
</SMInput>
|
||||||
:error="formData.phone.error"
|
|
||||||
@blur="fieldValidate(formData.phone)" />
|
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMCaptchaNotice />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div>
|
<div>
|
||||||
@@ -85,10 +47,10 @@
|
|||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Register"
|
label="Register"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Email Sent!</h1>
|
<h1>Email Sent!</h1>
|
||||||
@@ -113,154 +75,95 @@ import SMInput from "../components/SMInput.vue";
|
|||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMForm from "../components/SMForm.vue";
|
||||||
import axios from "axios";
|
import { api } from "../helpers/api";
|
||||||
|
import { FormControl, FormObject } from "../helpers/form";
|
||||||
import {
|
import {
|
||||||
useValidation,
|
And,
|
||||||
isValidated,
|
Custom,
|
||||||
fieldValidate,
|
Email,
|
||||||
restParseErrors,
|
Min,
|
||||||
} from "../helpers/validation";
|
Password,
|
||||||
|
Phone,
|
||||||
|
Required,
|
||||||
|
} from "../helpers/validate";
|
||||||
|
|
||||||
import { debounce } from "../helpers/common";
|
import { debounce } from "../helpers/common";
|
||||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
|
||||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||||
|
|
||||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const lastUsernameCheck = ref("");
|
|
||||||
const formLoading = ref(false);
|
const checkUsername = (value: string): boolean | string => {
|
||||||
const formDone = ref(false);
|
if (lastUsernameCheck.value != form.username.value) {
|
||||||
const formMessage = reactive({
|
lastUsernameCheck.value = form.username.value;
|
||||||
message: "",
|
api.get({
|
||||||
type: "error",
|
url: "/users",
|
||||||
icon: "",
|
params: {
|
||||||
});
|
username: form.username.value,
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
.then((response) => {
|
||||||
password: {
|
return "The username has already been taken.";
|
||||||
value: "",
|
})
|
||||||
error: "",
|
.catch((error) => {
|
||||||
rules: {
|
if (error.status != 404) {
|
||||||
required: true,
|
return (
|
||||||
required_message: "A password is needed",
|
error.json?.message ||
|
||||||
min: 8,
|
"An unexpected server error occurred."
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
if (
|
await recaptchaLoaded();
|
||||||
formData.username.value.length >= 4 &&
|
const captcha = await executeRecaptcha("submit");
|
||||||
lastUsernameCheck.value != formData.username.value
|
|
||||||
) {
|
await api.post({
|
||||||
lastUsernameCheck.value = formData.username.value;
|
url: "/register",
|
||||||
await axios.get(`users?username=${formData.username.value}`);
|
body: {
|
||||||
formData.username.error = "The username has already been taken.";
|
first_name: form.first_name.value,
|
||||||
}
|
last_name: form.last_name.value,
|
||||||
} catch (error) {
|
email: form.email.value,
|
||||||
if (error.response.status == 404) {
|
phone: form.phone.value,
|
||||||
formData.username.error = "";
|
username: form.username.value,
|
||||||
} else {
|
password: form.password.value,
|
||||||
formMessage.type = "error";
|
captcha_token: captcha,
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
},
|
||||||
formMessage.message =
|
});
|
||||||
error.response.message ||
|
|
||||||
"An unexpected server error occurred.";
|
formDone.value = true;
|
||||||
}
|
} catch (err) {
|
||||||
|
form.apiErrors(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.loading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lastUsernameCheck = ref("");
|
||||||
|
|
||||||
const debouncedFilter = debounce(checkUsername, 1000);
|
const debouncedFilter = debounce(checkUsername, 1000);
|
||||||
let oldUsernameValue = "";
|
let oldUsernameValue = "";
|
||||||
watch(
|
watch(
|
||||||
formData,
|
form,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (value.username.value !== oldUsernameValue) {
|
if (value.username.value !== oldUsernameValue) {
|
||||||
oldUsernameValue = value.username.value;
|
oldUsernameValue = value.username.value;
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog narrow>
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Resend Verify Email</h1>
|
<h1>Resend Verify Email</h1>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="username" />
|
||||||
: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 />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div>
|
<div class="small">
|
||||||
<span class="pr-1">Stuck?</span
|
<span class="pr-1">Stuck?</span
|
||||||
><router-link to="/contact"
|
><router-link to="/contact"
|
||||||
>Contact Us</router-link
|
>Contact Us</router-link
|
||||||
@@ -31,10 +19,10 @@
|
|||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Send"
|
label="Send"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Email Sent!</h1>
|
<h1>Email Sent!</h1>
|
||||||
@@ -51,7 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -60,69 +48,45 @@ import SMInput from "../components/SMInput.vue";
|
|||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||||
import SMDialog from "../components/SMDialog.vue";
|
import SMDialog from "../components/SMDialog.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMForm from "../components/SMForm.vue";
|
||||||
import axios from "axios";
|
import SMPage from "../components/SMPage.vue";
|
||||||
import { useRoute } from "vue-router";
|
import { api } from "../helpers/api";
|
||||||
import {
|
import { Required } from "../helpers/validate";
|
||||||
useValidation,
|
|
||||||
isValidated,
|
|
||||||
fieldValidate,
|
|
||||||
restParseErrors,
|
|
||||||
} from "../helpers/validation";
|
|
||||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
|
||||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||||
|
import { FormObject, FormControl } from "../helpers/form";
|
||||||
|
|
||||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
|
||||||
const formDone = ref(false);
|
const formDone = ref(false);
|
||||||
const formMessage = reactive({
|
const form = reactive(
|
||||||
message: "",
|
FormObject({
|
||||||
type: "error",
|
username: FormControl("", Required()),
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useValidation(formData);
|
const handleSubmit = async () => {
|
||||||
|
form.loading(true);
|
||||||
const submit = async () => {
|
|
||||||
formLoading.value = true;
|
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
let res = await axios.post("users/resendVerifyEmailCode", {
|
await api.post({
|
||||||
username: formData.username.value,
|
url: "/users/resendVerifyEmailCode",
|
||||||
|
body: {
|
||||||
|
username: form.username.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
}
|
} catch (error) {
|
||||||
} catch (err) {
|
if (error.status == 422) {
|
||||||
if (err.response.status == 422) {
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
} else {
|
} else {
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
form.apiErrors(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog narrow>
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Reset Password</h1>
|
<h1>Reset Password</h1>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="code" />
|
||||||
:type="formMessage.type"
|
<SMInput control="password" type="password" />
|
||||||
: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 />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #left>
|
<template #left>
|
||||||
<div>
|
<div class="small">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'forgot-password' }"
|
:to="{ name: 'forgot-password' }"
|
||||||
>Resend Code</router-link
|
>Resend Code</router-link
|
||||||
@@ -39,10 +20,10 @@
|
|||||||
<SMButton
|
<SMButton
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Reset Password"
|
label="Reset Password"
|
||||||
icon="fa-solid fa-arrow-right" />
|
icon="arrow-forward-outline" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Password Reset!</h1>
|
<h1>Password Reset!</h1>
|
||||||
@@ -57,94 +38,57 @@
|
|||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { 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 { 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 { 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 { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
|
||||||
const formDone = ref(false);
|
const formDone = ref(false);
|
||||||
const formMessage = reactive({
|
const form = reactive(
|
||||||
message: "",
|
FormObject({
|
||||||
type: "error",
|
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||||
icon: "",
|
password: FormControl("", And([Required(), Password()])),
|
||||||
});
|
})
|
||||||
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);
|
|
||||||
|
|
||||||
if (useRoute().query.code !== undefined) {
|
if (useRoute().query.code !== undefined) {
|
||||||
formData.code.value = useRoute().query.code;
|
form.code.value = useRoute().query.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const handleSubmit = async () => {
|
||||||
formLoading.value = true;
|
form.loading(true);
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
let res = await axios.post("users/resetPassword", {
|
await api.post({
|
||||||
code: formData.code.value,
|
url: "/users/resetPassword",
|
||||||
password: formData.password.value,
|
body: {
|
||||||
|
code: form.code.value,
|
||||||
|
password: form.password.value,
|
||||||
captcha_token: captcha,
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
}
|
} catch (error) {
|
||||||
} catch (err) {
|
form.apiError(error);
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer class="rules">
|
<SMPage class="rules">
|
||||||
<h1>Rules</h1>
|
<h1>Rules</h1>
|
||||||
<p>
|
<p>
|
||||||
Oh gosh, no body likes rules but to ensure that we have a fun,
|
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.
|
grief other players builds outside of the Survival game-mode.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SMPage from "../components/SMPage.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.rules {
|
.rules {
|
||||||
h2 {
|
h2 {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer class="terms">
|
<SMPage class="terms">
|
||||||
<h1>Terms and Conditions</h1>
|
<h1>Terms and Conditions</h1>
|
||||||
<p>
|
<p>
|
||||||
Please read these terms carefully. By accessing or using our website
|
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
|
be responsible for warranty and after sales service but this is
|
||||||
allowed under the Law.
|
allowed under the Law.
|
||||||
</p>
|
</p>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SMPage from "../components/SMPage.vue";
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMDialog narrow :loading="formLoading">
|
<SMDialog narrow>
|
||||||
<template v-if="!formDone">
|
<template v-if="!formDone">
|
||||||
<h1>Unsubscribe</h1>
|
<h1>Unsubscribe</h1>
|
||||||
<p>
|
<p>
|
||||||
If you would like to unsubscribe from our mailing list,
|
If you would like to unsubscribe from our mailing list,
|
||||||
you have come to the right page!
|
you have come to the right page!
|
||||||
</p>
|
</p>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
<SMInput control="email" />
|
||||||
: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 />
|
|
||||||
<SMFormFooter>
|
<SMFormFooter>
|
||||||
<template #right>
|
<template #right>
|
||||||
<SMButton type="submit" label="Unsubscribe" />
|
<SMButton type="submit" label="Unsubscribe" />
|
||||||
</template>
|
</template>
|
||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</form>
|
</SMForm>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h1>Unsubscribed</h1>
|
<h1>Unsubscribed</h1>
|
||||||
@@ -42,79 +30,56 @@
|
|||||||
</template>
|
</template>
|
||||||
</SMDialog>
|
</SMDialog>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { 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 { 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 { 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 { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||||
const formLoading = ref(false);
|
|
||||||
const formDone = ref(false);
|
const formDone = ref(false);
|
||||||
const formMessage = reactive({
|
const form = reactive(
|
||||||
message: "",
|
FormObject({
|
||||||
type: "error",
|
email: FormControl("", And([Required(), Email()])),
|
||||||
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.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useValidation(formData);
|
const handleSubmit = async () => {
|
||||||
|
form.loading(true);
|
||||||
const submit = async () => {
|
|
||||||
formLoading.value = true;
|
|
||||||
formMessage.type = "error";
|
|
||||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
|
||||||
formMessage.message = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isValidated(formData)) {
|
await recaptchaLoaded();
|
||||||
await recaptchaLoaded();
|
const captcha = await executeRecaptcha("submit");
|
||||||
const captcha = await executeRecaptcha("submit");
|
|
||||||
|
|
||||||
await axios.delete("subscriptions", {
|
await api.delete({
|
||||||
data: {
|
url: "/subscriptions",
|
||||||
email: formData.email.value,
|
body: {
|
||||||
captcha_token: captcha,
|
email: form.email.value,
|
||||||
},
|
captcha_token: captcha,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formDone.value = true;
|
formDone.value = true;
|
||||||
}
|
} catch (error) {
|
||||||
} catch (err) {
|
form.apiErrors(error);
|
||||||
restParseErrors(formData, [formMessage, "message"], err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
form.loading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (useRoute().query.email !== undefined) {
|
if (useRoute().query.email !== undefined) {
|
||||||
formData.email.value = useRoute().query.email;
|
form.email.value = useRoute().query.email;
|
||||||
submit();
|
handleSubmit();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer class="mx-auto workshop-list">
|
<SMPage class="mx-auto workshop-list">
|
||||||
<h1>Workshops</h1>
|
<h1>Workshops</h1>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<SMInput
|
<SMInput
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
event.location == 'online' ? 'Online Event' : event.address
|
event.location == 'online' ? 'Online Event' : event.address
|
||||||
"></SMPanel>
|
"></SMPanel>
|
||||||
</SMPanelList>
|
</SMPanelList>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -49,15 +49,14 @@ import SMInput from "../components/SMInput.vue";
|
|||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMMessage from "../components/SMMessage.vue";
|
||||||
import SMPanelList from "../components/SMPanelList.vue";
|
import SMPanelList from "../components/SMPanelList.vue";
|
||||||
import SMPanel from "../components/SMPanel.vue";
|
import SMPanel from "../components/SMPanel.vue";
|
||||||
|
import SMPage from "../components/SMPage.vue";
|
||||||
import { reactive, ref } from "vue";
|
import { reactive, ref } from "vue";
|
||||||
import axios from "axios";
|
import { api } from "../helpers/api";
|
||||||
import {
|
import {
|
||||||
buildUrlQuery,
|
|
||||||
timestampLocalToUtc,
|
timestampLocalToUtc,
|
||||||
timestampNowUtc,
|
timestampNowUtc,
|
||||||
timestampUtcToLocal,
|
timestampUtcToLocal,
|
||||||
} from "../helpers/common";
|
} from "../helpers/datetime";
|
||||||
import { format, parse, parseISO } from "date-fns";
|
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const events = reactive([]);
|
const events = reactive([]);
|
||||||
@@ -103,11 +102,13 @@ const handleLoad = async () => {
|
|||||||
query["end_at"] = ">" + timestampNowUtc();
|
query["end_at"] = ">" + timestampNowUtc();
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = buildUrlQuery("events", query);
|
let result = await api.get({
|
||||||
let result = await axios.get(url);
|
url: "/events",
|
||||||
|
params: query,
|
||||||
|
});
|
||||||
|
|
||||||
if (result.data.events) {
|
if (result.json.events) {
|
||||||
events.value = result.data.events;
|
events.value = result.json.events;
|
||||||
|
|
||||||
events.value.forEach((item) => {
|
events.value.forEach((item) => {
|
||||||
item.start_at = timestampUtcToLocal(item.start_at);
|
item.start_at = timestampUtcToLocal(item.start_at);
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<div
|
<div
|
||||||
class="workshop-image"
|
class="workshop-image"
|
||||||
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
||||||
<font-awesome-icon
|
<ion-icon
|
||||||
v-if="imageUrl.length == 0"
|
v-if="imageUrl.length == 0"
|
||||||
class="workshop-image-loader"
|
class="workshop-image-loader"
|
||||||
icon="fa-regular fa-image" />
|
name="image-outline" />
|
||||||
</div>
|
</div>
|
||||||
<template #inner>
|
<template #inner>
|
||||||
<SMMessage
|
<SMMessage
|
||||||
@@ -57,10 +57,7 @@
|
|||||||
label="Register for Event"></SMButton>
|
label="Register for Event"></SMButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="workshop-date">
|
<div class="workshop-date">
|
||||||
<h4>
|
<h4><ion-icon name="calendar-outline" />Date / Time</h4>
|
||||||
<font-awesome-icon
|
|
||||||
icon="fa-regular fa-calendar" />Date / Time
|
|
||||||
</h4>
|
|
||||||
<p
|
<p
|
||||||
v-for="(line, index) in workshopDate"
|
v-for="(line, index) in workshopDate"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -69,10 +66,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="workshop-location">
|
<div class="workshop-location">
|
||||||
<h4>
|
<h4><ion-icon name="location-outline" />Location</h4>
|
||||||
<font-awesome-icon
|
|
||||||
icon="fa-solid fa-location-dot" />Location
|
|
||||||
</h4>
|
|
||||||
<p>
|
<p>
|
||||||
{{
|
{{
|
||||||
event.location == "online"
|
event.location == "online"
|
||||||
@@ -88,19 +82,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios";
|
import { api } from "../helpers/api";
|
||||||
import { computed, ref, reactive } from "vue";
|
import { computed, ref, reactive } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useApplicationStore } from "../store/ApplicationStore";
|
import { useApplicationStore } from "../store/ApplicationStore";
|
||||||
import { format } from "date-fns";
|
|
||||||
import SMButton from "../components/SMButton.vue";
|
import SMButton from "../components/SMButton.vue";
|
||||||
import SMHTML from "../components/SMHTML.vue";
|
import SMHTML from "../components/SMHTML.vue";
|
||||||
import SMMessage from "../components/SMMessage.vue";
|
import SMMessage from "../components/SMMessage.vue";
|
||||||
import {
|
import {
|
||||||
|
format,
|
||||||
timestampUtcToLocal,
|
timestampUtcToLocal,
|
||||||
timestampBeforeNow,
|
timestampBeforeNow,
|
||||||
timestampAfterNow,
|
timestampAfterNow,
|
||||||
} from "../helpers/common";
|
} from "../helpers/datetime";
|
||||||
|
|
||||||
const applicationStore = useApplicationStore();
|
const applicationStore = useApplicationStore();
|
||||||
const event = ref({});
|
const event = ref({});
|
||||||
@@ -162,8 +156,8 @@ const handleLoad = async () => {
|
|||||||
formMessage.message = "";
|
formMessage.message = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await axios.get(`events/${route.params.id}`);
|
const result = await api.get(`events/${route.params.id}`);
|
||||||
event.value = result.data.event;
|
event.value = result.json.event;
|
||||||
|
|
||||||
event.value.start_at = timestampUtcToLocal(event.value.start_at);
|
event.value.start_at = timestampUtcToLocal(event.value.start_at);
|
||||||
event.value.end_at = timestampUtcToLocal(event.value.end_at);
|
event.value.end_at = timestampUtcToLocal(event.value.end_at);
|
||||||
@@ -172,16 +166,16 @@ const handleLoad = async () => {
|
|||||||
handleLoadImage();
|
handleLoadImage();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
formMessage.message =
|
formMessage.message =
|
||||||
error.response?.data?.message ||
|
error.json?.message ||
|
||||||
"Could not load event information from the server.";
|
"Could not load event information from the server.";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoadImage = async () => {
|
const handleLoadImage = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await axios.get(`media/${event.value.hero}`);
|
const result = await api.get(`media/${event.value.hero}`);
|
||||||
if (result.data.medium) {
|
if (result.json.medium) {
|
||||||
imageUrl.value = result.data.medium.url;
|
imageUrl.value = result.json.medium.url;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* empty */
|
/* empty */
|
||||||
|
|||||||
@@ -1,118 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer>
|
<SMPage>
|
||||||
<SMMessage
|
<SMForm v-model="form" @submit="handleSubmit">
|
||||||
v-if="formMessage.message"
|
|
||||||
:type="formMessage.type"
|
|
||||||
:message="formMessage.message"
|
|
||||||
:icon="formMessage.icon" />
|
|
||||||
<form @submit.prevent="submit">
|
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMInput
|
<SMInput control="title" />
|
||||||
v-model="formData.title.value"
|
|
||||||
label="Title"
|
|
||||||
required
|
|
||||||
:error="formData.title.error"
|
|
||||||
@blur="fieldValidate(formData.title)" />
|
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMEditor
|
<SMEditor
|
||||||
id="content"
|
id="content"
|
||||||
v-model="formData.content.value"
|
v-model="form.content.value"
|
||||||
@file-accept="fileAccept"
|
@file-accept="fileAccept"
|
||||||
@attachment-add="attachmentAdd" />
|
@attachment-add="attachmentAdd" />
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMButton type="submit" label="Save" />
|
<SMButton type="submit" label="Save" />
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</form>
|
</SMForm>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
import { api } from "../../helpers/api";
|
||||||
import DEditor from "../../components/SMEditor.vue";
|
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 SMInput from "../../components/SMInput.vue";
|
||||||
import SMButton from "../../components/SMButton.vue";
|
import SMButton from "../../components/SMButton.vue";
|
||||||
import SMDialog from "../../components/SMDialog.vue";
|
import SMPage from "../../components/SMPage.vue";
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
import SMForm from "../../components/SMForm.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";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const userStore = useUserStore();
|
const form = reactive(
|
||||||
const formMessage = reactive({
|
FormObject({
|
||||||
icon: "",
|
title: FormControl("", And([Required(), Min(2)])),
|
||||||
type: "",
|
content: FormControl("", Required()),
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
if (isValidated(formData)) {
|
await api.post({
|
||||||
let res = await axios.get("posts/" + route.params.id);
|
url: "/posts",
|
||||||
|
body: {
|
||||||
|
title: form.title.value,
|
||||||
|
content: form.content.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
formData.title.value = res.data.title;
|
form.message("The post has been saved", "success");
|
||||||
formData.content.value = res.data.content;
|
} catch (error) {
|
||||||
}
|
form.apiError(error);
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,21 +116,3 @@ const attachmentAdd = async (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
|
||||||
|
|||||||
@@ -1,58 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMContainer class="dashboard mx-auto">
|
<SMPage class="dashboard mx-auto">
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<div class="boxes">
|
<div class="boxes">
|
||||||
<router-link to="/dashboard/details" class="box">
|
<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>
|
<h2>My Details</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('admin/posts')"
|
v-if="userStore.permissions.includes('admin/posts')"
|
||||||
to="/dashboard/posts"
|
to="/dashboard/posts"
|
||||||
class="box">
|
class="box">
|
||||||
<font-awesome-icon icon="fa-regular fa-newspaper" />
|
<ion-icon name="newspaper-outline" />
|
||||||
<h2>Posts</h2>
|
<h2>Posts</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('admin/users')"
|
v-if="userStore.permissions.includes('admin/users')"
|
||||||
:to="{ name: 'user-list' }"
|
:to="{ name: 'user-list' }"
|
||||||
class="box">
|
class="box">
|
||||||
<font-awesome-icon icon="fa-solid fa-users" />
|
<ion-icon name="people-outline" />
|
||||||
<h2>Users</h2>
|
<h2>Users</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('admin/events')"
|
v-if="userStore.permissions.includes('admin/events')"
|
||||||
to="/dashboard/events"
|
to="/dashboard/events"
|
||||||
class="box">
|
class="box">
|
||||||
<font-awesome-icon icon="fa-regular fa-calendar" />
|
<ion-icon name="calendar-outline" />
|
||||||
<h2>Events</h2>
|
<h2>Events</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('admin/courses')"
|
v-if="userStore.permissions.includes('admin/courses')"
|
||||||
to="/dashboard/courses"
|
to="/dashboard/courses"
|
||||||
class="box">
|
class="box">
|
||||||
<font-awesome-icon icon="fa-solid fa-graduation-cap" />
|
<ion-icon name="school-outline" />
|
||||||
<h2>{{ courseBoxTitle }}</h2>
|
<h2>{{ courseBoxTitle }}</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('admin/media')"
|
v-if="userStore.permissions.includes('admin/media')"
|
||||||
to="/dashboard/media"
|
to="/dashboard/media"
|
||||||
class="box">
|
class="box">
|
||||||
<font-awesome-icon icon="fa-solid fa-photo-film" />
|
<ion-icon name="film-outline" />
|
||||||
<h2>Media</h2>
|
<h2>Media</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="userStore.permissions.includes('logs/discord')"
|
v-if="userStore.permissions.includes('logs/discord')"
|
||||||
:to="{ name: 'discord-bot-logs' }"
|
:to="{ name: 'discord-bot-logs' }"
|
||||||
class="box">
|
class="box">
|
||||||
<font-awesome-icon icon="fa-brands fa-discord" />
|
<ion-icon name="logo-discord" />
|
||||||
<h2>Discord Bot Logs</h2>
|
<h2>Discord Bot Logs</h2>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</SMContainer>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import SMPage from "../../components/SMPage.vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useUserStore } from "../../store/UserStore";
|
import { useUserStore } from "../../store/UserStore";
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ const courseBoxTitle = computed(() => {
|
|||||||
font-size: map-get($spacer, 3);
|
font-size: map-get($spacer, 3);
|
||||||
color: $font-color;
|
color: $font-color;
|
||||||
transition: background-color 0.3s, border 0.3s;
|
transition: background-color 0.3s, border 0.3s;
|
||||||
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@@ -97,7 +99,7 @@ const courseBoxTitle = computed(() => {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
ion-icon {
|
||||||
font-size: map-get($spacer, 5);
|
font-size: map-get($spacer, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ import {
|
|||||||
} from "../../helpers/validation";
|
} from "../../helpers/validation";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { timestampLocalToUtc, timestampUtcToLocal } from "../../helpers/common";
|
import { timestampLocalToUtc, timestampUtcToLocal } from "../../helpers/common";
|
||||||
import { parseISO } from "date-fns";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const formLoading = ref(false);
|
const formLoading = ref(false);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
:items="items"
|
:items="items"
|
||||||
:search-value="search">
|
:search-value="search">
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
</template>
|
</template>
|
||||||
<template #item-title="item">
|
<template #item-title="item">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #item-actions="item">
|
<template #item-actions="item">
|
||||||
<div class="action-wrapper">
|
<div class="action-wrapper">
|
||||||
<font-awesome-icon
|
<!-- <font-awesome-icon
|
||||||
icon="fa-solid fa-pen-to-square"
|
icon="fa-solid fa-pen-to-square"
|
||||||
@click="handleEdit(item)" />
|
@click="handleEdit(item)" />
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
icon="fa-regular fa-trash-can"
|
icon="fa-regular fa-trash-can"
|
||||||
@click="handleDelete(item)" />
|
@click="handleDelete(item)" /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</EasyDataTable>
|
</EasyDataTable>
|
||||||
@@ -66,6 +66,7 @@ import { debounce } from "../../helpers/common";
|
|||||||
import SMHeading from "../../components/SMHeading.vue";
|
import SMHeading from "../../components/SMHeading.vue";
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
import SMMessage from "../../components/SMMessage.vue";
|
||||||
import { restParseErrors } from "../../helpers/validation";
|
import { restParseErrors } from "../../helpers/validation";
|
||||||
|
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const search = ref("");
|
const search = ref("");
|
||||||
|
|||||||
@@ -29,19 +29,19 @@
|
|||||||
:items="items"
|
:items="items"
|
||||||
:search-value="search">
|
:search-value="search">
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
</template>
|
</template>
|
||||||
<template #item-size="item">
|
<template #item-size="item">
|
||||||
{{ bytesReadable(item.size) }}
|
{{ bytesReadable(item.size) }}
|
||||||
</template>
|
</template>
|
||||||
<template #item-actions="item">
|
<template #item-actions="item">
|
||||||
<div class="action-wrapper">
|
<div class="action-wrapper">
|
||||||
<font-awesome-icon
|
<!-- <font-awesome-icon
|
||||||
icon="fa-solid fa-pen-to-square"
|
icon="fa-solid fa-pen-to-square"
|
||||||
@click.stop="handleEdit(item)" />
|
@click.stop="handleEdit(item)" />
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
icon="fa-regular fa-trash-can"
|
icon="fa-regular fa-trash-can"
|
||||||
@click.stop="handleDelete(item)" />
|
@click.stop="handleDelete(item)" /> -->
|
||||||
<d-file-link :href="item.url" target="_blank" @click.stop=""
|
<d-file-link :href="item.url" target="_blank" @click.stop=""
|
||||||
><font-awesome-icon icon="fa-solid fa-download"
|
><font-awesome-icon icon="fa-solid fa-download"
|
||||||
/></d-file-link>
|
/></d-file-link>
|
||||||
@@ -65,6 +65,7 @@ import { debounce, parseErrorType, bytesReadable } from "../../helpers/common";
|
|||||||
import SMMessage from "../../components/SMMessage.vue";
|
import SMMessage from "../../components/SMMessage.vue";
|
||||||
import DFileLink from "../../components/DFileLink.vue";
|
import DFileLink from "../../components/DFileLink.vue";
|
||||||
import { useUserStore } from "../../store/UserStore";
|
import { useUserStore } from "../../store/UserStore";
|
||||||
|
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const search = ref("");
|
const search = ref("");
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
:items="items"
|
:items="items"
|
||||||
:search-value="search">
|
:search-value="search">
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
</template>
|
</template>
|
||||||
<template #item-title="item">
|
<template #item-title="item">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #item-actions="item">
|
<template #item-actions="item">
|
||||||
<div class="action-wrapper">
|
<div class="action-wrapper">
|
||||||
<font-awesome-icon
|
<!-- <font-awesome-icon
|
||||||
icon="fa-solid fa-pen-to-square"
|
icon="fa-solid fa-pen-to-square"
|
||||||
@click="handleEdit(item)" />
|
@click="handleEdit(item)" />
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
icon="fa-regular fa-trash-can"
|
icon="fa-regular fa-trash-can"
|
||||||
@click="handleDelete(item)" />
|
@click="handleDelete(item)" /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</EasyDataTable>
|
</EasyDataTable>
|
||||||
@@ -62,6 +62,7 @@ import SMButton from "../../components/SMButton.vue";
|
|||||||
import { debounce } from "../../helpers/common";
|
import { debounce } from "../../helpers/common";
|
||||||
import SMHeading from "../../components/SMHeading.vue";
|
import SMHeading from "../../components/SMHeading.vue";
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
import SMMessage from "../../components/SMMessage.vue";
|
||||||
|
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const search = ref("");
|
const search = ref("");
|
||||||
|
|||||||
@@ -16,16 +16,16 @@
|
|||||||
:header-item-class-name="headerItemClassNameFunction"
|
:header-item-class-name="headerItemClassNameFunction"
|
||||||
:body-item-class-name="bodyItemClassNameFunction">
|
:body-item-class-name="bodyItemClassNameFunction">
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
<SMLoadingIcon />
|
||||||
</template>
|
</template>
|
||||||
<template #item-actions="item">
|
<template #item-actions="item">
|
||||||
<div class="action-wrapper">
|
<div class="action-wrapper">
|
||||||
<font-awesome-icon
|
<!-- <font-awesome-icon
|
||||||
icon="fa-solid fa-pen-to-square"
|
icon="fa-solid fa-pen-to-square"
|
||||||
@click="handleEdit(item)" />
|
@click="handleEdit(item)" />
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
icon="fa-regular fa-trash-can"
|
icon="fa-regular fa-trash-can"
|
||||||
@click="handleDelete(item)" />
|
@click="handleDelete(item)" /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</EasyDataTable>
|
</EasyDataTable>
|
||||||
@@ -42,6 +42,7 @@ import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
|||||||
import { openDialog } from "vue3-promise-dialog";
|
import { openDialog } from "vue3-promise-dialog";
|
||||||
import SMHeading from "../../components/SMHeading.vue";
|
import SMHeading from "../../components/SMHeading.vue";
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
import SMMessage from "../../components/SMMessage.vue";
|
||||||
|
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchValue = ref("");
|
const searchValue = ref("");
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
@vite('resources/js/main.js')
|
@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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export default defineConfig({
|
|||||||
vue({
|
vue({
|
||||||
template: {
|
template: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
isCustomElement: (tag) => ["trix-editor"].includes(tag),
|
isCustomElement: (tag) =>
|
||||||
|
["trix-editor", "ion-icon"].includes(tag),
|
||||||
},
|
},
|
||||||
transformAssetUrls: {
|
transformAssetUrls: {
|
||||||
base: null,
|
base: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user