Compare commits
11 Commits
83bf47287c
...
vue
| Author | SHA1 | Date | |
|---|---|---|---|
| d9bc89b7f6 | |||
| 760f38374f | |||
| b9a4bd6d2d | |||
| ea14a1ef3e | |||
| 457bab2c18 | |||
| 3571f93e41 | |||
| e878b159bf | |||
| db70593852 | |||
| 73b8e6b04f | |||
| a49f73236b | |||
| c0b7ad8000 |
21
composer.lock
generated
21
composer.lock
generated
@@ -12,7 +12,7 @@
|
|||||||
"dist": {
|
"dist": {
|
||||||
"type": "path",
|
"type": "path",
|
||||||
"url": "../objects",
|
"url": "../objects",
|
||||||
"reference": "a9f8601384a0078cf8531576768e2c5bad4bbf95"
|
"reference": "bcae723140735b1432caaf3070ef4e29ecb73a76"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -27,16 +27,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "phpmailer/phpmailer",
|
||||||
"version": "v6.8.1",
|
"version": "v6.10.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
"reference": "e88da8d679acc3824ff231fdc553565b802ac016"
|
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016",
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||||
"reference": "e88da8d679acc3824ff231fdc553565b802ac016",
|
"reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"yoast/phpunit-polyfills": "^1.0.4"
|
"yoast/phpunit-polyfills": "^1.0.4"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1"
|
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-08-29T08:26:30+00:00"
|
"time": "2025-04-24T15:19:31+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
},
|
},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.3.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Settings
|
|||||||
const DB_NAME = 'spot';
|
const DB_NAME = 'spot';
|
||||||
const DB_ENC = 'utf8mb4';
|
const DB_ENC = 'utf8mb4';
|
||||||
const TEXT_ENC = 'UTF-8';
|
const TEXT_ENC = 'UTF-8';
|
||||||
const TIMEZONE = 'Europe/Paris';
|
const TIMEZONE = 'Europe/Zurich';
|
||||||
const MAIL_SERVER = '';
|
const MAIL_SERVER = '';
|
||||||
const MAIL_FROM = '';
|
const MAIL_FROM = '';
|
||||||
const MAIL_USER = '';
|
const MAIL_USER = '';
|
||||||
|
|||||||
69304
geo/snt.gpx
Executable file
69304
geo/snt.gpx
Executable file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ upload_title = Picture & Video Uploads
|
|||||||
upload_mode_archived= Project "$0" is archived. No upload allowed
|
upload_mode_archived= Project "$0" is archived. No upload allowed
|
||||||
upload_success = $0 uploaded successfully
|
upload_success = $0 uploaded successfully
|
||||||
upload_media_exist = Picture $0 already exists
|
upload_media_exist = Picture $0 already exists
|
||||||
|
new_position = New Position
|
||||||
|
|
||||||
post_message = Message
|
post_message = Message
|
||||||
post_name = Name
|
post_name = Name
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ upload_title = Cargar fotos y videos
|
|||||||
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
upload_mode_archived= El proyecto "$0" esta archivado. No se puede cargar
|
||||||
upload_success = $0 ha sido subido
|
upload_success = $0 ha sido subido
|
||||||
upload_media_exist = La imagen $0 ya existe
|
upload_media_exist = La imagen $0 ya existe
|
||||||
|
new_position = Nueva posición
|
||||||
|
|
||||||
post_message = Mensaje
|
post_message = Mensaje
|
||||||
post_name = Nombre
|
post_name = Nombre
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ upload_title = Uploader photos & vidéos
|
|||||||
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
upload_mode_archived= Le projet "$0" a été archivé. Aucun upload possible
|
||||||
upload_success = $0 a été uploadé
|
upload_success = $0 a été uploadé
|
||||||
upload_media_exist = l'image $0 existe déjà
|
upload_media_exist = l'image $0 existe déjà
|
||||||
|
new_position = Nouvelle position
|
||||||
|
|
||||||
post_message = Message
|
post_message = Message
|
||||||
post_name = Nom
|
post_name = Nom
|
||||||
|
|||||||
@@ -33,12 +33,10 @@ class Converter extends PhpObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function isGeoJsonValid($sCodeName) {
|
public static function isGeoJsonValid($sCodeName) {
|
||||||
$bResult = false;
|
|
||||||
$sGpxFilePath = Gpx::getFilePath($sCodeName);
|
$sGpxFilePath = Gpx::getFilePath($sCodeName);
|
||||||
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
|
$sGeoJsonFilePath = GeoJson::getFilePath($sCodeName);
|
||||||
|
|
||||||
//No need to generate if gpx is missing
|
//No need to generate if gpx is missing
|
||||||
if(!file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) > filemtime(Gpx::getFilePath($sCodeName))) $bResult = true;
|
return !file_exists($sGpxFilePath) || file_exists($sGeoJsonFilePath) && filemtime($sGeoJsonFilePath) >= filemtime($sGpxFilePath);
|
||||||
return $bResult;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
25
lib/Feed.php
25
lib/Feed.php
@@ -168,6 +168,31 @@ class Feed extends PhpObject {
|
|||||||
return $bNewMsg;
|
return $bNewMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addManualPosition($sLat, $sLng, $iTimestamp) {
|
||||||
|
$sTimeZone = date_default_timezone_get();
|
||||||
|
$oDateTime = new \DateTime('@'.$iTimestamp);
|
||||||
|
$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
|
||||||
|
$asWeather = $this->getWeather(array($sLat, $sLng), $iTimestamp);
|
||||||
|
|
||||||
|
$asMsg = [
|
||||||
|
'ref_msg_id' => $iTimestamp.'/man',
|
||||||
|
'id_feed' => $this->getFeedId(),
|
||||||
|
'type' => 'OK',
|
||||||
|
'latitude' => $sLat,
|
||||||
|
'longitude' => $sLng,
|
||||||
|
'iso_time' => $oDateTime->format("Y-m-d\TH:i:sO"), //Incorrect ISO 8601 format, but compliant with Spot data
|
||||||
|
'site_time' => $oDateTime->format(Db::TIMESTAMP_FORMAT),
|
||||||
|
'timezone' => $sTimeZone,
|
||||||
|
'unix_time' => $iTimestamp,
|
||||||
|
'content' => '',
|
||||||
|
'battery_state' => '',
|
||||||
|
'posted_on' => date(Db::TIMESTAMP_FORMAT),
|
||||||
|
];
|
||||||
|
|
||||||
|
$iMessageId = $this->oDb->insertRow(self::MSG_TABLE, array_merge($asMsg, $asWeather));
|
||||||
|
return $iMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
private function updateFeed() {
|
private function updateFeed() {
|
||||||
$bNewMsg = false;
|
$bNewMsg = false;
|
||||||
$asData = $this->retrieveFeed();
|
$asData = $this->retrieveFeed();
|
||||||
|
|||||||
33
lib/Spot.php
33
lib/Spot.php
@@ -223,6 +223,15 @@ class Spot extends Main
|
|||||||
|
|
||||||
//Send Update Email
|
//Send Update Email
|
||||||
if($bNewMsg) {
|
if($bNewMsg) {
|
||||||
|
$bSuccess = $this->sendEmail();
|
||||||
|
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||||
|
}
|
||||||
|
else $sDesc = 'no_new_msg';
|
||||||
|
|
||||||
|
return self::getJsonResult($bSuccess, $sDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendEmail() {
|
||||||
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
$oEmail = new Email($this->asContext['serv_name'], 'email_update');
|
||||||
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
|
||||||
|
|
||||||
@@ -250,12 +259,7 @@ class Spot extends Main
|
|||||||
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
if($iPostCount == self::MAIL_CHUNK_SIZE) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bSuccess = $oEmail->send();
|
return $oEmail->send();
|
||||||
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
|
||||||
}
|
|
||||||
else $sDesc = 'no_new_msg';
|
|
||||||
|
|
||||||
return self::getJsonResult($bSuccess, $sDesc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function genCronFile() {
|
public function genCronFile() {
|
||||||
@@ -632,6 +636,19 @@ class Spot extends Main
|
|||||||
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addPosition($sLat, $sLng, $iTimestamp) {
|
||||||
|
$oFeed = new Feed($this->oDb, $this->oProject->getFeedIds()[0]);
|
||||||
|
$bSuccess = ($oFeed->addManualPosition($sLat, $sLng, $iTimestamp) > 0);
|
||||||
|
|
||||||
|
if($bSuccess) {
|
||||||
|
$bSuccess = $this->sendEmail();
|
||||||
|
$sDesc = $bSuccess?'mail_sent':'mail_failure';
|
||||||
|
}
|
||||||
|
else $sDesc = 'error_commit_db';
|
||||||
|
|
||||||
|
return self::getJsonResult($bSuccess, $sDesc);
|
||||||
|
}
|
||||||
|
|
||||||
public function getAdminSettings($sType='') {
|
public function getAdminSettings($sType='') {
|
||||||
$oFeed = new Feed($this->oDb);
|
$oFeed = new Feed($this->oDb);
|
||||||
$asData = array(
|
$asData = array(
|
||||||
@@ -774,6 +791,10 @@ class Spot extends Main
|
|||||||
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
return self::getJsonResult($bSuccess, $sDesc, $asResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function buildGeoJSON($sCodeName) {
|
||||||
|
return Converter::convertToGeoJson($sCodeName);
|
||||||
|
}
|
||||||
|
|
||||||
public static function decToDms($dValue, $sType) {
|
public static function decToDms($dValue, $sType) {
|
||||||
if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude
|
if($sType=='lat') $sDirection = ($dValue >= 0)?'N':'S'; //Latitude
|
||||||
else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude
|
else $sDirection = ($dValue >= 0)?'E':'W'; //Longitude
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ $oValue = $_REQUEST['value'] ?? '';
|
|||||||
$iId = $_REQUEST['id'] ?? 0 ;
|
$iId = $_REQUEST['id'] ?? 0 ;
|
||||||
$sType = $_REQUEST['type'] ?? '';
|
$sType = $_REQUEST['type'] ?? '';
|
||||||
$sEmail = $_REQUEST['email'] ?? '';
|
$sEmail = $_REQUEST['email'] ?? '';
|
||||||
|
$sLat = $_REQUEST['latitude'] ?? '';
|
||||||
|
$sLng = $_REQUEST['longitude'] ?? '';
|
||||||
|
$iTimestamp = $_REQUEST['timestamp'] ?? 0;
|
||||||
|
|
||||||
//Initiate class
|
//Initiate class
|
||||||
$oSpot = new Spot(__FILE__, $sTimezone);
|
$oSpot = new Spot(__FILE__, $sTimezone);
|
||||||
@@ -74,6 +77,9 @@ if($sAction!='')
|
|||||||
case 'add_comment':
|
case 'add_comment':
|
||||||
$sResult = $oSpot->addComment($iId, $sContent);
|
$sResult = $oSpot->addComment($iId, $sContent);
|
||||||
break;
|
break;
|
||||||
|
case 'add_position':
|
||||||
|
$sResult = $oSpot->addPosition($sLat, $sLng, $iTimestamp);
|
||||||
|
break;
|
||||||
case 'admin_get':
|
case 'admin_get':
|
||||||
$sResult = $oSpot->getAdminSettings();
|
$sResult = $oSpot->getAdminSettings();
|
||||||
break;
|
break;
|
||||||
@@ -92,6 +98,9 @@ if($sAction!='')
|
|||||||
case 'sql':
|
case 'sql':
|
||||||
$sResult = $oSpot->getDbBuildScript();
|
$sResult = $oSpot->getDbBuildScript();
|
||||||
break;
|
break;
|
||||||
|
case 'build_geojson':
|
||||||
|
$sResult = $oSpot->buildGeoJSON($sName);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
$sResult = Main::getJsonResult(false, Main::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Spot Project
|
# Spot Project
|
||||||
[Spot](https://www.findmespot.com) & GPX integration
|
[Spot](https://www.findmespot.com) & GPX integration
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
* npm 18+
|
* npm 18+
|
||||||
* composer
|
* composer
|
||||||
* php-mbstring
|
* php-mbstring
|
||||||
@@ -11,13 +13,17 @@
|
|||||||
* ffprobe & ffmpeg
|
* ffprobe & ffmpeg
|
||||||
* STARTTLS Email Server (use Gmail if none available)
|
* STARTTLS Email Server (use Gmail if none available)
|
||||||
* Optional: Geo Caching Server (WMTS Caching Service)
|
* Optional: Geo Caching Server (WMTS Caching Service)
|
||||||
|
|
||||||
## PHP Configuration
|
## PHP Configuration
|
||||||
|
|
||||||
* max_execution_time = 300
|
* max_execution_time = 300
|
||||||
* memory_limit = 500M
|
* memory_limit = 500M
|
||||||
* post_max_size = 4G
|
* post_max_size = 4G
|
||||||
* upload_max_filesize = 4G
|
* upload_max_filesize = 4G
|
||||||
* max_file_uploads = 50
|
* max_file_uploads = 50
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Clone Git onto web server
|
1. Clone Git onto web server
|
||||||
2. composer install
|
2. composer install
|
||||||
3. npm install webpack
|
3. npm install webpack
|
||||||
@@ -27,7 +33,9 @@
|
|||||||
7. Copy settings-sample.php to settings.php and populate
|
7. Copy settings-sample.php to settings.php and populate
|
||||||
8. Go to #admin and create a new project, feed & maps
|
8. Go to #admin and create a new project, feed & maps
|
||||||
9. Add a GPX file named <project_codename>.gpx to /geo/
|
9. Add a GPX file named <project_codename>.gpx to /geo/
|
||||||
|
|
||||||
## To Do List
|
## To Do List
|
||||||
|
|
||||||
* Add mail frequency slider
|
* Add mail frequency slider
|
||||||
* Use WMTS servers directly when not using Geo Caching Server
|
* Use WMTS servers directly when not using Geo Caching Server
|
||||||
* Allow HEIF picture format
|
* Allow HEIF picture format
|
||||||
|
|||||||
19
src/Spot.vue
19
src/Spot.vue
@@ -1,21 +1,27 @@
|
|||||||
<script>
|
<script>
|
||||||
import Project from './components/project.vue';
|
import Project from './components/project.vue';
|
||||||
import Admin from './components/admin.vue';
|
import Admin from './components/admin.vue';
|
||||||
|
import Upload from './components/upload.vue';
|
||||||
|
|
||||||
const aoRoutes = {
|
const aoRoutes = {
|
||||||
'project': Project,
|
'project': Project,
|
||||||
'admin': Admin
|
'admin': Admin,
|
||||||
|
'upload': Upload
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hash: {}
|
hash: {},
|
||||||
|
consts: this.spot.consts,
|
||||||
|
user: this.spot.vars('user')
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
projects: this.spot.vars('projects')
|
projects: this.spot.vars('projects'),
|
||||||
|
consts: this.consts,
|
||||||
|
user: this.user
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ['spot'],
|
inject: ['spot'],
|
||||||
@@ -25,12 +31,15 @@ export default {
|
|||||||
return aoRoutes[this.hash.page];
|
return aoRoutes[this.hash.page];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
//User
|
||||||
|
this.user.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || this.consts.default_timezone;
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('hashchange', () => {this.onHashChange();});
|
window.addEventListener('hashchange', () => {this.onHashChange();});
|
||||||
var oEvent = new Event('hashchange');
|
var oEvent = new Event('hashchange');
|
||||||
window.dispatchEvent(oEvent);
|
window.dispatchEvent(oEvent);
|
||||||
}
|
},
|
||||||
,
|
|
||||||
methods: {
|
methods: {
|
||||||
_hash(hash, bReboot) {
|
_hash(hash, bReboot) {
|
||||||
bReboot = bReboot || false;
|
bReboot = bReboot || false;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import SpotIcon from './spotIcon.vue';
|
||||||
import SpotButton from './spotButton.vue';
|
import SpotButton from './spotButton.vue';
|
||||||
import AdminInput from './adminInput.vue';
|
import AdminInput from './adminInput.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
SpotIcon,
|
||||||
SpotButton,
|
SpotButton,
|
||||||
AdminInput
|
AdminInput
|
||||||
},
|
},
|
||||||
@@ -51,7 +53,6 @@ export default {
|
|||||||
createElem(sType) {
|
createElem(sType) {
|
||||||
this.spot.get2('admin_create', {type: sType})
|
this.spot.get2('admin_create', {type: sType})
|
||||||
.then((aoNewElemTypes) => {
|
.then((aoNewElemTypes) => {
|
||||||
console.log(aoNewElemTypes);
|
|
||||||
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
|
for(const [sType, aoNewElems] of Object.entries(aoNewElemTypes)) {
|
||||||
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
|
for(const [iKey, oNewElem] of Object.entries(aoNewElems)) {
|
||||||
oNewElem.type = sType;
|
oNewElem.type = sType;
|
||||||
@@ -60,7 +61,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((sMsg) => {console.log(sMsg);this.spot.onFeedback('error', sMsg, {'create':sType});});
|
.catch((sMsg) => {this.spot.onFeedback('error', sMsg, {'create':sType});});
|
||||||
},
|
},
|
||||||
deleteElem(oElem) {
|
deleteElem(oElem) {
|
||||||
const asInputs = {
|
const asInputs = {
|
||||||
@@ -118,7 +119,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div id="admin">
|
<div id="admin">
|
||||||
<a name="back" class="button" href="#project"><i class="fa fa-back push"></i>{{ l('nav_back') }}</a>
|
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="l('nav_back')" /></a>
|
||||||
<h1>{{ l('projects') }}</h1>
|
<h1>{{ l('projects') }}</h1>
|
||||||
<div id="project_section">
|
<div id="project_section">
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
import { Map, NavigationControl, Marker, LngLatBounds, LngLat } from 'maplibre-gl';
|
import { Map, NavigationControl, Marker, LngLatBounds, LngLat, Popup } from 'maplibre-gl';
|
||||||
|
import { createApp, defineComponent, nextTick, ref, defineCustomElement, provide, inject } from 'vue';
|
||||||
import simplebar from 'simplebar-vue';
|
import simplebar from 'simplebar-vue';
|
||||||
|
|
||||||
import autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
@@ -14,12 +14,14 @@ import lightbox from '../scripts/lightbox.js';
|
|||||||
import SpotIcon from './spotIcon.vue';
|
import SpotIcon from './spotIcon.vue';
|
||||||
import SpotButton from './spotButton.vue';
|
import SpotButton from './spotButton.vue';
|
||||||
import ProjectPost from './projectPost.vue';
|
import ProjectPost from './projectPost.vue';
|
||||||
|
import ProjectPopup from './projectPopup.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SpotIcon,
|
SpotIcon,
|
||||||
SpotButton,
|
SpotButton,
|
||||||
ProjectPost,
|
ProjectPost,
|
||||||
|
ProjectPopup,
|
||||||
simplebar
|
simplebar
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -36,7 +38,6 @@ export default {
|
|||||||
posts: [],
|
posts: [],
|
||||||
nlFeedbacks: [],
|
nlFeedbacks: [],
|
||||||
nlLoading: false,
|
nlLoading: false,
|
||||||
user: {name:'', email:''},
|
|
||||||
baseMaps: {},
|
baseMaps: {},
|
||||||
baseMap: null,
|
baseMap: null,
|
||||||
messages: null,
|
messages: null,
|
||||||
@@ -73,7 +74,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
baseMap(sNewBaseMap, sOldBaseMap) {
|
baseMap(sNewBaseMap, sOldBaseMap) {
|
||||||
if(sOldBaseMap) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
if(sOldBaseMap) this.map.setLayoutProperty(sOldBaseMap, 'visibility', 'none');
|
||||||
this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
if(sNewBaseMap) this.map.setLayoutProperty(sNewBaseMap, 'visibility', 'visible');
|
||||||
},
|
},
|
||||||
projectCodename(sNewCodeName, sOldCodeName) {
|
projectCodename(sNewCodeName, sOldCodeName) {
|
||||||
console.log('change in projectCodename: '+sNewCodeName);
|
console.log('change in projectCodename: '+sNewCodeName);
|
||||||
@@ -84,11 +85,10 @@ export default {
|
|||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
user: this.user,
|
|
||||||
project: this.project
|
project: this.project
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ['spot', 'projects'],
|
inject: ['spot', 'projects', 'user'],
|
||||||
mounted() {
|
mounted() {
|
||||||
this.spot.addPage('project', {
|
this.spot.addPage('project', {
|
||||||
onResize: () => {
|
onResize: () => {
|
||||||
@@ -110,7 +110,6 @@ export default {
|
|||||||
this.initProject();
|
this.initProject();
|
||||||
if(bFirstLoad) this.initLightbox();
|
if(bFirstLoad) this.initLightbox();
|
||||||
this.initFeed();
|
this.initFeed();
|
||||||
if(bFirstLoad) this.initSettings();
|
|
||||||
this.initMap();
|
this.initMap();
|
||||||
},
|
},
|
||||||
initProject() {
|
initProject() {
|
||||||
@@ -152,12 +151,10 @@ export default {
|
|||||||
//Scroll to post
|
//Scroll to post
|
||||||
if(this.$parent.hash.items.length == 3) this.findPost({type: this.$parent.hash.items[1], id: this.$parent.hash.items[2]});
|
if(this.$parent.hash.items.length == 3) this.findPost({type: this.$parent.hash.items[1], id: this.$parent.hash.items[2]});
|
||||||
},
|
},
|
||||||
initSettings() {
|
|
||||||
this.user = this.spot.vars('user');
|
|
||||||
},
|
|
||||||
async initMap() {
|
async initMap() {
|
||||||
//Get Map Info
|
//Get Map Info
|
||||||
const aoMarkers = await this.spot.get2('markers', {id_project: this.project.id});
|
const aoMarkers = await this.spot.get2('markers', {id_project: this.project.id});
|
||||||
|
this.baseMap = null;
|
||||||
this.baseMaps = aoMarkers.maps;
|
this.baseMaps = aoMarkers.maps;
|
||||||
this.messages = aoMarkers.messages;
|
this.messages = aoMarkers.messages;
|
||||||
|
|
||||||
@@ -193,7 +190,10 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.map.once('load', async () => {
|
this.map.once('load', async () => {
|
||||||
//Track
|
//Default Basemap
|
||||||
|
this.baseMap = this.baseMaps.filter((asBM) => asBM.default_map)[0].codename;
|
||||||
|
|
||||||
|
//Get track
|
||||||
const oTrack = await this.spot.get2('geojson', {id_project: this.project.id});
|
const oTrack = await this.spot.get2('geojson', {id_project: this.project.id});
|
||||||
this.map.addSource('track', {
|
this.map.addSource('track', {
|
||||||
'type': 'geojson',
|
'type': 'geojson',
|
||||||
@@ -224,9 +224,91 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//Markers
|
//Markers
|
||||||
|
let aoMarkerSource = {type:'geojson', data:{type: 'FeatureCollection', features: []}};
|
||||||
for(const oMsg of this.messages) {
|
for(const oMsg of this.messages) {
|
||||||
new Marker().setLngLat(new LngLat(oMsg.longitude, oMsg.latitude)).addTo(this.map);
|
aoMarkerSource.data.features.push({
|
||||||
|
'type': 'Feature',
|
||||||
|
'properties': {
|
||||||
|
...oMsg,
|
||||||
|
...{'description': ''}
|
||||||
|
},
|
||||||
|
'geometry': {
|
||||||
|
'type': 'Point',
|
||||||
|
'coordinates': [oMsg.longitude, oMsg.latitude]
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
//Tooltip
|
||||||
|
/*
|
||||||
|
let $Tooltip = $($('<div>', {'class':'info-window'})
|
||||||
|
.append($('<h1>')
|
||||||
|
.addIcon('fa-message fa-lg', true)
|
||||||
|
.append($('<span>').text(this.spot.lang('post_message')+' '+this.spot.lang('counter', oMsg.displayed_id)))
|
||||||
|
.append($('<span>', {'class':'message-type'}).text('('+oMsg.type+')'))
|
||||||
|
)
|
||||||
|
.append($('<div>', {'class':'separator'}))
|
||||||
|
.append($('<p>', {'class':'coordinates'})
|
||||||
|
.addIcon('fa-coords fa-fw fa-lg', true)
|
||||||
|
.append(this.getGoogleMapsLink(oMsg))
|
||||||
|
)
|
||||||
|
.append($('<p>', {'class':'time'})
|
||||||
|
.addIcon('fa-time fa-fw fa-lg', true)
|
||||||
|
.append(oMsg.formatted_time+(this.project.mode==this.spot.consts.modes.blog?' ('+oMsg.relative_time+')':''))))[0];
|
||||||
|
|
||||||
|
const vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
|
||||||
|
|
||||||
|
//let vTooltip = h(SpotIcon, {icon:'project', 'classes':'fa-fw', text:'hikes'});
|
||||||
|
|
||||||
|
oPopup.setDOMContent(vTooltip);
|
||||||
|
|
||||||
|
new Marker({
|
||||||
|
element: $('<div style="width:'+this.markerSize.width+'px;height:'+this.markerSize.height+'px;"><span class="fa-stack"><i class="fa fa-message fa-stack-2x"></i><i class="fa fa-message-in fa-rotate-270 fa-stack-1x"></i></span></div>')[0],
|
||||||
|
anchor: 'bottom'
|
||||||
|
})
|
||||||
|
.setLngLat(new LngLat(oMsg.longitude, oMsg.latitude))
|
||||||
|
.setPopup(oPopup)
|
||||||
|
.addTo(this.map)
|
||||||
|
;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
this.map.addSource('markers', aoMarkerSource);
|
||||||
|
const image = await this.map.loadImage('https://maplibre.org/maplibre-gl-js/docs/assets/custom_marker.png');
|
||||||
|
this.map.addImage('markerIcon', image.data);
|
||||||
|
this.map.addLayer({
|
||||||
|
'id': 'markers',
|
||||||
|
'type': 'symbol',
|
||||||
|
'source': 'markers',
|
||||||
|
'layout': {
|
||||||
|
//'icon-anchor': 'bottom',
|
||||||
|
'icon-image': 'markerIcon'
|
||||||
|
//'icon-overlap': 'always'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.map.on("click", "markers", (e) => {
|
||||||
|
var oPopup = new Popup({
|
||||||
|
anchor: 'bottom',
|
||||||
|
offset: [0, this.markerSize.height * -1],
|
||||||
|
closeButton: false
|
||||||
|
})
|
||||||
|
.setHTML('<div id="popup"></div>')
|
||||||
|
.setLngLat(e.lngLat)
|
||||||
|
.addTo(this.map);
|
||||||
|
|
||||||
|
let rProp = ref(e.features[0].properties);
|
||||||
|
const vPopup = defineComponent({
|
||||||
|
extends: ProjectPopup,
|
||||||
|
setup: () => {
|
||||||
|
console.log(rProp.value);
|
||||||
|
provide('options', rProp.value);
|
||||||
|
provide('spot', this.spot);
|
||||||
|
provide('project', this.project);
|
||||||
|
return {'options': rProp.value, 'spot':this.spot, 'project':this.project};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nextTick(() => {
|
||||||
|
createApp(vPopup).mount("#popup");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
//Centering map
|
//Centering map
|
||||||
let bOpenFeedPanel = !this.mobile;
|
let bOpenFeedPanel = !this.mobile;
|
||||||
@@ -242,7 +324,6 @@ export default {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//Fit to track
|
//Fit to track
|
||||||
console.log('Fit to track');
|
|
||||||
for(const iFeatureId in oTrack.features) {
|
for(const iFeatureId in oTrack.features) {
|
||||||
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
|
oBounds = oTrack.features[iFeatureId].geometry.coordinates.reduce(
|
||||||
(bounds, coord) => {
|
(bounds, coord) => {
|
||||||
@@ -269,9 +350,6 @@ export default {
|
|||||||
|
|
||||||
//Toggle only when map is ready, for the tilt effet
|
//Toggle only when map is ready, for the tilt effet
|
||||||
this.toggleFeedPanel(bOpenFeedPanel);
|
this.toggleFeedPanel(bOpenFeedPanel);
|
||||||
|
|
||||||
//Default Basemap
|
|
||||||
this.baseMap = this.baseMaps.filter((asBM)=>asBM.default_map)[0].codename;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.map.on('idle', () => {
|
this.map.on('idle', () => {
|
||||||
@@ -282,6 +360,14 @@ export default {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
getGoogleMapsLink(asInfo) {
|
||||||
|
return $('<a>', {
|
||||||
|
href:'https://www.google.com/maps/place/'+asInfo.lat_dms+'+'+asInfo.lon_dms+'/@'+asInfo.latitude+','+asInfo.longitude+',10z',
|
||||||
|
title: this.spot.lang('see_on_google'),
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noreferrer noopener'
|
||||||
|
}).text(asInfo.lat_dms+' '+asInfo.lon_dms);
|
||||||
},
|
},
|
||||||
async getNextFeed() {
|
async getNextFeed() {
|
||||||
if(!this.feed.outOfData && !this.feed.loading) {
|
if(!this.feed.outOfData && !this.feed.loading) {
|
||||||
|
|||||||
50
src/components/projectPopup.vue
Normal file
50
src/components/projectPopup.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script>
|
||||||
|
import { options } from 'lightbox2';
|
||||||
|
import projectMapLink from './projectMapLink.vue';
|
||||||
|
import spotIcon from './spotIcon.vue';
|
||||||
|
import projectRelTime from './projectRelTime.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
spotIcon,
|
||||||
|
projectMapLink,
|
||||||
|
projectRelTime
|
||||||
|
},
|
||||||
|
//props: {
|
||||||
|
// options: Object,
|
||||||
|
//},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//inject: ['options', 'spot', 'project'],
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="info-window">
|
||||||
|
<h1>
|
||||||
|
<spotIcon :icon="'message'" :classes="'fa-lg'" :text="spot.lang('post_message')+' '+spot.lang('counter', options.displayed_id)" />
|
||||||
|
<span class="message-type">({{ options.type }})</span>
|
||||||
|
</h1>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<p class="coordinates">
|
||||||
|
<spotIcon :icon="'coords'" :classes="'fa-fw fa-lg'" :margin="true" />
|
||||||
|
<projectMapLink :options="options" />
|
||||||
|
</p>
|
||||||
|
<p class="time">
|
||||||
|
<spotIcon :icon="'time'" :classes="'fa-fw fa-lg'" :text="options.formatted_time" />
|
||||||
|
<span v-if="project.mode==spot.consts.modes.blog"> ({{ options.relative_time }})</span>
|
||||||
|
</p>
|
||||||
|
<p class="timezone" v-if="options.day_offset != '0'">
|
||||||
|
<spotIcon :icon="'timezone'" :classes="'fa-fw fa-lg'" :margin="true" />
|
||||||
|
<projectRelTime :localTime="options.formatted_time_local" :offset="options.day_offset" />
|
||||||
|
</p>
|
||||||
|
<p class="weather" v-if="options.weather_icon && options.weather_icon!='unknown'" :title="options.weather_cond==''?'':spot.lang(options.weather_cond)">
|
||||||
|
<spotIcon :icon="options.weather_icon" :classes="'fa-fw fa-lg'" :text="options.weather_temp+'°C'" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -10,7 +10,8 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
{{ localTime.substr(-5) }}
|
<span>{{ localTime.substring(-5) }}</span>
|
||||||
<sup v-if="offset != '0'" :title="offset+' '+spot.lang('unit_day')+' ('+localTime.substr(0, 5)+')'">{{ ' '+offset }}</sup>
|
<sup v-if="offset != '0'" :title="offset+' '+spot.lang('unit_day')+' ('+localTime.substring(0, 5)+')'">{{ ' '+offset }}</sup>
|
||||||
|
<span> {{ spot.lang('local_time', ' ').trim() }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
100
src/components/upload.vue
Normal file
100
src/components/upload.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script>
|
||||||
|
import SpotIcon from './spotIcon.vue';
|
||||||
|
import SpotButton from './spotButton.vue';
|
||||||
|
import "blueimp-file-upload/js/vendor/jquery.ui.widget.js";
|
||||||
|
import "blueimp-file-upload/js/jquery.iframe-transport.js";
|
||||||
|
import "blueimp-file-upload/js/jquery.fileupload.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'upload',
|
||||||
|
components: { SpotButton, SpotIcon },
|
||||||
|
inject: ['spot', 'projects', 'consts', 'user'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
project: this.projects[this.spot.vars('default_project_codename')],
|
||||||
|
files: [],
|
||||||
|
logs: [],
|
||||||
|
progress: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.spot.addPage('upload', {});
|
||||||
|
|
||||||
|
if(this.project.editable) {
|
||||||
|
$('#fileupload')
|
||||||
|
.fileupload({
|
||||||
|
dataType: 'json',
|
||||||
|
formData: {t: this.user.timezone},
|
||||||
|
acceptFileTypes: /(\.|\/)(gif|jpe?g|png|mov)$/i,
|
||||||
|
done: (e, asData) => {
|
||||||
|
$.each(asData.result.files, (iKey, oFile) => {
|
||||||
|
let bError = ('error' in oFile);
|
||||||
|
|
||||||
|
//Feedback
|
||||||
|
this.logs.push(bError?oFile.error:(this.spot.lang('upload_success', [oFile.name])));
|
||||||
|
|
||||||
|
//Comments
|
||||||
|
oFile.content = '';
|
||||||
|
if(!bError) this.files.push(oFile);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
progressall: (e, data) => {
|
||||||
|
this.progress = parseInt(data.loaded / data.total * 100, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else this.logs = [this.spot.lang('upload_mode_archived', [this.project.name])];
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addComment(oFile) {
|
||||||
|
this.spot.get2('add_comment', {id: oFile.id, content: oFile.content})
|
||||||
|
.then((asData) => {this.logs.push(this.spot.lang('media_comment_update', asData.filename));})
|
||||||
|
.catch((sMsgId) => {this.logs.push(this.spot.lang(sMsgId));});
|
||||||
|
},
|
||||||
|
addPosition() {
|
||||||
|
if(navigator.geolocation) {
|
||||||
|
this.logs.push('Determining position...');
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
this.logs.push('Sending position...');
|
||||||
|
this.spot.get2('add_position', {'latitude':position.coords.latitude, 'longitude':position.coords.longitude, 'timestamp':Math.round(position.timestamp / 1000)})
|
||||||
|
.then((asData) => {this.logs.push('Position sent');})
|
||||||
|
.catch((sMsgId) => {this.logs.push(self.lang(sMsgId));});
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.logs.push(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else this.logs.push('This browser does not support geolocation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div id="upload">
|
||||||
|
<a name="back" class="button" href="#project"><SpotIcon :icon="'back'" :text="spot.lang('nav_back')" /></a>
|
||||||
|
<h1>{{ spot.lang('upload_title') }}</h1>
|
||||||
|
<h2>{{ this.project.name }}</h2>
|
||||||
|
<div class="section" v-if="project.editable">
|
||||||
|
<input id="fileupload" type="file" name="files[]" :data-url="this.spot.getActionLink('upload')" multiple />
|
||||||
|
</div>
|
||||||
|
<div class="section progress" v-if="progress > 0">
|
||||||
|
<div class="bar" :style="{width:progress+'%'}"></div>
|
||||||
|
</div>
|
||||||
|
<div class="section comment" v-for="file in files">
|
||||||
|
<img class="thumb" :src="file.thumbnail" />
|
||||||
|
<div class="form">
|
||||||
|
<input class="content" name="content" type="text" v-model="file.content" />
|
||||||
|
<input class="id" name="id" type="hidden" :value="file.id" />
|
||||||
|
<SpotButton :classes="'save'" :icon="'save'" :text="spot.lang('save')" @click="addComment(file)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section location">
|
||||||
|
<SpotButton :icon="'message'" :text="spot.lang('new_position')" @click="addPosition()" />
|
||||||
|
</div>
|
||||||
|
<div class="section logs" v-if="logs.length > 0">
|
||||||
|
<p class="log" v-for="log in logs">{{ log }}.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -79,6 +79,9 @@ $fa-css-prefix: fa;
|
|||||||
.#{$fa-css-prefix}-config:before { content: fa-content($fa-var-cogs); }
|
.#{$fa-css-prefix}-config:before { content: fa-content($fa-var-cogs); }
|
||||||
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); }
|
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-cloud-upload); }
|
||||||
|
|
||||||
|
/* Upload */
|
||||||
|
.#{$fa-css-prefix}-save:before { content: fa-content($fa-var-floppy-disk); }
|
||||||
|
|
||||||
/* Feed */
|
/* Feed */
|
||||||
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
|
.#{$fa-css-prefix}-post:before { content: fa-content($fa-var-comment); }
|
||||||
.#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); }
|
.#{$fa-css-prefix}-media:before { content: fa-content($fa-var-photo-video); }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
/* Leaflet Popup */
|
/* Leaflet Popup */
|
||||||
.leaflet-popup-content {
|
.maplibregl-popup-content {
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
|
|||||||
@@ -1,36 +1,53 @@
|
|||||||
#upload {
|
#upload {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0 1rem 1rem 1rem;
|
||||||
|
border-bottom: 1px solid #EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
.bar {
|
.bar {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background: green;
|
background: green;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
margin-top: 1em;
|
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
form {
|
|
||||||
|
.form {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: calc(70% - 1em);
|
width: calc(70% - 2rem);
|
||||||
min-width: calc(100% - 100px - 1em);
|
min-width: calc(100% - 100px - 2rem);
|
||||||
margin-left: 1em;
|
padding: 1rem;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 100%;
|
width: calc(100% - 2rem);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 1px solid #333;
|
background: #EEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save {
|
.save {
|
||||||
margin-top: 1em;
|
margin-top: 1rem;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
p.log {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user