drop axios/date-fns/fontawesome
This commit is contained in:
@@ -240,7 +240,7 @@ class UserController extends ApiController
|
||||
}
|
||||
|
||||
return $this->respondError([
|
||||
'code' => 'The code was not found or has expired'
|
||||
'code' => 'The code was not found or has expired.'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ class UserController extends ApiController
|
||||
}//end if
|
||||
|
||||
return $this->respondWithErrors([
|
||||
'code' => 'The code was not found or has expired'
|
||||
'code' => 'The code was not found or has expired.'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vuepic/vue-datepicker": "^3.6.4",
|
||||
"date-fns": "^2.29.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"element-plus": "^2.2.27",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persistedstate": "^3.0.1",
|
||||
@@ -32,7 +25,6 @@
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"axios": "^1.1.2",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
@@ -56,22 +48,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
|
||||
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@element-plus/icons-vue": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.0.10.tgz",
|
||||
"integrity": "sha512-ygEZ1mwPjcPo/OulhzLE7mtDrQBWI8vZzEWSNB2W/RNCRjoQGwbaK4N8lV4rid7Ts4qvySU3njMN7YCiSlSaTQ==",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@es-joy/jsdoccomment": {
|
||||
"version": "0.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz",
|
||||
@@ -439,85 +415,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.1.1.tgz",
|
||||
"integrity": "sha512-PL7g3dhA4dHgZfujkuD8Q+tfJJynEtnNQSPzmucCnxMvkxf4cLBJw/ZYqZUn4HCh33U3WHrAfv2R2tbi9UCSmw=="
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
|
||||
"integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz",
|
||||
"integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz",
|
||||
"integrity": "sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.2.1.tgz",
|
||||
"integrity": "sha512-L8l4MfdHPmZlJ72PvzdfwOwbwcCAL0vx48tJRnI6u1PJXh+j2f3yDoKyQgO3qjEsgD5Fr2tQV/cPP8F/k6aUig==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.2.1.tgz",
|
||||
"integrity": "sha512-wiqcNDNom75x+pe88FclpKz7aOSqS2lOivZeicMV5KRwOAeypxEYWAK/0v+7r+LrEY30+qzh8r2XDaEHvoLsMA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz",
|
||||
"integrity": "sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/vue-fontawesome": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.3.tgz",
|
||||
"integrity": "sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==",
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"vue": ">= 3.0.0 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||
@@ -644,16 +541,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"name": "@sxzz/popperjs-es",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
|
||||
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.0.tgz",
|
||||
@@ -685,19 +572,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.6.tgz",
|
||||
"integrity": "sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
@@ -710,11 +584,6 @@
|
||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz",
|
||||
@@ -1037,39 +906,6 @@
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.12.0.tgz",
|
||||
"integrity": "sha512-h/Di8Bvf6xRcvS/PvUVheiMYYz3U0tH3X25YxONSaAUBa841ayMwxkuzx/DGUMCW/wHWzD8tRy2zYmOC36r4sg==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.16",
|
||||
"@vueuse/metadata": "9.12.0",
|
||||
"@vueuse/shared": "9.12.0",
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.12.0.tgz",
|
||||
"integrity": "sha512-9oJ9MM9lFLlmvxXUqsR1wLt1uF7EVbP5iYaHJYqk+G2PbMjY6EXvZeTjbdO89HgoF5cI6z49o2zT/jD9SVoNpQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.12.0.tgz",
|
||||
"integrity": "sha512-TWuJLACQ0BVithVTRbex4Wf1a1VaRuSpVeyEd4vMUWl54PzlE0ciFUshKCXnlLuD0lxIaLK4Ypj3NXYzZh4+SQ==",
|
||||
"dependencies": {
|
||||
"vue-demi": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
@@ -1331,28 +1167,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/async-validator": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.1.tgz",
|
||||
"integrity": "sha512-78pWJsQTceInlyaeBQeYZ/QgZeWS8hGeKiIJiDKQe3hEyBb7sEMq0K4gjx+Va6WHTYO4zI/RRl8qGRzn0YMadA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1538,18 +1352,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -1622,11 +1424,6 @@
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.7",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
|
||||
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -1650,15 +1447,6 @@
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -1702,31 +1490,6 @@
|
||||
"integrity": "sha512-47o4PPgxfU1KMNejz+Dgaodf7YTcg48uOfV1oM6cs3adrl2+7R+dHkt3Jpxqo0LRCbGJEzTKMUt0RdvByb/leg==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.2.29",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.29.tgz",
|
||||
"integrity": "sha512-g4dcrURrKkR5uUX8n5RVnnqGnimoki9HfqS4yHHG6XwCHBkZGozdq4x+478BzeWUe31h++BO+7dakSx4VnM8RQ==",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.0.6",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"dayjs": "^1.11.3",
|
||||
"escape-html": "^1.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-unified": "^1.0.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-wheel-es": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
@@ -1799,11 +1562,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -2195,40 +1953,6 @@
|
||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -2638,22 +2362,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
||||
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
||||
"peerDependencies": {
|
||||
"@types/lodash-es": "*",
|
||||
"lodash": "*",
|
||||
"lodash-es": "*"
|
||||
}
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
@@ -2681,11 +2391,6 @@
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -2718,6 +2423,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -2726,6 +2432,7 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -2794,11 +2501,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw=="
|
||||
},
|
||||
"node_modules/normalize.css": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
|
||||
@@ -3034,12 +2736,6 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"axios": "^1.1.2",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
@@ -22,16 +21,9 @@
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vuepic/vue-datepicker": "^3.6.4",
|
||||
"date-fns": "^2.29.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"element-plus": "^2.2.27",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persistedstate": "^3.0.1",
|
||||
|
||||
BIN
public/img/background.jpg
Normal file
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;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
|
||||
&.required:after {
|
||||
content: " *";
|
||||
color: $danger-color;
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
margin-right: map-get($spacer, 3);
|
||||
|
||||
&:after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 12px;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
color: $font-color;
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
// input,
|
||||
// select,
|
||||
// textarea {
|
||||
// box-sizing: border-box;
|
||||
// display: block;
|
||||
// width: 100%;
|
||||
// border: 1px solid $border-color;
|
||||
// border-radius: 12px;
|
||||
// padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
// color: $font-color;
|
||||
// margin-bottom: map-get($spacer, 4);
|
||||
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
// -webkit-appearance: none;
|
||||
// -moz-appearance: none;
|
||||
// appearance: none;
|
||||
// }
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
// textarea {
|
||||
// resize: none;
|
||||
// }
|
||||
|
||||
select {
|
||||
padding-right: 2.5rem;
|
||||
@@ -178,39 +159,6 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
.loader-cover {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: map-get($spacer, 5) calc(map-get($spacer, 5) * 2);
|
||||
|
||||
border: 1px solid transparent;
|
||||
border-radius: 24px;
|
||||
|
||||
svg {
|
||||
font-size: calc(map-get($spacer, 5) * 1.5);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: map-get($spacer, 4);
|
||||
padding-top: map-get($spacer, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Button */
|
||||
button.button,
|
||||
a.button,
|
||||
@@ -301,53 +249,6 @@ label.button {
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Group */
|
||||
.form-group {
|
||||
margin-bottom: map-get($spacer, 3);
|
||||
padding: 0 4px;
|
||||
flex: 1;
|
||||
|
||||
input,
|
||||
textarea {
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
.form-group-info {
|
||||
font-size: 85%;
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
.form-group-error {
|
||||
// display: none;
|
||||
font-size: 85%;
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
color: $danger-color;
|
||||
}
|
||||
|
||||
.form-group-help {
|
||||
font-size: 85%;
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
color: $secondary-color;
|
||||
|
||||
svg {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
input,
|
||||
textarea,
|
||||
.input-file-group,
|
||||
.input-media-group .input-media-display {
|
||||
border: 2px solid $danger-color;
|
||||
}
|
||||
|
||||
.form-group-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Page Errors */
|
||||
.page-error {
|
||||
display: flex;
|
||||
|
||||
@@ -125,33 +125,33 @@
|
||||
/* Margin */
|
||||
@each $index, $size in $spacer {
|
||||
.m-#{$index} {
|
||||
margin: #{$size};
|
||||
margin: #{$size} !important;
|
||||
}
|
||||
|
||||
.mt-#{$index} {
|
||||
margin-top: #{$size};
|
||||
margin-top: #{$size} !important;
|
||||
}
|
||||
|
||||
.mb-#{$index} {
|
||||
margin-bottom: #{$size};
|
||||
margin-bottom: #{$size} !important;
|
||||
}
|
||||
|
||||
.ml-#{$index} {
|
||||
margin-left: #{$size};
|
||||
margin-left: #{$size} !important;
|
||||
}
|
||||
|
||||
.mr-#{$index} {
|
||||
margin-right: #{$size};
|
||||
margin-right: #{$size} !important;
|
||||
}
|
||||
|
||||
.mx-#{$index} {
|
||||
margin-left: #{$size};
|
||||
margin-right: #{$size};
|
||||
margin-left: #{$size} !important;
|
||||
margin-right: #{$size} !important;
|
||||
}
|
||||
|
||||
.my-#{$index} {
|
||||
margin-top: #{$size};
|
||||
margin-bottom: #{$size};
|
||||
margin-top: #{$size} !important;
|
||||
margin-bottom: #{$size} !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
resources/js/bootstrap.js
vendored
8
resources/js/bootstrap.js
vendored
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import _ from "lodash";
|
||||
window._ = _;
|
||||
|
||||
/**
|
||||
@@ -7,10 +7,10 @@ window._ = _;
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
// import axios from 'axios';
|
||||
// window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
/**
|
||||
* Echo exposes an expressive API for subscribing to channels and listening
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
]"
|
||||
:type="buttonType">
|
||||
{{ label }}
|
||||
<font-awesome-icon v-if="icon" :icon="icon" />
|
||||
<ion-icon v-if="icon" :icon="icon" />
|
||||
</a>
|
||||
<button
|
||||
v-else-if="to == null"
|
||||
@@ -24,7 +24,7 @@
|
||||
]"
|
||||
:type="buttonType">
|
||||
{{ label }}
|
||||
<font-awesome-icon v-if="icon" :icon="icon" />
|
||||
<ion-icon v-if="icon" :icon="icon" />
|
||||
</button>
|
||||
<router-link
|
||||
v-else
|
||||
@@ -37,7 +37,7 @@
|
||||
{ 'button-block': block },
|
||||
]">
|
||||
{{ label }}
|
||||
<font-awesome-icon v-if="icon" :icon="icon" />
|
||||
<ion-icon v-if="icon" :icon="icon" />
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
@@ -83,5 +83,11 @@ const classType = props.type == "submit" ? "primary" : props.type;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,22 +7,20 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="carousel-slide-prev" @click="handleSlidePrev">
|
||||
<font-awesome-icon icon="fa-solid fa-chevron-left" />
|
||||
<ion-icon name="chevron-back-outline" />
|
||||
</div>
|
||||
<div class="carousel-slide-next" @click="handleSlideNext">
|
||||
<font-awesome-icon icon="fa-solid fa-chevron-right" />
|
||||
<ion-icon name="chevron-forward-outline" />
|
||||
</div>
|
||||
<div class="carousel-slide-indicators">
|
||||
<div
|
||||
v-for="(indicator, index) in slideElements"
|
||||
:key="index"
|
||||
class="carousel-slide-indicator-dot">
|
||||
<font-awesome-icon
|
||||
v-if="currentSlide != index"
|
||||
icon="fa-regular fa-circle"
|
||||
@click="handleIndicator(index)" />
|
||||
<font-awesome-icon v-else icon="fa-solid fa-circle" />
|
||||
</div>
|
||||
:class="[
|
||||
'carousel-slide-indicator-item',
|
||||
{ highlighted: currentSlide == index },
|
||||
]"
|
||||
@click="handleIndicator(index)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -184,12 +182,20 @@ const disconnectMutationObserver = () => {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
.carousel-slide-indicator-item {
|
||||
height: map-get($spacer, 1);
|
||||
width: map-get($spacer, 1);
|
||||
border: 1px solid white;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 80%;
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 calc(#{map-get($spacer, 1)} / 3);
|
||||
color: #fff;
|
||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||
|
||||
&.highlighted {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="carousel-slide"
|
||||
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
||||
<div v-if="imageUrl == null" class="carousel-slide-loading">
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
</div>
|
||||
<div v-else class="carousel-slide-body">
|
||||
<div class="carousel-slide-content">
|
||||
@@ -20,9 +20,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { ref } from "vue";
|
||||
import { api } from "../helpers/api";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -56,9 +57,9 @@ let imageUrl = ref(null);
|
||||
|
||||
const handleLoad = async () => {
|
||||
try {
|
||||
let result = await axios.get(`media/${props.image}`);
|
||||
if (result.data.medium) {
|
||||
imageUrl.value = result.data.medium.url;
|
||||
let result = await api.get(`/media/${props.image}`);
|
||||
if (result.json.medium) {
|
||||
imageUrl.value = result.json.medium.url;
|
||||
}
|
||||
} catch (error) {
|
||||
imageUrl.value = "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="['container', { full: isFull }]">
|
||||
<div :class="['container', { full: isFull }]" :style="styleObject">
|
||||
<SMLoader :loading="loading">
|
||||
<d-error-forbidden
|
||||
v-if="pageError == 403 || !hasPermission()"></d-error-forbidden>
|
||||
@@ -50,9 +50,19 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
const slots = useSlots();
|
||||
const userStore = useUserStore();
|
||||
let styleObject = {};
|
||||
|
||||
if (props.background != "") {
|
||||
styleObject["backgroundImage"] = `url('${props.background}')`;
|
||||
}
|
||||
|
||||
const hasPermission = () => {
|
||||
return (
|
||||
@@ -76,6 +86,9 @@ const isFull = computed(() => {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
&.full {
|
||||
padding-left: 0;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="help" class="form-group-help">
|
||||
<font-awesome-icon v-if="helpIcon" :icon="helpIcon" />
|
||||
<!-- <font-awesome-icon v-if="helpIcon" :icon="helpIcon" /> -->
|
||||
{{ help }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="dialog-loading-cover">
|
||||
<div class="dialog-loading">
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
<span>{{ loadingMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,6 +18,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
@@ -50,6 +52,7 @@ defineProps({
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
min-width: map-get($spacer, 5) * 12;
|
||||
box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.5);
|
||||
|
||||
& > h1 {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -5,33 +5,33 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://facebook.com/stemmechanics"
|
||||
><font-awesome-icon icon="fa-brands fa-facebook"
|
||||
/></a>
|
||||
><ion-icon name="logo-facebook"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://mastodon.au/@stemmechanics"
|
||||
><font-awesome-icon icon="fa-brands fa-mastodon"
|
||||
/></a>
|
||||
><ion-icon name="logo-mastodon"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/@stemmechanics"
|
||||
><font-awesome-icon icon="fa-brands fa-youtube"
|
||||
/></a>
|
||||
><ion-icon name="logo-youtube"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/stemmechanics"
|
||||
><font-awesome-icon icon="fa-brands fa-twitter"
|
||||
/></a>
|
||||
><ion-icon name="logo-twitter"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/stemmechanics"
|
||||
><font-awesome-icon icon="fa-brands fa-github"
|
||||
/></a>
|
||||
><ion-icon name="logo-github"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/yNzk4x7mpD"
|
||||
><font-awesome-icon icon="fa-brands fa-discord"
|
||||
/></a>
|
||||
><ion-icon name="logo-discord"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</SMColumn>
|
||||
|
||||
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>
|
||||
<div class="form-footer">
|
||||
<div class="form-footer-column form-footer-column-left">
|
||||
<div class="sm-form-footer">
|
||||
<div class="sm-form-footer-column sm-form-footer-column-left">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
<div class="form-footer-column form-footer-column-right">
|
||||
<div class="sm-form-footer-column sm-form-footer-column-right">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.form-footer {
|
||||
.sm-form-footer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
// margin-bottom: map-get($spacer, 3);
|
||||
|
||||
.form-footer-column {
|
||||
.sm-form-footer-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.form-footer-column-left,
|
||||
&.form-footer-column-right {
|
||||
&.sm-form-footer-column-left,
|
||||
&.sm-form-footer-column-right {
|
||||
a,
|
||||
button {
|
||||
margin-left: map-get($spacer, 1);
|
||||
@@ -36,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.form-footer-column-right {
|
||||
&.sm-form-footer-column-right {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -44,12 +43,12 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.form-footer {
|
||||
.sm-form-footer {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.form-footer-column {
|
||||
&.form-footer-column-left,
|
||||
&.form-footer-column-right {
|
||||
.sm-form-footer-column {
|
||||
&.sm-form-footer-column-left,
|
||||
&.sm-form-footer-column-right {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
justify-content: center;
|
||||
@@ -66,11 +65,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.form-footer-column-left {
|
||||
&.sm-form-footer-column-left {
|
||||
margin-bottom: -#{calc(map-get($spacer, 1) / 2)};
|
||||
}
|
||||
|
||||
&.form-footer-column-right {
|
||||
&.sm-form-footer-column-right {
|
||||
margin-top: -#{calc(map-get($spacer, 1) / 2)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
v-if="back != ''"
|
||||
:to="{ name: back }"
|
||||
class="heading-back">
|
||||
<font-awesome-icon icon="fa-solid fa-arrow-left" />{{ backLabel }}
|
||||
<ion-icon name="arrow-back-outline" />{{ backLabel }}
|
||||
</router-link>
|
||||
<router-link v-if="close != ''" :to="{ name: close }" class="close">
|
||||
<font-awesome-icon icon="fa-solid fa-close" />
|
||||
<ion-icon name="close-outline" />
|
||||
</router-link>
|
||||
<span v-if="closeBack" class="close" @click="handleBack">
|
||||
<font-awesome-icon icon="fa-solid fa-close" />
|
||||
<ion-icon name="close-outline" />
|
||||
</span>
|
||||
<h1>{{ heading }}</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
<template>
|
||||
<div :class="['form-group', { 'has-error': error }]">
|
||||
<label v-if="label" :class="{ required: required, inline: inline }">{{
|
||||
label
|
||||
}}</label>
|
||||
<div
|
||||
:class="[
|
||||
'sm-input-group',
|
||||
{
|
||||
'sm-input-active': inputActive,
|
||||
'sm-feedback-invalid': feedbackInvalid,
|
||||
},
|
||||
]">
|
||||
<label v-if="label">{{ label }}</label>
|
||||
<ion-icon
|
||||
class="sm-invalid-icon"
|
||||
name="alert-circle-outline"></ion-icon>
|
||||
<input
|
||||
v-if="
|
||||
type == 'text' ||
|
||||
type == 'email' ||
|
||||
type == 'password' ||
|
||||
type == 'email' ||
|
||||
type == 'url'
|
||||
"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@input="input"
|
||||
:value="value"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keydown="handleBlur" />
|
||||
@keydown="handleKeydown" />
|
||||
<textarea
|
||||
v-if="type == 'textarea'"
|
||||
rows="5"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@input="input"
|
||||
:value="value"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keydown="handleBlur"></textarea>
|
||||
@keydown="handleKeydown"></textarea>
|
||||
<div v-if="type == 'file'" class="input-file-group">
|
||||
<input
|
||||
id="file"
|
||||
@@ -40,37 +50,31 @@
|
||||
props.modelValue
|
||||
}}</a>
|
||||
<span v-if="type == 'static'">{{ props.modelValue }}</span>
|
||||
<div v-if="type == 'media'" class="input-media-group">
|
||||
<div class="input-media-display">
|
||||
<img v-if="mediaUrl.length > 0" :src="mediaUrl" />
|
||||
<font-awesome-icon v-else icon="fa-regular fa-image" />
|
||||
</div>
|
||||
<div v-if="type == 'media'" class="form-group-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<a class="button" @click.prevent="handleMediaSelect">Select file</a>
|
||||
</div>
|
||||
<div v-if="type != 'media'" class="form-group-error">{{ error }}</div>
|
||||
<div v-if="slots.default" class="form-group-info">
|
||||
<slot></slot>
|
||||
<div v-if="slots.default || feedbackInvalid" class="sm-input-help">
|
||||
<span v-if="feedbackInvalid" class="sm-input-invalid">{{
|
||||
feedbackInvalid
|
||||
}}</span>
|
||||
<span v-if="slots.default" class="sm-input-info">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="help" class="form-group-help">
|
||||
<font-awesome-icon v-if="helpIcon" :icon="helpIcon" />
|
||||
<ion-icon v-if="helpIcon" name="information-circle-outline" />
|
||||
{{ help }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots, ref, watch } from "vue";
|
||||
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import axios from "axios";
|
||||
import { watch, computed, useSlots, ref, inject } from "vue";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
@@ -90,7 +94,7 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
error: {
|
||||
feedbackInvalid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
@@ -106,100 +110,215 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
href: {
|
||||
control: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "blur"]);
|
||||
const emits = defineEmits(["update:modelValue", "focus", "blur", "keydown"]);
|
||||
const slots = useSlots();
|
||||
const mediaUrl = ref("");
|
||||
|
||||
const objForm = inject("form", props.form);
|
||||
const objControl =
|
||||
!isEmpty(objForm) && props.control != "" ? objForm[props.control] : null;
|
||||
|
||||
const label = ref("");
|
||||
const feedbackInvalid = ref("");
|
||||
|
||||
watch(
|
||||
() => props.label,
|
||||
(newValue) => {
|
||||
label.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const value = ref(props.modelValue);
|
||||
if (objControl) {
|
||||
if (value.value.length > 0) {
|
||||
objControl.value = value.value;
|
||||
} else {
|
||||
value.value = objControl.value;
|
||||
}
|
||||
|
||||
if (label.value.length == 0) {
|
||||
label.value = toTitleCase(props.control);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => objControl.validation.result.valid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue
|
||||
? ""
|
||||
: objControl.validation.result.invalidMessages[0];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
feedbackInvalid.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const inputActive = ref(value.value.length > 0);
|
||||
|
||||
const handleChange = (event) => {
|
||||
emits("update:modelValue", event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
value.value = target.value;
|
||||
emits("update:modelValue", target.value);
|
||||
|
||||
if (objControl) {
|
||||
objControl.value = target.value;
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (event: Event) => {
|
||||
inputActive.value = true;
|
||||
|
||||
if (event instanceof KeyboardEvent) {
|
||||
if (event.key === undefined || event.key === "Tab") {
|
||||
emits("blur", event);
|
||||
}
|
||||
}
|
||||
|
||||
emits("focus", event);
|
||||
};
|
||||
|
||||
const handleBlur = (event: Event) => {
|
||||
if (objControl) {
|
||||
objForm.validate(props.control);
|
||||
feedbackInvalid.value = objForm[props.control].validation.result.valid
|
||||
? ""
|
||||
: objForm[props.control].validation.result.invalidMessages[0];
|
||||
}
|
||||
|
||||
const target = event.target as HTMLInputElement;
|
||||
|
||||
if (target.value.length == 0) {
|
||||
inputActive.value = false;
|
||||
}
|
||||
|
||||
emits("blur", event);
|
||||
};
|
||||
|
||||
const input = (event) => {
|
||||
emits("update:modelValue", event.target.value);
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
if (event.keyCode == undefined || event.keyCode == 9) {
|
||||
emits("blur", event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelect = async (event) => {
|
||||
let result = await openDialog(SMDialogMedia);
|
||||
|
||||
console.log(result);
|
||||
if (result) {
|
||||
mediaUrl.value = result.url;
|
||||
emits("update:modelValue", result.id);
|
||||
}
|
||||
const handleKeydown = (event: Event) => {
|
||||
emits("keydown", event);
|
||||
};
|
||||
|
||||
const inline = computed(() => {
|
||||
return ["static", "link"].includes(props.type);
|
||||
});
|
||||
|
||||
const handleLoad = async () => {
|
||||
if (props.type == "media" && props.modelValue.length > 0) {
|
||||
try {
|
||||
let result = await axios.get(`media/${props.modelValue}`);
|
||||
mediaUrl.value = result.data.medium.url;
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.input-media-group {
|
||||
.sm-input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
max-width: 26rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
|
||||
.input-media-display {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
&.sm-input-active {
|
||||
label {
|
||||
transform: translate(8px, -3px) scale(0.7);
|
||||
color: $secondary-color-dark;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: calc(#{map-get($spacer, 2)} * 1.5) map-get($spacer, 3)
|
||||
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: calc(#{map-get($spacer, 2)} * 2) map-get($spacer, 3)
|
||||
calc(#{map-get($spacer, 2)} / 2) map-get($spacer, 3);
|
||||
}
|
||||
}
|
||||
|
||||
&.sm-feedback-invalid {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 2px solid $danger-color;
|
||||
}
|
||||
|
||||
.sm-invalid-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
line-height: 1.5;
|
||||
transform-origin: top left;
|
||||
transform: translate(0, 1px) scale(1);
|
||||
transition: all 0.1s ease-in-out;
|
||||
color: $font-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sm-invalid-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
color: $danger-color;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid $border-color;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
color: $font-color;
|
||||
margin-bottom: map-get($spacer, 1);
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
svg {
|
||||
padding: 4rem;
|
||||
}
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
max-width: 13rem;
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-media-group + .form-group-error {
|
||||
text-align: center;
|
||||
}
|
||||
.sm-input-help {
|
||||
font-size: 75%;
|
||||
margin: 0 map-get($spacer, 1);
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.input-media-group {
|
||||
max-width: 13rem;
|
||||
.sm-input-invalid {
|
||||
color: $danger-color;
|
||||
padding-right: map-get($spacer, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<template v-if="loading">
|
||||
<transition name="fade">
|
||||
<div v-if="loading" class="loader-cover">
|
||||
<div class="loader">
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
</div>
|
||||
<div v-if="loading" class="sm-loader">
|
||||
<SMLoadingIcon />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -12,6 +10,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
@@ -19,3 +19,20 @@ defineProps({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sm-loader {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 10000;
|
||||
}
|
||||
</style>
|
||||
|
||||
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>
|
||||
<div :class="['message', type]">
|
||||
<font-awesome-icon v-if="icon" :icon="icon" />{{ message }}
|
||||
<ion-icon v-if="icon" :name="icon"></ion-icon>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,16 +24,13 @@ defineProps({
|
||||
|
||||
<style lang="scss">
|
||||
.message {
|
||||
display: flex;
|
||||
padding: map-get($spacer, 2) map-get($spacer, 3);
|
||||
margin-bottom: map-get($spacer, 4);
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
word-break: break-word;
|
||||
|
||||
svg {
|
||||
padding-right: map-get($spacer, 1);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: $primary-color-lighter;
|
||||
color: $primary-color-darker;
|
||||
@@ -53,5 +51,20 @@ defineProps({
|
||||
border: 1px solid $danger-color-lighter;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
height: 1.3em;
|
||||
width: 1.3em;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<SMContainer
|
||||
:full="true"
|
||||
:class="['navbar', { showDropdown: showToggle }]"
|
||||
:class="['sm-navbar', { showDropdown: showToggle }]"
|
||||
@click="handleHideMenu">
|
||||
<template #inner>
|
||||
<div class="navbar-container">
|
||||
@@ -24,12 +24,12 @@
|
||||
:to="{ name: 'workshop-list' }"
|
||||
class="navbar-cta"
|
||||
label="Find a workshop"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
<div class="menuButton" @click.stop="handleToggleMenu">
|
||||
<span>Menu</span
|
||||
><font-awesome-icon
|
||||
icon="fa-solid fa-bars"
|
||||
class="menuButtonIcon" />
|
||||
><ion-icon
|
||||
class="menuButtonIcon"
|
||||
name="reorder-three-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,9 +37,7 @@
|
||||
<ul class="navbar-dropdown">
|
||||
<li class="ml-auto">
|
||||
<div class="menuClose" @click.stop="handleToggleMenu">
|
||||
<font-awesome-icon
|
||||
icon="fa-solid fa-xmark"
|
||||
class="menuCloseIcon" />
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="item in menuItems">
|
||||
@@ -47,7 +45,7 @@
|
||||
v-if="item.show == undefined || item.show()"
|
||||
:key="item.name">
|
||||
<router-link :to="item.to"
|
||||
><font-awesome-icon :icon="item.icon" />{{
|
||||
><ion-icon :name="item.icon" />{{
|
||||
item.label
|
||||
}}</router-link
|
||||
>
|
||||
@@ -69,13 +67,13 @@ const menuItems = [
|
||||
name: "news",
|
||||
label: "News",
|
||||
to: "/news",
|
||||
icon: "fa-regular fa-newspaper",
|
||||
icon: "newspaper-outline",
|
||||
},
|
||||
{
|
||||
name: "workshops",
|
||||
label: "Workshops",
|
||||
to: "/workshops",
|
||||
icon: "fa-solid fa-pen-ruler",
|
||||
icon: "shapes-outline",
|
||||
},
|
||||
// {
|
||||
// name: "courses",
|
||||
@@ -87,13 +85,13 @@ const menuItems = [
|
||||
name: "contact",
|
||||
label: "Contact us",
|
||||
to: "/contact",
|
||||
icon: "fa-regular fa-envelope",
|
||||
icon: "mail-outline",
|
||||
},
|
||||
{
|
||||
name: "register",
|
||||
label: "Register",
|
||||
to: "/register",
|
||||
icon: "fa-solid fa-pen-to-square",
|
||||
icon: "person-add-outline",
|
||||
show: () => !userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
@@ -101,7 +99,7 @@ const menuItems = [
|
||||
name: "login",
|
||||
label: "Log in",
|
||||
to: "/login",
|
||||
icon: "fa-solid fa-right-to-bracket",
|
||||
icon: "log-in-outline",
|
||||
show: () => !userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
@@ -109,7 +107,7 @@ const menuItems = [
|
||||
name: "dashboard",
|
||||
label: "Dashboard",
|
||||
to: "/dashboard",
|
||||
icon: "fa-regular fa-circle-user",
|
||||
icon: "apps-outline",
|
||||
show: () => userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
@@ -117,7 +115,7 @@ const menuItems = [
|
||||
name: "logout",
|
||||
label: "Log out",
|
||||
to: "/logout",
|
||||
icon: "fa-solid fa-right-from-bracket",
|
||||
icon: "log-out-outline",
|
||||
show: () => userStore.id,
|
||||
inNav: false,
|
||||
},
|
||||
@@ -135,7 +133,7 @@ const handleHideMenu = () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar {
|
||||
.sm-navbar {
|
||||
height: 4.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -143,6 +141,7 @@ const handleHideMenu = () => {
|
||||
position: relative;
|
||||
flex: 0 0 auto !important;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
|
||||
&.showDropdown {
|
||||
.navbar-dropdown-cover {
|
||||
@@ -195,8 +194,10 @@ const handleHideMenu = () => {
|
||||
display: inline-block;
|
||||
width: map-get($spacer, 5) * 3;
|
||||
|
||||
svg {
|
||||
ion-icon {
|
||||
padding-right: map-get($spacer, 1);
|
||||
font-size: map-get($spacer, 4);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +225,7 @@ const handleHideMenu = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.menuClose svg {
|
||||
.menuClose ion-icon {
|
||||
cursor: pointer;
|
||||
font-size: map-get($spacer, 4);
|
||||
padding-left: map-get($spacer, 1);
|
||||
@@ -271,7 +272,7 @@ const handleHideMenu = () => {
|
||||
|
||||
.menuButtonIcon {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 1.4rem;
|
||||
font-size: map-get($spacer, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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-internal v-if="error >= 500"></d-error-internal>
|
||||
<d-error-not-found v-if="error == 404"></d-error-not-found>
|
||||
<template v-if="slots.default">
|
||||
<template v-if="slots.default && error < 300">
|
||||
<slot></slot>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -11,23 +11,22 @@
|
||||
{{ format(new Date(date), "MMM") }}
|
||||
</div>
|
||||
</div>
|
||||
<font-awesome-icon
|
||||
<ion-icon
|
||||
v-if="hideImageLoader == false"
|
||||
class="panel-image-loader"
|
||||
icon="fa-regular fa-image" />
|
||||
name="image-outline" />
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<h3 class="panel-title">{{ title }}</h3>
|
||||
<div v-if="showDate" class="panel-date">
|
||||
<font-awesome-icon
|
||||
<ion-icon
|
||||
v-if="showTime == false && endDate.length == 0"
|
||||
icon="fa-regular fa-calendar" /><font-awesome-icon
|
||||
v-else
|
||||
icon="fa-regular fa-clock" />
|
||||
name="calendar-outline" />
|
||||
<ion-icon v-else name="time-outline" />
|
||||
<p>{{ panelDate }}</p>
|
||||
</div>
|
||||
<div v-if="location" class="panel-location">
|
||||
<font-awesome-icon icon="fa-solid fa-location-dot" />
|
||||
<ion-icon name="location-outline" />
|
||||
<p>{{ location }}</p>
|
||||
</div>
|
||||
<div v-if="content" class="panel-content">{{ panelContent }}</div>
|
||||
@@ -39,7 +38,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { onMounted, computed, ref } from "vue";
|
||||
import {
|
||||
excerpt,
|
||||
@@ -49,6 +47,7 @@ import {
|
||||
} from "../helpers/common";
|
||||
import { format } from "date-fns";
|
||||
import SMButton from "./SMButton.vue";
|
||||
import { api } from "../helpers/api";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -151,10 +150,10 @@ const hideImageLoader = computed(() => {
|
||||
onMounted(async () => {
|
||||
if (imageUrl.value && imageUrl.value.length > 0 && isUUID(imageUrl.value)) {
|
||||
try {
|
||||
let result = await axios.get(`media/${props.image}`);
|
||||
let result = await api.get(`/media/${props.image}`);
|
||||
|
||||
if (result.data.medium) {
|
||||
imageUrl.value = result.data.medium.url;
|
||||
if (result.json.medium) {
|
||||
imageUrl.value = result.json.medium.url;
|
||||
}
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="panel-list">
|
||||
<div v-if="loading" class="panel-list-loading">
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
</div>
|
||||
<div v-else-if="notFound" class="panel-list-not-found">
|
||||
<font-awesome-icon icon="fa-regular fa-face-frown-open" />
|
||||
<ion-icon name="alert-circle-outline" />
|
||||
<p>{{ notFoundText }}</p>
|
||||
</div>
|
||||
<slot></slot>
|
||||
@@ -12,6 +12,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMLoadingIcon from "./SMLoadingIcon.vue";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="help" class="form-group-help">
|
||||
<font-awesome-icon v-if="helpIcon" :icon="helpIcon" />
|
||||
<ion-icon v-if="helpIcon" name="information-circle-outline" />
|
||||
{{ help }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
||||
import { closeDialog } from "vue3-promise-dialog";
|
||||
@@ -82,15 +81,17 @@ const handleConfirm = async () => {
|
||||
if (isValidated(formData)) {
|
||||
try {
|
||||
formLoading.value = true;
|
||||
await axios.put(`users/${userStore.id}`, {
|
||||
password: formData.password.value,
|
||||
await api.put({
|
||||
url: `/users/${userStore.id}`,
|
||||
body: {
|
||||
password: formData.password.value,
|
||||
},
|
||||
});
|
||||
|
||||
isSuccessful.value = true;
|
||||
} catch (err) {
|
||||
formData.password.error =
|
||||
err.response?.data?.message ||
|
||||
"An unexpected error occurred";
|
||||
err.json?.message || "An unexpected error occurred";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,19 +26,19 @@
|
||||
>Page {{ page }} of {{ totalPages }}</span
|
||||
>
|
||||
<span class="media-browser-page-changer">
|
||||
<font-awesome-icon
|
||||
<ion-icon
|
||||
name="chevron-back-outline"
|
||||
:class="[
|
||||
'changer-button',
|
||||
{ disabled: prevDisabled },
|
||||
]"
|
||||
icon="fa-solid fa-angle-left"
|
||||
@click="handlePrev" />
|
||||
<font-awesome-icon
|
||||
<ion-icon
|
||||
name="chevron-forward-outline"
|
||||
:class="[
|
||||
'changer-button',
|
||||
{ disabled: nextDisabled },
|
||||
]"
|
||||
icon="fa-solid fa-angle-right"
|
||||
@click="handleNext" />
|
||||
</span>
|
||||
</div>
|
||||
@@ -73,7 +73,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { computed, watch, ref, reactive, onMounted, onUnmounted } from "vue";
|
||||
import { closeDialog } from "vue3-promise-dialog";
|
||||
import SMButton from "../SMButton.vue";
|
||||
@@ -82,6 +81,7 @@ import SMDialog from "../SMDialog.vue";
|
||||
import SMMessage from "../SMMessage.vue";
|
||||
import SMModal from "../SMModal.vue";
|
||||
import { toParamString } from "../../helpers/common";
|
||||
import { api } from "../../helpers/api";
|
||||
|
||||
const uploader = ref(null);
|
||||
const formLoading = ref(false);
|
||||
@@ -134,18 +134,18 @@ const handleLoad = async () => {
|
||||
params.limit = perPage.value;
|
||||
// params.fields = "url";
|
||||
|
||||
let res = await axios.get(`media${toParamString(params)}`);
|
||||
let res = await api.get(`/media${toParamString(params)}`);
|
||||
|
||||
totalItems.value = res.data.total;
|
||||
mediaItems.value = res.data.media;
|
||||
totalItems.value = res.json.total;
|
||||
mediaItems.value = res.json.media;
|
||||
} catch (error) {
|
||||
if (error.response.status == 404) {
|
||||
if (error.status == 404) {
|
||||
formMessage.type = "primary";
|
||||
formMessage.icon = "fa-regular fa-folder-open";
|
||||
formMessage.message = "No media items found";
|
||||
} else {
|
||||
formMessage.message =
|
||||
error.response?.data?.message || "An unexpected error occurred";
|
||||
error?.json?.message || "An unexpected error occurred";
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -165,18 +165,20 @@ const handleUpload = async () => {
|
||||
if (uploader.value.files[0] instanceof File) {
|
||||
submitFormData.append("file", uploader.value.files[0]);
|
||||
|
||||
let res = await axios.post("media", submitFormData, {
|
||||
let res = await api.post({
|
||||
url: "/media",
|
||||
body: submitFormData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: (progressEvent) =>
|
||||
(formLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||
(progressEvent.loaded / progressEvent.total) * 100
|
||||
)}%`),
|
||||
// onUploadProgress: (progressEvent) =>
|
||||
// (formLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||
// (progressEvent.loaded / progressEvent.total) * 100
|
||||
// )}%`),
|
||||
});
|
||||
|
||||
if (res.data.medium) {
|
||||
closeDialog(res.data.medium);
|
||||
if (res.json.medium) {
|
||||
closeDialog(res.json.medium);
|
||||
} else {
|
||||
formMessage.message =
|
||||
"An unexpected response was received from the server";
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<template>
|
||||
<div class="page-error forbidden">
|
||||
<div class="image"></div>
|
||||
<div class="content">
|
||||
<h1>The cat says no!</h1>
|
||||
<p>You do not have the needed access to see this page</p>
|
||||
<SMPage no-breadcrumbs>
|
||||
<div class="page-error forbidden">
|
||||
<div class="image"></div>
|
||||
<div class="content">
|
||||
<h1>The cat says no!</h1>
|
||||
<p>You do not have the needed access to see this page</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../SMPage.vue";
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error.forbidden .image {
|
||||
background-image: url('/img/403.jpg');
|
||||
background-image: url("/img/403.jpg");
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
<template>
|
||||
<div class="page-error internal">
|
||||
<div class="image"></div>
|
||||
<div class="content">
|
||||
<h1>The cat has broken something</h1>
|
||||
<p>We are working to fix that what was broken. Please try again later.</p>
|
||||
<SMPage no-breadcrumbs>
|
||||
<div class="page-error internal">
|
||||
<div class="image"></div>
|
||||
<div class="content">
|
||||
<h1>The cat has broken something</h1>
|
||||
<p>
|
||||
We are working to fix that what was broken. Please try again
|
||||
later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../SMPage.vue";
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error.internal .image {
|
||||
background-image: url('/img/500.jpg');
|
||||
background-image: url("/img/500.jpg");
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<template>
|
||||
<div class="page-error not-found">
|
||||
<div class="image"></div>
|
||||
<div class="content">
|
||||
<h1>Opps</h1>
|
||||
<p>The page you asked for was not found</p>
|
||||
<SMPage no-breadcrumbs>
|
||||
<div class="page-error not-found">
|
||||
<div class="image"></div>
|
||||
<div class="content">
|
||||
<h1>Opps</h1>
|
||||
<p>The page you asked for was not found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../SMPage.vue";
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-error.not-found .image {
|
||||
background-image: url('/img/404.jpg');
|
||||
background-image: url("/img/404.jpg");
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
190
resources/js/helpers/api.ts
Normal file
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 = () => {
|
||||
var i,
|
||||
undefined,
|
||||
@@ -173,41 +171,6 @@ export function parseErrorType(
|
||||
return def;
|
||||
}
|
||||
|
||||
export const relativeDate = (d) => {
|
||||
if (isString(d)) {
|
||||
d = new Date(d);
|
||||
}
|
||||
|
||||
// const d = new Date(0);
|
||||
// // d.setUTCSeconds(parseInt(epoch));
|
||||
// d.setUTCSeconds(epoch);
|
||||
|
||||
const now = new Date();
|
||||
const dif = Math.round((now.getTime() - d.getTime()) / 1000);
|
||||
|
||||
if (dif < 60) {
|
||||
// let v = dif;
|
||||
// return v + " sec" + (v != 1 ? "s" : "") + " ago";
|
||||
return "Just now";
|
||||
} else if (dif < 3600) {
|
||||
const v = Math.round(dif / 60);
|
||||
return v + " min" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 86400) {
|
||||
const v = Math.round(dif / 3600);
|
||||
return v + " hour" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 604800) {
|
||||
const v = Math.round(dif / 86400);
|
||||
return v + " day" + (v != 1 ? "s" : "") + " ago";
|
||||
} else if (dif < 2419200) {
|
||||
const v = Math.round(dif / 604800);
|
||||
return v + " week" + (v != 1 ? "s" : "") + " ago";
|
||||
}
|
||||
|
||||
return (
|
||||
monthString[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
export const buildUrlQuery = (url, query) => {
|
||||
let s = "";
|
||||
|
||||
@@ -347,90 +310,6 @@ export const isUUID = (uuid) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const timestampUtcToLocal = (utc) => {
|
||||
try {
|
||||
let iso = new Date(
|
||||
utc.replace(
|
||||
/([0-9]{4}-[0-9]{2}-[0-9]{2}),? ([0-9]{2}:[0-9]{2}:[0-9]{2})/,
|
||||
"$1T$2.000Z"
|
||||
)
|
||||
);
|
||||
return format(iso, "yyyy/MM/dd HH:mm:ss");
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const timestampLocalToUtc = (local) => {
|
||||
try {
|
||||
let d = new Date(local);
|
||||
return d
|
||||
.toISOString()
|
||||
.replace(
|
||||
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
|
||||
"$1 $2"
|
||||
);
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const timestampNowLocal = () => {
|
||||
let d = new Date();
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
("0" + (d.getMonth() + 1)).slice(-2) +
|
||||
"-"("0" + d.getDate()).slice(-2) +
|
||||
" " +
|
||||
("0" + d.getHours()).slice(-2) +
|
||||
":" +
|
||||
("0" + d.getMinutes()).slice(-2) +
|
||||
":" +
|
||||
("0" + d.getSeconds()).slice(-2)
|
||||
);
|
||||
};
|
||||
|
||||
export const timestampNowUtc = () => {
|
||||
try {
|
||||
let d = new Date();
|
||||
return d
|
||||
.toISOString()
|
||||
.replace(
|
||||
/([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/,
|
||||
"$1 $2"
|
||||
);
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const timestampBeforeNow = (timestamp) => {
|
||||
try {
|
||||
return new Date(timestamp) < new Date();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const timestampAfterNow = (timestamp) => {
|
||||
try {
|
||||
return new Date(timestamp) > new Date();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export {
|
||||
transitionEndEventName,
|
||||
waitForElementRender,
|
||||
|
||||
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 piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import Router from "@/router";
|
||||
import "./axios.js";
|
||||
// import "./axios.js";
|
||||
import "normalize.css";
|
||||
import "../css/app.scss";
|
||||
import App from "./views/App.vue";
|
||||
import FontAwesomeIcon from "@/helpers/fontawesome";
|
||||
// import FontAwesomeIcon from "@/helpers/fontawesome";
|
||||
import SMContainer from "./components/SMContainer.vue";
|
||||
import SMRow from "./components/SMRow.vue";
|
||||
import SMColumn from "./components/SMColumn.vue";
|
||||
@@ -19,7 +19,7 @@ const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createApp(App)
|
||||
.component("FontAwesomeIcon", FontAwesomeIcon)
|
||||
// .component("FontAwesomeIcon", FontAwesomeIcon)
|
||||
.use(pinia)
|
||||
.use(Router)
|
||||
.use(PromiseDialog)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
import { createWebHistory, createRouter } from "vue-router";
|
||||
import { useUserStore } from "@/store/UserStore";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { api } from "../helpers/api";
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
@@ -386,10 +386,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
let redirect = false;
|
||||
|
||||
try {
|
||||
let res = await axios.get("me");
|
||||
userStore.setUserDetails(res.data.user);
|
||||
let res = await api.get("/me");
|
||||
userStore.setUserDetails(res.json.user);
|
||||
} catch (err) {
|
||||
if (err.response.status == 401) {
|
||||
if (err.status == 401) {
|
||||
userStore.clearUser();
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from "axios";
|
||||
import { api } from "../helpers/api";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export interface UserDetails {
|
||||
@@ -51,16 +51,16 @@ export const useUserStore = defineStore({
|
||||
},
|
||||
|
||||
async fetchUser() {
|
||||
const res = await axios.get("users/" + this.$state.id);
|
||||
const res = await api.get("/users/" + this.$state.id);
|
||||
|
||||
this.$state.id = res.data.user.id;
|
||||
this.$state.token = res.data.token;
|
||||
this.$state.username = res.data.user.username;
|
||||
this.$state.firstName = res.data.user.first_name;
|
||||
this.$state.lastName = res.data.user.last_name;
|
||||
this.$state.email = res.data.user.email;
|
||||
this.$state.phone = res.data.user.phone;
|
||||
this.$state.permissions = res.data.user.permissions || [];
|
||||
this.$state.id = res.json.user.id;
|
||||
this.$state.token = res.json.token;
|
||||
this.$state.username = res.json.user.username;
|
||||
this.$state.firstName = res.json.user.first_name;
|
||||
this.$state.lastName = res.json.user.last_name;
|
||||
this.$state.email = res.json.user.email;
|
||||
this.$state.phone = res.json.user.phone;
|
||||
this.$state.permissions = res.json.user.permissions || [];
|
||||
},
|
||||
|
||||
clearUser() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<SMNavbar />
|
||||
<SMBreadcrumbs />
|
||||
<main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
@@ -14,7 +13,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMNavbar from "../components/SMNavbar.vue";
|
||||
import SMBreadcrumbs from "../components/SMBreadcrumbs.vue";
|
||||
import SMFooter from "../components/SMFooter.vue";
|
||||
import { DialogWrapper } from "vue3-promise-dialog";
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer class="page-contact">
|
||||
<SMPage class="page-contact">
|
||||
<SMRow break-large>
|
||||
<SMColumn>
|
||||
<h1 class="text-left">Contact Us</h1>
|
||||
@@ -42,43 +42,20 @@
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<div>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<template v-if="!formDone">
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMDialog narrow>
|
||||
<template v-if="!formSubmitted">
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="name" />
|
||||
<SMInput control="email" type="email" />
|
||||
<SMInput
|
||||
v-model="formData.name.value"
|
||||
name="name"
|
||||
label="Name"
|
||||
required
|
||||
:error="formData.name.error"
|
||||
@blur="fieldValidate(formData.name)" />
|
||||
<SMInput
|
||||
v-model="formData.email.value"
|
||||
name="email"
|
||||
label="Email"
|
||||
required
|
||||
:error="formData.email.error"
|
||||
@blur="fieldValidate(formData.email)" />
|
||||
<SMInput
|
||||
v-model="formData.content.value"
|
||||
name="content"
|
||||
type="textarea"
|
||||
control="content"
|
||||
label="Message"
|
||||
required
|
||||
:error="formData.content.error"
|
||||
@blur="fieldValidate(formData.content)" />
|
||||
<SMCaptchaNotice />
|
||||
type="textarea" />
|
||||
<SMButton
|
||||
type="submit"
|
||||
block
|
||||
label="Send Message"
|
||||
icon="fa-regular fa-paper-plane" />
|
||||
</form>
|
||||
label="Send Message" />
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Message Sent!</h1>
|
||||
@@ -86,98 +63,65 @@
|
||||
Your message as been sent to us. We will respond
|
||||
as soon as we can.
|
||||
</p>
|
||||
<SMButton block to="/" label="Home" />
|
||||
<SMButton
|
||||
block
|
||||
:to="{ name: 'home' }"
|
||||
label="Home" />
|
||||
</template>
|
||||
</SMDialog>
|
||||
</div>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Email, Min, Required } from "../helpers/validate";
|
||||
|
||||
import { ref, reactive } from "vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
name: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A name is needed",
|
||||
min: 4,
|
||||
min_message: "A name needs to be is at least 4 characters",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A email address is needed",
|
||||
email: true,
|
||||
email_message: "That email address does not look right",
|
||||
},
|
||||
},
|
||||
content: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A message is required",
|
||||
min: 8,
|
||||
min_message: "The message needs to be at least %d characters",
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
name: FormControl("", And([Required(), Min(4)])),
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
content: FormControl("", And([Required(), Min(8)])),
|
||||
})
|
||||
);
|
||||
const formSubmitted = ref(false);
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
await axios.post("contact", {
|
||||
name: formData.name.value,
|
||||
email: formData.email.value,
|
||||
await api.post({
|
||||
url: "/contact",
|
||||
body: {
|
||||
name: form.name.value,
|
||||
email: form.email.value,
|
||||
captcha_token: captcha,
|
||||
content: formData.content.value,
|
||||
});
|
||||
content: form.content.value,
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
formSubmitted.value = true;
|
||||
} catch (err) {
|
||||
formLoading.value = false;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
console.log(err);
|
||||
form.apiErrors(err);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<SMDialog class="mt-5" narrow>
|
||||
<template v-if="!formDone">
|
||||
<h1>Email Verify</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model="formData.code.value"
|
||||
name="code"
|
||||
label="Code"
|
||||
required
|
||||
:error="formData.code.error"
|
||||
@blur="fieldValidate(formData.code)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="code" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<div class="small">
|
||||
<router-link to="/resend-verify-email"
|
||||
>Resend Code</router-link
|
||||
>
|
||||
@@ -30,10 +18,10 @@
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Verify Code"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email Verified!</h1>
|
||||
@@ -48,7 +36,7 @@
|
||||
</template>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -57,70 +45,47 @@ import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import { And, Max, Min, Required } from "../helpers/validate";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import { FormControl, FormObject } from "../helpers/form";
|
||||
import { api } from "../helpers/api";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
code: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "The code is needed",
|
||||
min: 6,
|
||||
min_message: "The code should be 6 characters",
|
||||
max: 6,
|
||||
max_message: "The code should be 6 characters",
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||
})
|
||||
);
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
await axios.post("users/verifyEmail", {
|
||||
code: formData.code.value,
|
||||
await api.post({
|
||||
url: "/users/verifyEmail",
|
||||
body: {
|
||||
code: form.code.value,
|
||||
captcha_token: captcha,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
formDone.value = true;
|
||||
} catch (err) {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
form.apiErrors(err);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
|
||||
if (useRoute().query.code !== undefined) {
|
||||
formData.code.value = useRoute().query.code;
|
||||
submit();
|
||||
form.code.value = useRoute().query.code;
|
||||
handleSubmit();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@ import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.vue";
|
||||
import SMPanel from "../components/SMPanel.vue";
|
||||
import { reactive } from "vue";
|
||||
import axios from "axios";
|
||||
import { api } from "../helpers/api";
|
||||
|
||||
const events = reactive([]);
|
||||
|
||||
@@ -39,12 +39,16 @@ const handleLoad = async () => {
|
||||
formMessage.message = "";
|
||||
|
||||
try {
|
||||
let result = await axios.get("events?limit=10");
|
||||
events.value = result.data.events;
|
||||
let result = await api.get({
|
||||
url: "/events",
|
||||
params: {
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
events.value = result.json.events;
|
||||
} catch (error) {
|
||||
formMessage.message =
|
||||
error.response?.data?.message ||
|
||||
"Could not load any events from the server.";
|
||||
error.json?.message || "Could not load any events from the server.";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<SMDialog narrow class="mt-5">
|
||||
<template v-if="!formDone">
|
||||
<h1>Forgot Password</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model="formData.username.value"
|
||||
name="username"
|
||||
label="Username"
|
||||
required
|
||||
:error="formData.username.error"
|
||||
@blur="fieldValidate(formData.username)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="username" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<div class="small">
|
||||
<span class="pr-1">Remember?</span
|
||||
><router-link :to="{ name: 'login' }"
|
||||
>Log in</router-link
|
||||
@@ -31,10 +19,10 @@
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Send"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email Sent!</h1>
|
||||
@@ -51,78 +39,54 @@
|
||||
</template>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Required, Min } from "../helpers/validate";
|
||||
import { ref, reactive } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
username: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "Your username is needed",
|
||||
min: 4,
|
||||
min_message: "Your username is at least %d characters",
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
username: FormControl("", And([Required(), Min(4)])),
|
||||
})
|
||||
);
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
let res = await axios.post("users/forgotPassword", {
|
||||
username: formData.username.value,
|
||||
await api.post({
|
||||
url: "/users/forgotPassword",
|
||||
body: {
|
||||
username: form.username.value,
|
||||
captcha_token: captcha,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.response.status == 422) {
|
||||
formDone.value = true;
|
||||
} catch (error) {
|
||||
if (error.status == 422) {
|
||||
formDone.value = true;
|
||||
} else {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
form.apiErrors(error);
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<SMDialog narrow class="mt-5">
|
||||
<template v-if="!formDone">
|
||||
<h1>Forgot Username</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model:error="formData.email.error"
|
||||
v-model="formData.email.value"
|
||||
name="email"
|
||||
label="Email"
|
||||
required
|
||||
@blur="fieldValidate(formData.email)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="email" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<div class="small">
|
||||
<span class="pr-1">Remember?</span
|
||||
><router-link :to="{ name: 'login' }"
|
||||
>Log in</router-link
|
||||
@@ -31,10 +19,10 @@
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Send"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email Sent!</h1>
|
||||
@@ -50,72 +38,52 @@
|
||||
</template>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Required, Email } from "../helpers/validate";
|
||||
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
email: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "An email address is required",
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
})
|
||||
);
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
let res = await axios.post("users/forgotUsername", {
|
||||
email: formData.email.value,
|
||||
await api.post({
|
||||
url: "/users/forgotUsername",
|
||||
body: {
|
||||
email: form.email.value,
|
||||
captcha_token: captcha,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
formDone.value = true;
|
||||
} catch (error) {
|
||||
form.apiErrors(error);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer full class="home">
|
||||
<SMPage full class="home">
|
||||
<SMCarousel>
|
||||
<SMCarouselSlide
|
||||
v-for="(slide, index) in slides"
|
||||
@@ -110,100 +110,85 @@
|
||||
Sign up for our mailing list to receive expert tips and tricks,
|
||||
as well as updates on upcoming workshops.
|
||||
</p>
|
||||
<SMDialog :loading="formLoading" class="p-0">
|
||||
<form @submit.prevent="handleSubscribe">
|
||||
<SMDialog class="p-0">
|
||||
<SMForm v-model="form" @submit.prevent="handleSubscribe">
|
||||
<div class="form-row">
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<SMInput
|
||||
v-model="subscribeFormData.email.value"
|
||||
placeholder="Email address"
|
||||
:error="subscribeFormData.email.error"
|
||||
@blur="fieldValidate(subscribeFormData.email)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMInput control="email" />
|
||||
<SMButton type="submit" label="Subscribe" />
|
||||
</div>
|
||||
</form>
|
||||
</SMForm>
|
||||
</SMDialog>
|
||||
</SMContainer>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { reactive, ref } from "vue";
|
||||
import { buildUrlQuery, excerpt, timestampNowUtc } from "../helpers/common";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
clearFormData,
|
||||
} from "../helpers/validation";
|
||||
import { excerpt } from "../helpers/common";
|
||||
import { timestampNowUtc } from "../helpers/datetime";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMCarousel from "../components/SMCarousel.vue";
|
||||
import SMCarouselSlide from "../components/SMCarouselSlide.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
import { api } from "../helpers/api";
|
||||
|
||||
const slides = ref([]);
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const subscribeFormData = reactive({
|
||||
email: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "An email address is needed.",
|
||||
email: true,
|
||||
email_message: "That does not appear to be an email address.",
|
||||
},
|
||||
},
|
||||
});
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formLoading = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
})
|
||||
);
|
||||
|
||||
const handleLoad = async () => {
|
||||
slides.value = [];
|
||||
let posts = [];
|
||||
let events = [];
|
||||
|
||||
try {
|
||||
let result = await axios.get(buildUrlQuery("posts", { limit: 3 }));
|
||||
if (result.data.posts) {
|
||||
result.data.posts.forEach((post) => {
|
||||
posts.push({
|
||||
title: post.title,
|
||||
content: excerpt(post.content, 200),
|
||||
image: post.hero,
|
||||
url: { name: "post-view", params: { slug: post.slug } },
|
||||
cta: "Read More...",
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
try {
|
||||
let query = {
|
||||
api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: 3,
|
||||
end_at: ">" + timestampNowUtc(),
|
||||
};
|
||||
},
|
||||
progress: ({ loaded, total }) => {
|
||||
console.log("progress", `${loaded} - ${total}`);
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.posts) {
|
||||
response.data.posts.forEach((post) => {
|
||||
posts.push({
|
||||
title: post.title,
|
||||
content: excerpt(post.content, 200),
|
||||
image: post.hero,
|
||||
url: { name: "post-view", params: { slug: post.slug } },
|
||||
cta: "Read More...",
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error", error);
|
||||
/* empty */
|
||||
});
|
||||
|
||||
let result = await axios.get(buildUrlQuery("events", query));
|
||||
if (result.data.events) {
|
||||
result.data.events.forEach((event) => {
|
||||
try {
|
||||
let result = await api.get({
|
||||
url: "/events",
|
||||
params: {
|
||||
limit: 3,
|
||||
end_at: ">" + timestampNowUtc(),
|
||||
},
|
||||
});
|
||||
|
||||
if (result.json.events) {
|
||||
result.json.events.forEach((event) => {
|
||||
events.push({
|
||||
title: event.title,
|
||||
content: excerpt(event.content, 200),
|
||||
@@ -228,34 +213,30 @@ const handleLoad = async () => {
|
||||
};
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.icon = "";
|
||||
formMessage.type = "error";
|
||||
formMessage.message = "";
|
||||
form.loading(true);
|
||||
form.message();
|
||||
|
||||
try {
|
||||
if (isValidated(subscribeFormData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
await axios.post("subscriptions", {
|
||||
email: subscribeFormData.email.value,
|
||||
await api.post({
|
||||
url: "/subscriptions",
|
||||
body: {
|
||||
email: form.email.value,
|
||||
captcha_token: captcha,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
clearFormData(subscribeFormData);
|
||||
|
||||
formMessage.type = "success";
|
||||
formMessage.message = "Your email address has been subscribed.";
|
||||
}
|
||||
form.email.value = "";
|
||||
form.message("Your email address has been subscribed.", "success");
|
||||
} catch (err) {
|
||||
restParseErrors(subscribeFormData, [formMessage, "message"], err);
|
||||
form.apiErrors(err);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
|
||||
useValidation(subscribeFormData);
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,157 +1,96 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMDialog narrow>
|
||||
<h1>Log in</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model:error="formData.username.error"
|
||||
v-model="formData.username.value"
|
||||
name="username"
|
||||
label="Username"
|
||||
required
|
||||
@blur="fieldValidate(formData.username)">
|
||||
<router-link to="/forgot-username"
|
||||
>Forgot username?</router-link
|
||||
>
|
||||
</SMInput>
|
||||
<SMInput
|
||||
v-model="formData.password.value"
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
:error="formData.password.error"
|
||||
@blur="fieldValidate(formData.password)">
|
||||
<router-link to="/forgot-password"
|
||||
>Forgot password?</router-link
|
||||
>
|
||||
</SMInput>
|
||||
<SMCaptchaNotice />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<span class="pr-1">Need an account?</span
|
||||
><router-link to="/register"
|
||||
>Register</router-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Log in"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMDialog>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMDialog narrow class="mt-5">
|
||||
<h1>Log in</h1>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="username">
|
||||
<router-link to="/forgot-username"
|
||||
>Forgot username?</router-link
|
||||
>
|
||||
</SMInput>
|
||||
<SMInput control="password" type="password">
|
||||
<router-link to="/forgot-password"
|
||||
>Forgot password?</router-link
|
||||
>
|
||||
</SMInput>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div class="small">
|
||||
<span class="pr-1">Need an account?</span
|
||||
><router-link to="/register">Register</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Log in"
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</SMForm>
|
||||
</SMDialog>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { reactive } from "vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Min, Required, Password } from "../helpers/validate";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const formLoading = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
username: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "Your username is needed",
|
||||
min: 4,
|
||||
min_message: "Your username is at least 6 characters",
|
||||
},
|
||||
},
|
||||
password: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A password is required",
|
||||
min: 8,
|
||||
min_message: "Your password needs to be at least %d characters",
|
||||
password: "special",
|
||||
password_message:
|
||||
"Your password needs to have at least a letter, a number and a special character",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useValidation(formData);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
username: FormControl("", And([Required(), Min(4)])),
|
||||
password: FormControl("", Password()),
|
||||
})
|
||||
);
|
||||
|
||||
const redirect = useRoute().query.redirect;
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.message();
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
let res = await axios.post("login", {
|
||||
username: formData.username.value,
|
||||
password: formData.password.value,
|
||||
});
|
||||
let res = await api.post({
|
||||
url: "/login",
|
||||
body: {
|
||||
username: form.username.value,
|
||||
password: form.password.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.data.token !== undefined) {
|
||||
userStore.setUserDetails(res.data.user);
|
||||
userStore.setUserToken(res.data.token);
|
||||
if (redirect !== undefined) {
|
||||
if (redirect.startsWith("api/")) {
|
||||
window.location.href =
|
||||
redirect +
|
||||
"?token=" +
|
||||
encodeURIComponent(res.data.token);
|
||||
} else {
|
||||
router.push({ path: redirect });
|
||||
}
|
||||
} else {
|
||||
router.push({ name: "dashboard" });
|
||||
}
|
||||
userStore.setUserDetails(res.json.user);
|
||||
userStore.setUserToken(res.json.token);
|
||||
if (redirect !== undefined) {
|
||||
if (redirect.startsWith("api/")) {
|
||||
window.location.href =
|
||||
redirect + "?token=" + encodeURIComponent(res.json.token);
|
||||
} else {
|
||||
formMessage.message =
|
||||
"An unexpected error occurred on the server. Please try again later";
|
||||
router.push({ path: redirect });
|
||||
}
|
||||
} else {
|
||||
router.push({ name: "dashboard" });
|
||||
}
|
||||
} catch (err) {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
console.log(err);
|
||||
form.apiErrors(err);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
if (userStore.token) {
|
||||
userStore.clearUser();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow>
|
||||
<SMDialog narrow class="mt-5" :loading="formLoading">
|
||||
<h1>Logged out</h1>
|
||||
<SMRow>
|
||||
<SMColumn class="justify-content-center">
|
||||
@@ -17,33 +17,28 @@
|
||||
</SMRow>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import SMButton from "@/components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import { api } from "../helpers/api";
|
||||
import { ref } from "vue";
|
||||
import { useUserStore } from "../store/UserStore";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const formLoading = ref(false);
|
||||
const formMessage = reactive({
|
||||
type: "info",
|
||||
message: "Logging you out...",
|
||||
icon: "",
|
||||
});
|
||||
|
||||
formLoading.value = true;
|
||||
|
||||
const logout = async () => {
|
||||
formLoading.value = true;
|
||||
|
||||
try {
|
||||
await axios.post("logout");
|
||||
await api.post({
|
||||
url: "/logout",
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer class="news-list">
|
||||
<SMPage class="news-list">
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:icon="formMessage.icon"
|
||||
@@ -22,15 +22,16 @@
|
||||
button="Read More"
|
||||
button-type="outline" />
|
||||
</SMPanelList>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { api } from "../helpers/api";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.vue";
|
||||
import SMPanel from "../components/SMPanel.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { timestampUtcToLocal } from "../helpers/common";
|
||||
|
||||
const formMessage = reactive({
|
||||
@@ -48,8 +49,13 @@ const handleLoad = async () => {
|
||||
formMessage.message = "";
|
||||
|
||||
try {
|
||||
let result = await axios.get("posts?limit=5");
|
||||
posts.value = result.data.posts;
|
||||
let result = await api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
limit: 5,
|
||||
},
|
||||
});
|
||||
posts.value = result.json.posts;
|
||||
|
||||
posts.value.forEach((post) => {
|
||||
post.publish_at = timestampUtcToLocal(post.publish_at);
|
||||
@@ -65,5 +71,3 @@ const handleLoad = async () => {
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer :loading="pageLoading" full class="page-post-view">
|
||||
<SMPage :loading="pageLoading" full class="page-post-view">
|
||||
<SMPageError :error="error">
|
||||
<div
|
||||
class="heading-image"
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="heading-info">
|
||||
<h1>{{ post.title }}</h1>
|
||||
<div class="date-author">
|
||||
<font-awesome-icon icon="fa-solid fa-calendar" />
|
||||
<ion-icon name="calendar-outline" />
|
||||
{{ formattedPublishAt(post.publish_at) }}, by
|
||||
{{ post.user_username }}
|
||||
</div>
|
||||
@@ -18,16 +18,17 @@
|
||||
<component :is="formattedContent" ref="content"></component>
|
||||
</SMContainer>
|
||||
</SMPageError>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import axios from "axios";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMPageError from "../components/SMPageError.vue";
|
||||
import { fullMonthString, timestampUtcToLocal } from "../helpers/common";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { api } from "../helpers/api";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
const route = useRoute();
|
||||
@@ -39,16 +40,20 @@ let pageLoading = ref(true);
|
||||
const loadData = async () => {
|
||||
if (route.params.slug) {
|
||||
try {
|
||||
let res = await axios.get(
|
||||
`posts?slug==${route.params.slug}&limit=1`
|
||||
);
|
||||
if (!res.data.posts) {
|
||||
let res = await api.get({
|
||||
url: "/posts",
|
||||
params: {
|
||||
slug: `=${route.params.slug}`,
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
if (!res.json.posts) {
|
||||
error.value = 500;
|
||||
} else {
|
||||
if (res.data.total == 0) {
|
||||
if (res.json.total == 0) {
|
||||
error.value = 404;
|
||||
} else {
|
||||
post.value = res.data.posts[0];
|
||||
post.value = res.json.posts[0];
|
||||
|
||||
post.value.publish_at = timestampUtcToLocal(
|
||||
post.value.publish_at
|
||||
@@ -57,19 +62,19 @@ const loadData = async () => {
|
||||
applicationStore.setDynamicTitle(post.value.title);
|
||||
|
||||
try {
|
||||
let result = await axios.get(
|
||||
`media/${post.value.hero}`
|
||||
);
|
||||
post.value.hero_url = result.data.medium.url;
|
||||
let result = await api.get({
|
||||
url: `/media/${post.value.hero}`,
|
||||
});
|
||||
post.value.hero_url = result.json.medium.url;
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
try {
|
||||
let result = await axios.get(
|
||||
`users/${post.value.user_id}`
|
||||
);
|
||||
post.value.user_username = result.data.user.username;
|
||||
let result = await api.get({
|
||||
url: `/users/${post.value.user_id}`,
|
||||
});
|
||||
post.value.user_username = result.json.user.username;
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
@@ -144,12 +149,15 @@ loadData();
|
||||
|
||||
.content {
|
||||
margin-top: map-get($spacer, 4);
|
||||
line-height: 1.5rem;
|
||||
padding: 0 map-get($spacer, 3);
|
||||
|
||||
a span {
|
||||
color: $primary-color !important;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,76 +1,38 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMRow>
|
||||
<SMDialog :narrow="formDone" :loading="formLoading">
|
||||
<SMDialog :narrow="formDone">
|
||||
<template v-if="!formDone">
|
||||
<h1>Register</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.username.value"
|
||||
label="Username"
|
||||
required
|
||||
:error="formData.username.error"
|
||||
@blur="
|
||||
fieldValidate(formData.username)
|
||||
"></SMInput>
|
||||
<SMInput control="username" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.password.value"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
:error="formData.password.error"
|
||||
@blur="
|
||||
fieldValidate(formData.password)
|
||||
"></SMInput>
|
||||
control="password"
|
||||
type="password"></SMInput>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.first_name.value"
|
||||
label="First Name"
|
||||
required
|
||||
:error="formData.first_name.error"
|
||||
@blur="
|
||||
fieldValidate(formData.first_name)
|
||||
" />
|
||||
<SMInput control="first_name" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.last_name.value"
|
||||
label="Last Name"
|
||||
required
|
||||
:error="formData.last_name.error"
|
||||
@blur="fieldValidate(formData.last_name)" />
|
||||
<SMInput control="last_name" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.email.value"
|
||||
label="Email"
|
||||
required
|
||||
:error="formData.email.error"
|
||||
@blur="fieldValidate(formData.email)" />
|
||||
<SMInput control="email" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.phone.value"
|
||||
label="Phone Number"
|
||||
:error="formData.phone.error"
|
||||
@blur="fieldValidate(formData.phone)" />
|
||||
<SMInput control="phone">
|
||||
This field is optional.
|
||||
</SMInput>
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMCaptchaNotice />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
@@ -85,10 +47,10 @@
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Register"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email Sent!</h1>
|
||||
@@ -113,154 +75,95 @@ import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { FormControl, FormObject } from "../helpers/form";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
And,
|
||||
Custom,
|
||||
Email,
|
||||
Min,
|
||||
Password,
|
||||
Phone,
|
||||
Required,
|
||||
} from "../helpers/validate";
|
||||
|
||||
import { debounce } from "../helpers/common";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const lastUsernameCheck = ref("");
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
first_name: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A first name is needed",
|
||||
min: 2,
|
||||
min_message: "Your first name should be at least 2 letters long",
|
||||
},
|
||||
},
|
||||
last_name: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A last name is needed",
|
||||
min: 2,
|
||||
min_message: "Your last name should be at least 2 letters long",
|
||||
},
|
||||
},
|
||||
email: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A email address is needed",
|
||||
email: true,
|
||||
email_message: "Your email address is not correct",
|
||||
},
|
||||
},
|
||||
phone: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
phone: true,
|
||||
phone_message: "Your phone number does not look correct",
|
||||
},
|
||||
},
|
||||
username: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A username is needed",
|
||||
min: 4,
|
||||
min_message: "Your username needs to be at least %d characters",
|
||||
custom: () => {
|
||||
checkUsername();
|
||||
|
||||
const checkUsername = (value: string): boolean | string => {
|
||||
if (lastUsernameCheck.value != form.username.value) {
|
||||
lastUsernameCheck.value = form.username.value;
|
||||
api.get({
|
||||
url: "/users",
|
||||
params: {
|
||||
username: form.username.value,
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A password is needed",
|
||||
min: 8,
|
||||
min_message: "Your password needs to be at least %d characters",
|
||||
password: "special",
|
||||
password_message:
|
||||
"Your password needs to have at least a letter, a number and a special character",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
let res = await axios.post("register", {
|
||||
first_name: formData.first_name.value,
|
||||
last_name: formData.last_name.value,
|
||||
email: formData.email.value,
|
||||
phone: formData.phone.value,
|
||||
username: formData.username.value,
|
||||
password: formData.password.value,
|
||||
captcha_token: captcha,
|
||||
})
|
||||
.then((response) => {
|
||||
return "The username has already been taken.";
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status != 404) {
|
||||
return (
|
||||
error.json?.message ||
|
||||
"An unexpected server error occurred."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const checkUsername = async () => {
|
||||
const formDone = ref(false);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
first_name: FormControl("", Required()),
|
||||
last_name: FormControl("", Required()),
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
phone: FormControl("", Phone()),
|
||||
username: FormControl("", And([Min(4), Custom(checkUsername)])),
|
||||
password: FormControl("", And([Required(), Password()])),
|
||||
})
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (
|
||||
formData.username.value.length >= 4 &&
|
||||
lastUsernameCheck.value != formData.username.value
|
||||
) {
|
||||
lastUsernameCheck.value = formData.username.value;
|
||||
await axios.get(`users?username=${formData.username.value}`);
|
||||
formData.username.error = "The username has already been taken.";
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response.status == 404) {
|
||||
formData.username.error = "";
|
||||
} else {
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message =
|
||||
error.response.message ||
|
||||
"An unexpected server error occurred.";
|
||||
}
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
await api.post({
|
||||
url: "/register",
|
||||
body: {
|
||||
first_name: form.first_name.value,
|
||||
last_name: form.last_name.value,
|
||||
email: form.email.value,
|
||||
phone: form.phone.value,
|
||||
username: form.username.value,
|
||||
password: form.password.value,
|
||||
captcha_token: captcha,
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
} catch (err) {
|
||||
form.apiErrors(err);
|
||||
}
|
||||
|
||||
form.loading(false);
|
||||
};
|
||||
|
||||
const lastUsernameCheck = ref("");
|
||||
|
||||
const debouncedFilter = debounce(checkUsername, 1000);
|
||||
let oldUsernameValue = "";
|
||||
watch(
|
||||
formData,
|
||||
form,
|
||||
(value) => {
|
||||
if (value.username.value !== oldUsernameValue) {
|
||||
oldUsernameValue = value.username.value;
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<SMDialog narrow>
|
||||
<template v-if="!formDone">
|
||||
<h1>Resend Verify Email</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model="formData.username.value"
|
||||
name="username"
|
||||
label="Username"
|
||||
required
|
||||
:error="formData.username.error"
|
||||
@blur="fieldValidate(formData.username)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="username" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<div class="small">
|
||||
<span class="pr-1">Stuck?</span
|
||||
><router-link to="/contact"
|
||||
>Contact Us</router-link
|
||||
@@ -31,10 +19,10 @@
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Send"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Email Sent!</h1>
|
||||
@@ -51,7 +39,7 @@
|
||||
</template>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -60,69 +48,45 @@ import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Required } from "../helpers/validate";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
username: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "Your username is needed",
|
||||
min: 4,
|
||||
min_message: "Your username is at least %d characters",
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
username: FormControl("", Required()),
|
||||
})
|
||||
);
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
let res = await axios.post("users/resendVerifyEmailCode", {
|
||||
username: formData.username.value,
|
||||
await api.post({
|
||||
url: "/users/resendVerifyEmailCode",
|
||||
body: {
|
||||
username: form.username.value,
|
||||
captcha_token: captcha,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.response.status == 422) {
|
||||
formDone.value = true;
|
||||
} catch (error) {
|
||||
if (error.status == 422) {
|
||||
formDone.value = true;
|
||||
} else {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
form.apiErrors(error);
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,34 +1,15 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<SMDialog narrow>
|
||||
<template v-if="!formDone">
|
||||
<h1>Reset Password</h1>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model="formData.code.value"
|
||||
name="code"
|
||||
label="Reset Code"
|
||||
required
|
||||
:error="formData.code.error"
|
||||
@blur="fieldValidate(formData.code)" />
|
||||
<SMInput
|
||||
v-model="formData.password.value"
|
||||
type="password"
|
||||
name="password"
|
||||
label="New Password"
|
||||
required
|
||||
:error="formData.password.error"
|
||||
@blur="fieldValidate(formData.password)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="code" />
|
||||
<SMInput control="password" type="password" />
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
<div>
|
||||
<div class="small">
|
||||
<router-link
|
||||
:to="{ name: 'forgot-password' }"
|
||||
>Resend Code</router-link
|
||||
@@ -39,10 +20,10 @@
|
||||
<SMButton
|
||||
type="submit"
|
||||
label="Reset Password"
|
||||
icon="fa-solid fa-arrow-right" />
|
||||
icon="arrow-forward-outline" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Password Reset!</h1>
|
||||
@@ -57,94 +38,57 @@
|
||||
</template>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Required, Min, Max, Password } from "../helpers/validate";
|
||||
import { ref, reactive } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
code: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "The code is needed",
|
||||
min: 6,
|
||||
min_message: "The code should be 6 characters",
|
||||
max: 6,
|
||||
max_message: "The code should be 6 characters",
|
||||
},
|
||||
},
|
||||
password: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A new password is required",
|
||||
min: 8,
|
||||
min_message: "Your password needs to be at least %d characters",
|
||||
password: "special",
|
||||
password_message:
|
||||
"Your password needs to have at least a letter, a number and a special character",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useValidation(formData);
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
code: FormControl("", And([Required(), Min(6), Max(6)])),
|
||||
password: FormControl("", And([Required(), Password()])),
|
||||
})
|
||||
);
|
||||
|
||||
if (useRoute().query.code !== undefined) {
|
||||
formData.code.value = useRoute().query.code;
|
||||
form.code.value = useRoute().query.code;
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
let res = await axios.post("users/resetPassword", {
|
||||
code: formData.code.value,
|
||||
password: formData.password.value,
|
||||
await api.post({
|
||||
url: "/users/resetPassword",
|
||||
body: {
|
||||
code: form.code.value,
|
||||
password: form.password.value,
|
||||
captcha_token: captcha,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
formDone.value = true;
|
||||
} catch (error) {
|
||||
form.apiError(error);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer class="rules">
|
||||
<SMPage class="rules">
|
||||
<h1>Rules</h1>
|
||||
<p>
|
||||
Oh gosh, no body likes rules but to ensure that we have a fun,
|
||||
@@ -72,9 +72,13 @@
|
||||
grief other players builds outside of the Survival game-mode.
|
||||
</li>
|
||||
</ul>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.rules {
|
||||
h2 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer class="terms">
|
||||
<SMPage class="terms">
|
||||
<h1>Terms and Conditions</h1>
|
||||
<p>
|
||||
Please read these terms carefully. By accessing or using our website
|
||||
@@ -560,5 +560,9 @@
|
||||
be responsible for warranty and after sales service but this is
|
||||
allowed under the Law.
|
||||
</p>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
</script>
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMPage no-breadcrumbs background="/img/background.jpg">
|
||||
<SMRow>
|
||||
<SMDialog narrow :loading="formLoading">
|
||||
<SMDialog narrow>
|
||||
<template v-if="!formDone">
|
||||
<h1>Unsubscribe</h1>
|
||||
<p>
|
||||
If you would like to unsubscribe from our mailing list,
|
||||
you have come to the right page!
|
||||
</p>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMInput
|
||||
v-model="formData.email.value"
|
||||
name="email"
|
||||
label="Email"
|
||||
required
|
||||
:error="formData.email.error"
|
||||
@blur="fieldValidate(formData.email)" />
|
||||
<SMCaptchaNotice />
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMInput control="email" />
|
||||
<SMFormFooter>
|
||||
<template #right>
|
||||
<SMButton type="submit" label="Unsubscribe" />
|
||||
</template>
|
||||
</SMFormFooter>
|
||||
</form>
|
||||
</SMForm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1>Unsubscribed</h1>
|
||||
@@ -42,79 +30,56 @@
|
||||
</template>
|
||||
</SMDialog>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { api } from "../helpers/api";
|
||||
import { FormObject, FormControl } from "../helpers/form";
|
||||
import { And, Email, Required } from "../helpers/validate";
|
||||
import { ref, reactive } from "vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import { useRoute } from "vue-router";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../helpers/validation";
|
||||
import SMCaptchaNotice from "../components/SMCaptchaNotice.vue";
|
||||
import { useReCaptcha } from "vue-recaptcha-v3";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMDialog from "../components/SMDialog.vue";
|
||||
import SMForm from "../components/SMForm.vue";
|
||||
import SMFormFooter from "../components/SMFormFooter.vue";
|
||||
import SMInput from "../components/SMInput.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
|
||||
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
|
||||
const formLoading = ref(false);
|
||||
const formDone = ref(false);
|
||||
const formMessage = reactive({
|
||||
message: "",
|
||||
type: "error",
|
||||
icon: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
email: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "An email address is required.",
|
||||
email: true,
|
||||
email_message: "That does not look like an email address.",
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
email: FormControl("", And([Required(), Email()])),
|
||||
})
|
||||
);
|
||||
|
||||
useValidation(formData);
|
||||
|
||||
const submit = async () => {
|
||||
formLoading.value = true;
|
||||
formMessage.type = "error";
|
||||
formMessage.icon = "fa-solid fa-circle-exclamation";
|
||||
formMessage.message = "";
|
||||
const handleSubmit = async () => {
|
||||
form.loading(true);
|
||||
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
await recaptchaLoaded();
|
||||
const captcha = await executeRecaptcha("submit");
|
||||
|
||||
await axios.delete("subscriptions", {
|
||||
data: {
|
||||
email: formData.email.value,
|
||||
captcha_token: captcha,
|
||||
},
|
||||
});
|
||||
await api.delete({
|
||||
url: "/subscriptions",
|
||||
body: {
|
||||
email: form.email.value,
|
||||
captcha_token: captcha,
|
||||
},
|
||||
});
|
||||
|
||||
formDone.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
formDone.value = true;
|
||||
} catch (error) {
|
||||
form.apiErrors(error);
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
form.loading(false);
|
||||
};
|
||||
|
||||
if (useRoute().query.email !== undefined) {
|
||||
formData.email.value = useRoute().query.email;
|
||||
submit();
|
||||
form.email.value = useRoute().query.email;
|
||||
handleSubmit();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SMContainer class="mx-auto workshop-list">
|
||||
<SMPage class="mx-auto workshop-list">
|
||||
<h1>Workshops</h1>
|
||||
<div class="toolbar">
|
||||
<SMInput
|
||||
@@ -40,7 +40,7 @@
|
||||
event.location == 'online' ? 'Online Event' : event.address
|
||||
"></SMPanel>
|
||||
</SMPanelList>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -49,15 +49,14 @@ import SMInput from "../components/SMInput.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import SMPanelList from "../components/SMPanelList.vue";
|
||||
import SMPanel from "../components/SMPanel.vue";
|
||||
import SMPage from "../components/SMPage.vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { api } from "../helpers/api";
|
||||
import {
|
||||
buildUrlQuery,
|
||||
timestampLocalToUtc,
|
||||
timestampNowUtc,
|
||||
timestampUtcToLocal,
|
||||
} from "../helpers/common";
|
||||
import { format, parse, parseISO } from "date-fns";
|
||||
} from "../helpers/datetime";
|
||||
|
||||
const loading = ref(true);
|
||||
const events = reactive([]);
|
||||
@@ -103,11 +102,13 @@ const handleLoad = async () => {
|
||||
query["end_at"] = ">" + timestampNowUtc();
|
||||
}
|
||||
|
||||
const url = buildUrlQuery("events", query);
|
||||
let result = await axios.get(url);
|
||||
let result = await api.get({
|
||||
url: "/events",
|
||||
params: query,
|
||||
});
|
||||
|
||||
if (result.data.events) {
|
||||
events.value = result.data.events;
|
||||
if (result.json.events) {
|
||||
events.value = result.json.events;
|
||||
|
||||
events.value.forEach((item) => {
|
||||
item.start_at = timestampUtcToLocal(item.start_at);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div
|
||||
class="workshop-image"
|
||||
:style="{ backgroundImage: `url('${imageUrl}')` }">
|
||||
<font-awesome-icon
|
||||
<ion-icon
|
||||
v-if="imageUrl.length == 0"
|
||||
class="workshop-image-loader"
|
||||
icon="fa-regular fa-image" />
|
||||
name="image-outline" />
|
||||
</div>
|
||||
<template #inner>
|
||||
<SMMessage
|
||||
@@ -57,10 +57,7 @@
|
||||
label="Register for Event"></SMButton>
|
||||
</div>
|
||||
<div class="workshop-date">
|
||||
<h4>
|
||||
<font-awesome-icon
|
||||
icon="fa-regular fa-calendar" />Date / Time
|
||||
</h4>
|
||||
<h4><ion-icon name="calendar-outline" />Date / Time</h4>
|
||||
<p
|
||||
v-for="(line, index) in workshopDate"
|
||||
:key="index"
|
||||
@@ -69,10 +66,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="workshop-location">
|
||||
<h4>
|
||||
<font-awesome-icon
|
||||
icon="fa-solid fa-location-dot" />Location
|
||||
</h4>
|
||||
<h4><ion-icon name="location-outline" />Location</h4>
|
||||
<p>
|
||||
{{
|
||||
event.location == "online"
|
||||
@@ -88,19 +82,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import { api } from "../helpers/api";
|
||||
import { computed, ref, reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { format } from "date-fns";
|
||||
import SMButton from "../components/SMButton.vue";
|
||||
import SMHTML from "../components/SMHTML.vue";
|
||||
import SMMessage from "../components/SMMessage.vue";
|
||||
import {
|
||||
format,
|
||||
timestampUtcToLocal,
|
||||
timestampBeforeNow,
|
||||
timestampAfterNow,
|
||||
} from "../helpers/common";
|
||||
} from "../helpers/datetime";
|
||||
|
||||
const applicationStore = useApplicationStore();
|
||||
const event = ref({});
|
||||
@@ -162,8 +156,8 @@ const handleLoad = async () => {
|
||||
formMessage.message = "";
|
||||
|
||||
try {
|
||||
const result = await axios.get(`events/${route.params.id}`);
|
||||
event.value = result.data.event;
|
||||
const result = await api.get(`events/${route.params.id}`);
|
||||
event.value = result.json.event;
|
||||
|
||||
event.value.start_at = timestampUtcToLocal(event.value.start_at);
|
||||
event.value.end_at = timestampUtcToLocal(event.value.end_at);
|
||||
@@ -172,16 +166,16 @@ const handleLoad = async () => {
|
||||
handleLoadImage();
|
||||
} catch (error) {
|
||||
formMessage.message =
|
||||
error.response?.data?.message ||
|
||||
error.json?.message ||
|
||||
"Could not load event information from the server.";
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadImage = async () => {
|
||||
try {
|
||||
const result = await axios.get(`media/${event.value.hero}`);
|
||||
if (result.data.medium) {
|
||||
imageUrl.value = result.data.medium.url;
|
||||
const result = await api.get(`media/${event.value.hero}`);
|
||||
if (result.json.medium) {
|
||||
imageUrl.value = result.json.medium.url;
|
||||
}
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
|
||||
@@ -1,118 +1,72 @@
|
||||
<template>
|
||||
<SMContainer>
|
||||
<SMMessage
|
||||
v-if="formMessage.message"
|
||||
:type="formMessage.type"
|
||||
:message="formMessage.message"
|
||||
:icon="formMessage.icon" />
|
||||
<form @submit.prevent="submit">
|
||||
<SMPage>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
<SMInput
|
||||
v-model="formData.title.value"
|
||||
label="Title"
|
||||
required
|
||||
:error="formData.title.error"
|
||||
@blur="fieldValidate(formData.title)" />
|
||||
<SMInput control="title" />
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMEditor
|
||||
id="content"
|
||||
v-model="formData.content.value"
|
||||
v-model="form.content.value"
|
||||
@file-accept="fileAccept"
|
||||
@attachment-add="attachmentAdd" />
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMButton type="submit" label="Save" />
|
||||
</SMRow>
|
||||
</form>
|
||||
</SMContainer>
|
||||
</SMForm>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import DEditor from "../../components/SMEditor.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { FormObject, FormControl } from "../../helpers/form";
|
||||
import { And, Required, Min } from "../../helpers/validate";
|
||||
import { reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMDialog from "../../components/SMDialog.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import axios from "axios";
|
||||
import {
|
||||
useValidation,
|
||||
isValidated,
|
||||
fieldValidate,
|
||||
restParseErrors,
|
||||
} from "../../helpers/validation";
|
||||
import { useUserStore } from "@/store/UserStore";
|
||||
import { useRoute } from "vue-router";
|
||||
import { createTemplateLiteral } from "@vue/compiler-core";
|
||||
import SMPage from "../../components/SMPage.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const formMessage = reactive({
|
||||
icon: "",
|
||||
type: "",
|
||||
message: "",
|
||||
});
|
||||
const formData = reactive({
|
||||
title: {
|
||||
value: "",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A first name is needed",
|
||||
min: 2,
|
||||
min_message: "Your first name should be at least 2 letters long",
|
||||
},
|
||||
},
|
||||
content: {
|
||||
value: "<div>Hello <strong>People</strong> persons!</div>",
|
||||
error: "",
|
||||
rules: {
|
||||
required: true,
|
||||
required_message: "A last name is needed",
|
||||
min: 2,
|
||||
min_message: "Your last name should be at least 2 letters long",
|
||||
},
|
||||
},
|
||||
});
|
||||
const form = reactive(
|
||||
FormObject({
|
||||
title: FormControl("", And([Required(), Min(2)])),
|
||||
content: FormControl("", Required()),
|
||||
})
|
||||
);
|
||||
|
||||
useValidation(formData);
|
||||
// const getPostById = async () => {
|
||||
// try {
|
||||
// if (isValidated(formData)) {
|
||||
// let res = await axios.get("posts/" + route.params.id);
|
||||
|
||||
const getPostById = async () => {
|
||||
// formData.title.value = res.data.title;
|
||||
// formData.content.value = res.data.content;
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.log(err);
|
||||
// formMessage.icon = "";
|
||||
// formMessage.type = "error";
|
||||
// formMessage.message = "";
|
||||
// restParseErrors(formData, [formMessage, "message"], err);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
let res = await axios.get("posts/" + route.params.id);
|
||||
await api.post({
|
||||
url: "/posts",
|
||||
body: {
|
||||
title: form.title.value,
|
||||
content: form.content.value,
|
||||
},
|
||||
});
|
||||
|
||||
formData.title.value = res.data.title;
|
||||
formData.content.value = res.data.content;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
formMessage.icon = "";
|
||||
formMessage.type = "error";
|
||||
formMessage.message = "";
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (isValidated(formData)) {
|
||||
let res = await axios.post("posts", {
|
||||
title: formData.title.value,
|
||||
content: formData.content.value,
|
||||
});
|
||||
|
||||
console.log(ref);
|
||||
formMessage.type = "success";
|
||||
formMessage.message = "Your details have been updated";
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
formMessage.icon = "";
|
||||
formMessage.type = "error";
|
||||
formMessage.message = "";
|
||||
restParseErrors(formData, [formMessage, "message"], err);
|
||||
form.message("The post has been saved", "success");
|
||||
} catch (error) {
|
||||
form.apiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,21 +116,3 @@ const attachmentAdd = async (event) => {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// .dialog {
|
||||
// flex-direction: column;
|
||||
// margin: 0 auto;
|
||||
// max-width: 600px;
|
||||
// }
|
||||
|
||||
// .buttonFooter {
|
||||
// flex-direction: row;
|
||||
// }
|
||||
|
||||
// @media screen and (max-width: 768px) {
|
||||
// .buttonFooter {
|
||||
// flex-direction: column-reverse;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
<template>
|
||||
<SMContainer class="dashboard mx-auto">
|
||||
<SMPage class="dashboard mx-auto">
|
||||
<h1>Dashboard</h1>
|
||||
<div class="boxes">
|
||||
<router-link to="/dashboard/details" class="box">
|
||||
<font-awesome-icon icon="fa-solid fa-user-pen" />
|
||||
<ion-icon name="location-outline" />
|
||||
<h2>My Details</h2>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/posts')"
|
||||
to="/dashboard/posts"
|
||||
class="box">
|
||||
<font-awesome-icon icon="fa-regular fa-newspaper" />
|
||||
<ion-icon name="newspaper-outline" />
|
||||
<h2>Posts</h2>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/users')"
|
||||
:to="{ name: 'user-list' }"
|
||||
class="box">
|
||||
<font-awesome-icon icon="fa-solid fa-users" />
|
||||
<ion-icon name="people-outline" />
|
||||
<h2>Users</h2>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/events')"
|
||||
to="/dashboard/events"
|
||||
class="box">
|
||||
<font-awesome-icon icon="fa-regular fa-calendar" />
|
||||
<ion-icon name="calendar-outline" />
|
||||
<h2>Events</h2>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/courses')"
|
||||
to="/dashboard/courses"
|
||||
class="box">
|
||||
<font-awesome-icon icon="fa-solid fa-graduation-cap" />
|
||||
<ion-icon name="school-outline" />
|
||||
<h2>{{ courseBoxTitle }}</h2>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/media')"
|
||||
to="/dashboard/media"
|
||||
class="box">
|
||||
<font-awesome-icon icon="fa-solid fa-photo-film" />
|
||||
<ion-icon name="film-outline" />
|
||||
<h2>Media</h2>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('logs/discord')"
|
||||
:to="{ name: 'discord-bot-logs' }"
|
||||
class="box">
|
||||
<font-awesome-icon icon="fa-brands fa-discord" />
|
||||
<ion-icon name="logo-discord" />
|
||||
<h2>Discord Bot Logs</h2>
|
||||
</router-link>
|
||||
</div>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SMPage from "../../components/SMPage.vue";
|
||||
import { computed } from "vue";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
|
||||
@@ -90,6 +91,7 @@ const courseBoxTitle = computed(() => {
|
||||
font-size: map-get($spacer, 3);
|
||||
color: $font-color;
|
||||
transition: background-color 0.3s, border 0.3s;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
@@ -97,7 +99,7 @@ const courseBoxTitle = computed(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
ion-icon {
|
||||
font-size: map-get($spacer, 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,6 @@ import {
|
||||
} from "../../helpers/validation";
|
||||
import { useRoute } from "vue-router";
|
||||
import { timestampLocalToUtc, timestampUtcToLocal } from "../../helpers/common";
|
||||
import { parseISO } from "date-fns";
|
||||
|
||||
const route = useRoute();
|
||||
const formLoading = ref(false);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
:items="items"
|
||||
:search-value="search">
|
||||
<template #loading>
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
</template>
|
||||
<template #item-title="item">
|
||||
<router-link
|
||||
@@ -36,12 +36,12 @@
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<div class="action-wrapper">
|
||||
<font-awesome-icon
|
||||
<!-- <font-awesome-icon
|
||||
icon="fa-solid fa-pen-to-square"
|
||||
@click="handleEdit(item)" />
|
||||
<font-awesome-icon
|
||||
icon="fa-regular fa-trash-can"
|
||||
@click="handleDelete(item)" />
|
||||
@click="handleDelete(item)" /> -->
|
||||
</div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
@@ -66,6 +66,7 @@ import { debounce } from "../../helpers/common";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import { restParseErrors } from "../../helpers/validation";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
|
||||
@@ -29,19 +29,19 @@
|
||||
:items="items"
|
||||
:search-value="search">
|
||||
<template #loading>
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
</template>
|
||||
<template #item-size="item">
|
||||
{{ bytesReadable(item.size) }}
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<div class="action-wrapper">
|
||||
<font-awesome-icon
|
||||
<!-- <font-awesome-icon
|
||||
icon="fa-solid fa-pen-to-square"
|
||||
@click.stop="handleEdit(item)" />
|
||||
<font-awesome-icon
|
||||
icon="fa-regular fa-trash-can"
|
||||
@click.stop="handleDelete(item)" />
|
||||
@click.stop="handleDelete(item)" /> -->
|
||||
<d-file-link :href="item.url" target="_blank" @click.stop=""
|
||||
><font-awesome-icon icon="fa-solid fa-download"
|
||||
/></d-file-link>
|
||||
@@ -65,6 +65,7 @@ import { debounce, parseErrorType, bytesReadable } from "../../helpers/common";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import DFileLink from "../../components/DFileLink.vue";
|
||||
import { useUserStore } from "../../store/UserStore";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
:items="items"
|
||||
:search-value="search">
|
||||
<template #loading>
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
</template>
|
||||
<template #item-title="item">
|
||||
<router-link
|
||||
@@ -36,12 +36,12 @@
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<div class="action-wrapper">
|
||||
<font-awesome-icon
|
||||
<!-- <font-awesome-icon
|
||||
icon="fa-solid fa-pen-to-square"
|
||||
@click="handleEdit(item)" />
|
||||
<font-awesome-icon
|
||||
icon="fa-regular fa-trash-can"
|
||||
@click="handleDelete(item)" />
|
||||
@click="handleDelete(item)" /> -->
|
||||
</div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
@@ -62,6 +62,7 @@ import SMButton from "../../components/SMButton.vue";
|
||||
import { debounce } from "../../helpers/common";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
:header-item-class-name="headerItemClassNameFunction"
|
||||
:body-item-class-name="bodyItemClassNameFunction">
|
||||
<template #loading>
|
||||
<font-awesome-icon icon="fa-solid fa-spinner" pulse />
|
||||
<SMLoadingIcon />
|
||||
</template>
|
||||
<template #item-actions="item">
|
||||
<div class="action-wrapper">
|
||||
<font-awesome-icon
|
||||
<!-- <font-awesome-icon
|
||||
icon="fa-solid fa-pen-to-square"
|
||||
@click="handleEdit(item)" />
|
||||
<font-awesome-icon
|
||||
icon="fa-regular fa-trash-can"
|
||||
@click="handleDelete(item)" />
|
||||
@click="handleDelete(item)" /> -->
|
||||
</div>
|
||||
</template>
|
||||
</EasyDataTable>
|
||||
@@ -42,6 +42,7 @@ import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import { openDialog } from "vue3-promise-dialog";
|
||||
import SMHeading from "../../components/SMHeading.vue";
|
||||
import SMMessage from "../../components/SMMessage.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const searchValue = ref("");
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
@vite('resources/js/main.js')
|
||||
<script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script>
|
||||
<script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,8 @@ export default defineConfig({
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => ["trix-editor"].includes(tag),
|
||||
isCustomElement: (tag) =>
|
||||
["trix-editor", "ion-icon"].includes(tag),
|
||||
},
|
||||
transformAssetUrls: {
|
||||
base: null,
|
||||
|
||||
Reference in New Issue
Block a user