Initial changes testing and running vue3
This commit is contained in:
1 parent
68f71c9d6c
commit
9f37d3faf1
68 files changed
+23452
-12
No files matched your search
+4
-1
@@ -8,7 +8,10 @@ const ports = {
|
||||
}
|
||||
|
||||
//Allow user of @ in in require calls. Config in package.json
|
||||
require('module-alias/register')
|
||||
// Point module-alias at the repo root explicitly. The bare /register entry
|
||||
// locates package.json by walking up from its own directory, which lands
|
||||
// inside .pnpm/ under pnpm's symlinked store and fails to find it.
|
||||
require('module-alias')(require('path').join(__dirname, '..'))
|
||||
|
||||
//Auth helper, used for decoding users web token
|
||||
let Auth = require('@helpers/Auth')
|
||||
|
||||
@@ -1,22 +1,116 @@
|
||||
{ backupFile ? "" }:
|
||||
with import <nixpkgs> { };
|
||||
|
||||
let
|
||||
pythonPackages = python311Packages;
|
||||
in pkgs.mkShell rec {
|
||||
pkgs.mkShell {
|
||||
name = "Solid Scribe Env";
|
||||
|
||||
buildInputs = [
|
||||
# node for server
|
||||
buildInputs = [
|
||||
nodejs
|
||||
#nodePackages.npm
|
||||
nodemon
|
||||
pnpm
|
||||
pm2
|
||||
mariadb
|
||||
openssl
|
||||
];
|
||||
|
||||
postShellHook = ''
|
||||
echo("Welcome to solid scribe")
|
||||
shellHook = ''
|
||||
echo "Welcome to Solid Scribe"
|
||||
alias npm=pnpm
|
||||
'';
|
||||
|
||||
MYSQL_DATADIR="$PWD/.mysql/data"
|
||||
MYSQL_SOCKET="$PWD/.mysql/mysql.sock"
|
||||
MYSQL_PIDFILE="$PWD/.mysql/mysql.pid"
|
||||
|
||||
mkdir -p "$MYSQL_DATADIR"
|
||||
|
||||
# Initialize data directory on first run
|
||||
if [ ! -d "$MYSQL_DATADIR/mysql" ]; then
|
||||
echo "Initializing MySQL data directory (first run)..."
|
||||
mysql_install_db \
|
||||
--datadir="$MYSQL_DATADIR" \
|
||||
--skip-test-db \
|
||||
--auth-root-authentication-method=normal \
|
||||
2>/dev/null
|
||||
echo "Initialization complete."
|
||||
fi
|
||||
|
||||
# Start mysqld if not already running
|
||||
STARTED_MYSQL=0
|
||||
if ! mysqladmin --socket="$MYSQL_SOCKET" ping &>/dev/null 2>&1; then
|
||||
STARTED_MYSQL=1
|
||||
echo "Starting MySQL..."
|
||||
mysqld --no-defaults \
|
||||
--datadir="$MYSQL_DATADIR" \
|
||||
--socket="$MYSQL_SOCKET" \
|
||||
--pid-file="$MYSQL_PIDFILE" \
|
||||
--port=3306 \
|
||||
--bind-address=127.0.0.1 \
|
||||
--log-error="$PWD/.mysql/error.log" &
|
||||
|
||||
until mysqladmin --socket="$MYSQL_SOCKET" ping &>/dev/null 2>&1; do
|
||||
sleep 0.5
|
||||
done
|
||||
echo "MySQL ready."
|
||||
|
||||
mysql -u root --socket="$MYSQL_SOCKET" \
|
||||
-e "CREATE DATABASE IF NOT EXISTS application;" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Set app DB env vars — dotenv won't override these since they're already in process.env
|
||||
export DB_HOST=127.0.0.1
|
||||
export DB_USER=root
|
||||
export DB_PASS=
|
||||
|
||||
# Stable dev JWT key, persisted across shell sessions
|
||||
if [ -z "''${JSON_KEY:-}" ]; then
|
||||
if [ -s "$PWD/.mysql/jwt_key" ]; then
|
||||
export JSON_KEY=$(cat "$PWD/.mysql/jwt_key")
|
||||
else
|
||||
export JSON_KEY=$(openssl rand -hex 32)
|
||||
echo "$JSON_KEY" > "$PWD/.mysql/jwt_key"
|
||||
echo "Generated dev JSON_KEY (saved to .mysql/jwt_key)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore a SQL dump — handles both plain .sql and .gz files
|
||||
restore-db() {
|
||||
local file="''${1:-}"
|
||||
if [ -z "$file" ]; then
|
||||
echo "Usage: restore-db <backup.sql[.gz]>"
|
||||
return 1
|
||||
fi
|
||||
echo "Restoring database from $file ..."
|
||||
if [[ "$file" == *.gz ]]; then
|
||||
gunzip -c "$file" | mysql -u root --socket="$MYSQL_SOCKET"
|
||||
else
|
||||
mysql -u root --socket="$MYSQL_SOCKET" < "$file"
|
||||
fi
|
||||
# The dump is --all-databases, so it includes the `mysql` schema and
|
||||
# overwrites local root with production's credentials. In-memory grants
|
||||
# are still the old ones until FLUSH, so fix root now while we can
|
||||
# still authenticate.
|
||||
mysql -u root --socket="$MYSQL_SOCKET" -e "
|
||||
ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD(''');
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;"
|
||||
|
||||
echo "Restore complete."
|
||||
}
|
||||
export -f restore-db
|
||||
|
||||
# Auto-restore if --argstr backupFile was passed to nix-shell
|
||||
if [ -n "${backupFile}" ]; then
|
||||
restore-db "${backupFile}"
|
||||
fi
|
||||
|
||||
if [ "$STARTED_MYSQL" = "1" ]; then
|
||||
trap "echo 'Stopping MySQL...'; mysqladmin -u root --socket=$MYSQL_SOCKET shutdown 2>/dev/null" EXIT
|
||||
else
|
||||
echo "MySQL already running (started by another shell) -- leaving it up on exit."
|
||||
fi
|
||||
|
||||
# Do not start the app here.
|
||||
# This same shell.nix is used for hosting the app on dev
|
||||
# use the startDevServerAndClient.sh script
|
||||
'';
|
||||
}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Local dev: API server + v2client (Vue 3 / Vite).
|
||||
# Run from inside nix-shell, which provides node/pnpm/pm2/mariadb,
|
||||
# starts the local MySQL, and exports DB_* and JSON_KEY.
|
||||
#
|
||||
# Unlike startDevServerAndClient.sh (remote dev box, /home/mab/ss, Vue 2
|
||||
# client) this is path-relative and points at v2client-vite-vue3.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
if [ -z "${JSON_KEY:-}" ]; then
|
||||
echo "JSON_KEY is not set -- run this from inside nix-shell." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! mysqladmin --socket="$REPO/.mysql/mysql.sock" ping &>/dev/null; then
|
||||
echo "Local MySQL is not running -- run this from inside nix-shell." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo '::--:: Starting API server (:3000) + socket.io (:3001) under pm2...'
|
||||
cd "$REPO/server"
|
||||
# `env -u name`: nix's mkShell exports its `name` attr ("Solid Scribe Env")
|
||||
# as $name, and pm2 uses $name to override the app name from ecosystem.config.js.
|
||||
pm2 delete NoteServer &>/dev/null || true
|
||||
pm2 flush &>/dev/null || true
|
||||
env -u name pm2 start ecosystem.config.js
|
||||
cd "$REPO"
|
||||
|
||||
echo '::--:: Starting v2client dev server -> http://localhost:5173'
|
||||
cd "$REPO/v2client-vite-vue3"
|
||||
[ -d node_modules ] || pnpm install
|
||||
screen -S "V2ClientScreen" -X quit &>/dev/null || true
|
||||
screen -dmS "V2ClientScreen" bash -c "cd '$REPO/v2client-vite-vue3'; pnpm dev"
|
||||
|
||||
cat <<MSG
|
||||
|
||||
::--:: Up.
|
||||
client http://localhost:5173 (proxies /api -> :3000, /socket -> :3001)
|
||||
api http://localhost:3000
|
||||
|
||||
logs pm2 logs NoteServer
|
||||
screen -r V2ClientScreen (detach: ctrl-a d)
|
||||
stop pm2 delete NoteServer; screen -S V2ClientScreen -X quit
|
||||
MSG
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
# # #!/bin/bash
|
||||
#Syncs local data with sandbox
|
||||
|
||||
# v - Verbose
|
||||
@@ -11,5 +11,6 @@
|
||||
# e - Send Files via SSH
|
||||
# z - Compress for speed
|
||||
# h - Human Readable file sizes
|
||||
# p - port
|
||||
|
||||
rsync -e 'ssh' --exclude-from=dontSync.txt -havzC --update mab@marvin.local:/home/mab/ss/ .
|
||||
rsync -e 'ssh -p 13326' --exclude-from=dontSync.txt -havz --update mab@blarvin.local:/home/mab/ss/ .
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Solid Scribe</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1766
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "v2client-vite-vue3",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"axios": "^1.1.3",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fomantic-ui-css": "^2.9.0",
|
||||
"mitt": "^3.0.1",
|
||||
"socket.io-client": "^2.5.0",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
Generated
+1666
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
|
||||
|
||||
<!-- <div class="ui container" v-if="showFakeSite">
|
||||
<div class="ui basic very padded segment">
|
||||
<div class="ui inverted red segment">
|
||||
<h1>WARNING - False site detected</h1>
|
||||
<h2>The Domain for this website is not correct.</h2>
|
||||
<h2>Only trust <a class="ui button" href="https://www.solidscribe.com">https://www.solidscribe.com</a></h2>
|
||||
<h2>Do not any enter any personal information into this website.</h2>
|
||||
<h2>You will be redirected to the correct domain in {{redirectSeconds}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="auth-block" v-if="requireAuth">
|
||||
<div class="ui raised inverted segment">
|
||||
<div class="ui centered header">
|
||||
Authentication Required
|
||||
</div>
|
||||
<div class="ui small inverted centered header" v-if="$store.getters.getUsername">
|
||||
<i class="green user outline icon"></i>
|
||||
{{ $store.getters.getUsername }}
|
||||
</div>
|
||||
|
||||
<div class="ui large form">
|
||||
<div class="field">
|
||||
<div class="ui small inverted header">Password</div>
|
||||
<div class="ui input">
|
||||
<input type="password" v-model="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui small inverted header">One Time Password</div>
|
||||
<div class="ui input">
|
||||
<input type="password" v-model="otp" placeholder="One Time Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui fluid inverted black button">
|
||||
<i class="unlock icon"></i>
|
||||
Submit</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<global-site-menu v-if="!showFakeSite" />
|
||||
|
||||
<router-view v-if="!showFakeSite" />
|
||||
|
||||
<global-notification />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
|
||||
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
'global-site-menu': GlobalSiteMenu,
|
||||
'global-notification': GlobalNotification,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
showFakeSite:false, //Incorrect domain detection
|
||||
redirectSeconds: 15,
|
||||
fetchingInProgress: false, //Prevent start getting token while fetch is in progress
|
||||
blockUntilNextRequest: false, //If token was just renewed, don't fetch more until next request
|
||||
|
||||
requireAuth: false,
|
||||
password: '',
|
||||
otp: '',
|
||||
}
|
||||
},
|
||||
|
||||
//Axios response interceptor
|
||||
// - Gets new session tokens from server and uses them in app
|
||||
beforeCreate: function(){
|
||||
|
||||
const root = 'https://www.solidscribe.com'
|
||||
// const root = 'https://192.168.1.164'
|
||||
const manifest = {
|
||||
"theme_color":"#000",
|
||||
"background_color": "#000",
|
||||
"description": "Take Notes",
|
||||
"display_override": ["fullscreen", "minimal-ui"],
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": root+"/api/static/assets/logo.png",
|
||||
"sizes": "495x495",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": root+"/api/static/assets/maskable_icon.png",
|
||||
"sizes": "826x826",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"screenshots" : [
|
||||
{
|
||||
"src": root+"/api/static/assets/lightmode.png",
|
||||
"sizes": "1000x1080",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": root+"/api/static/assets/darkmode.png",
|
||||
"sizes": "1000x1081",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"shortcuts" : [
|
||||
{
|
||||
"name": "Files & Links",
|
||||
"url": " /attachments",
|
||||
"description": "All Note files and links",
|
||||
"icons": [{
|
||||
"src": root+"/api/static/assets/icon1.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "Metric Track",
|
||||
"url": "/metrictrack",
|
||||
"description": "Metric Tracking",
|
||||
"icons": [{
|
||||
"src": root+"/api/static/assets/icon2.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}]
|
||||
}
|
||||
],
|
||||
"name": "Solid Scribe",
|
||||
"short_name": "Solid Scribe",
|
||||
"author": "Max",
|
||||
"scope": root,
|
||||
"start_url": window.location.href
|
||||
}
|
||||
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'manifest'
|
||||
link.href = `data:application/json;base64,${btoa(JSON.stringify(manifest))}`
|
||||
document.head.appendChild(link)
|
||||
|
||||
//Before all requests going out
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
|
||||
//Enable token fetching after another request is made
|
||||
if(this.blockUntilNextRequest){
|
||||
this.fetchingInProgress = false
|
||||
this.blockUntilNextRequest = false
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Add a response interceptor, token can be renewed on every response
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
|
||||
if(typeof response.headers.remaininguses !== 'undefined'){
|
||||
|
||||
// console.log(response.headers.remaininguses)
|
||||
//Look at remaining uses of token, if its less than five, request a new one
|
||||
if(response.headers.remaininguses < 15 && !this.fetchingInProgress && !this.blockUntilNextRequest){
|
||||
this.fetchingInProgress = true
|
||||
const currentToken = localStorage.getItem('loginToken')
|
||||
this.$io.emit('renew_session_token', currentToken)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
|
||||
//Catch all authorization errors, log user out if we encounter one
|
||||
if(error.response && error.response.status == 401){
|
||||
|
||||
this.$router.push('/')
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$bus.$emit('notification', 'Error: You have been logged out.')
|
||||
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
//Puts token into state on page load
|
||||
let token = localStorage.getItem('loginToken')
|
||||
let username = localStorage.getItem('username')
|
||||
|
||||
//
|
||||
if(token && token.length > 0){
|
||||
|
||||
//setup username display
|
||||
this.$store.commit('setUsername', username)
|
||||
|
||||
//Set session token on every request if set
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
|
||||
//Setup websockets into vue instance
|
||||
const socket = this.$io
|
||||
socket.on('connect', () => {
|
||||
|
||||
//Put user into personal event room for live note updates, etc
|
||||
this.$io.emit('user_connect', token)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//Detect if user is on a mobile browser and set a flag in store
|
||||
this.$store.commit('detectIsUserOnMobile')
|
||||
|
||||
//Set Main theme color
|
||||
const accentColor = localStorage.getItem('main-accent')
|
||||
if(accentColor){
|
||||
document.documentElement.style.setProperty('--main-accent', accentColor)
|
||||
}
|
||||
|
||||
//Set color theme based on local storage
|
||||
const themeNumber = localStorage.getItem('nightMode')
|
||||
if(themeNumber != null){
|
||||
this.$store.commit('toggleNightMode', themeNumber)
|
||||
}
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
// Vite does not shim node's `process` into the browser bundle the way
|
||||
// webpack/vue-cli did; import.meta.env.DEV is the equivalent.
|
||||
const isDev = import.meta.env.DEV
|
||||
// if(window.location.hostname.toLowerCase().replace('www.','') != "solidscribe.com" && !isDev){
|
||||
// this.showFakeSite = true
|
||||
// setInterval(() => {
|
||||
// this.redirectSeconds--
|
||||
// if(this.redirectSeconds == 0){
|
||||
// window.location.href = 'https://www.solidscribe.com'
|
||||
// }
|
||||
// }, 1000)
|
||||
// }
|
||||
|
||||
//Update totals for entire app on event
|
||||
this.$io.on('update_counts', () => {
|
||||
console.log('Got event, update totals')
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
})
|
||||
|
||||
this.$io.on('recievend_new_token', newToken => {
|
||||
|
||||
// console.log('Got a new token')
|
||||
|
||||
axios.defaults.headers.common['authorizationtoken'] = newToken
|
||||
localStorage.setItem('loginToken', newToken)
|
||||
|
||||
//Disable getting new tokens until next request
|
||||
this.blockUntilNextRequest = true
|
||||
})
|
||||
|
||||
//Track users active sessions
|
||||
this.$io.on('update_active_user_count', countData => {
|
||||
this.$store.commit('setActiveSessions', countData)
|
||||
})
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
//Map logged in from state
|
||||
return this.$store.getters.getLoggedIn
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loginGateway() {
|
||||
if(!this.loggedIn){
|
||||
console.log('This user is not logged in')
|
||||
this.$router.push({'path':'/login'})
|
||||
return
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
|
||||
this.$router.push('/')
|
||||
axios.post('/api/user/logout')
|
||||
|
||||
setTimeout(() => {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
}, 200)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
import mitt from 'mitt'
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
// Preserves the Vue 2 $on/$emit/$off API used throughout the codebase
|
||||
const EventBus = {
|
||||
$on: (...args) => emitter.on(...args),
|
||||
$off: (...args) => emitter.off(...args),
|
||||
$emit: (...args) => emitter.emit(...args),
|
||||
}
|
||||
|
||||
export default EventBus
|
||||
@@ -0,0 +1,68 @@
|
||||
const helpers = {}
|
||||
|
||||
helpers.timeAgo = (time) => {
|
||||
|
||||
if(time == null){
|
||||
time = Math.round(time/1000)
|
||||
}
|
||||
|
||||
if(time.toString().length >= 13){
|
||||
time = Math.round(time/1000)
|
||||
}
|
||||
|
||||
const time_formats = [
|
||||
[ 60, 'seconds', 1 ],
|
||||
[ 120, '1 minute ago', '1 minute from now' ],
|
||||
[ 3600, 'minutes', 60 ],
|
||||
[ 7200, '1 hour ago', '1 hour from now' ],
|
||||
[ 86400, 'hours', 3600 ],
|
||||
[ 172800, 'yesterday', 'tomorrow' ],
|
||||
[ 604800, 'days', 86400 ],
|
||||
[ 1209600, 'last week', 'next week' ],
|
||||
[ 2419200, 'weeks', 604800 ],
|
||||
[ 4838400, 'last month', 'next month' ],
|
||||
[ 29030400, 'months', 2419200 ],
|
||||
[ 58060800, 'last year', 'next year' ],
|
||||
[ 2903040000, 'years', 29030400 ],
|
||||
[ 5806080000, 'last century', 'next century' ],
|
||||
[ 58060800000, 'centuries', 2903040000 ]
|
||||
]
|
||||
|
||||
//How many seconds ago was input event time?
|
||||
let seconds = Math.round((+new Date)/1000) - time
|
||||
let token = 'ago'
|
||||
let list_choice = 1
|
||||
|
||||
if (seconds == 0) {
|
||||
return 'Just now'
|
||||
}
|
||||
|
||||
if (seconds < 0) {
|
||||
seconds = Math.abs(seconds)
|
||||
token = 'from now'
|
||||
list_choice = 2
|
||||
}
|
||||
|
||||
|
||||
let i = 0
|
||||
let format = null
|
||||
while (format = time_formats[i++]){
|
||||
if (seconds < format[0]) {
|
||||
if (typeof format[2] == 'string') {
|
||||
return format[list_choice]
|
||||
} else {
|
||||
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return time
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
install(app) {
|
||||
app.config.globalProperties.$helpers = helpers
|
||||
}
|
||||
}
|
||||
|
||||
export default plugin
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,112 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.numtainer {
|
||||
height: 1.1em;
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.start-high {
|
||||
color: #4dc86a;
|
||||
animation: startHigh 0.5s forwards;
|
||||
}
|
||||
|
||||
.start-low {
|
||||
color: #4dc86a;
|
||||
animation: startLow 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes startLow {
|
||||
0% {
|
||||
margin-top: 0;
|
||||
}
|
||||
100% {
|
||||
margin-top: -1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes startHigh {
|
||||
0% {
|
||||
margin-top: -1.2em;
|
||||
}
|
||||
100% {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="numtainer">
|
||||
|
||||
<div v-if="animateUp">
|
||||
<div class="start-high">{{ newNumber }}</div>
|
||||
<div>{{ oldNumber }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="animateDown">
|
||||
<div class="start-low">{{ oldNumber }}</div>
|
||||
<div>{{ newNumber }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="totals">{{ totals[numberId] }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'AnimatedCounterComponent',
|
||||
props: [ 'numberId' ],
|
||||
data () {
|
||||
return {
|
||||
oldNumber: 100,
|
||||
newNumber: 99,
|
||||
animateUp: false,
|
||||
animateDown: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['totals'])
|
||||
},
|
||||
watch:{
|
||||
totals(newVal, oldVal){
|
||||
if(oldVal && newVal && newVal[this.numberId] != oldVal[this.numberId]){
|
||||
|
||||
// console.log('New number ', newVal[this.numberId])
|
||||
|
||||
this.oldNumber = oldVal[this.numberId]
|
||||
this.newNumber = newVal[this.numberId]
|
||||
|
||||
if(this.oldNumber > this.newNumber){
|
||||
this.animateDown = true
|
||||
} else {
|
||||
this.animateUp = true
|
||||
}
|
||||
|
||||
setTimeout( () => {
|
||||
this.animateUp = false
|
||||
this.animateDown = false
|
||||
}, 550)
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
},
|
||||
methods: {
|
||||
onFileClick(file){
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,245 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.attachment-display-card {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
border-radius: 4px;
|
||||
margin: 0 0 15px;
|
||||
max-height: 10000px;
|
||||
}
|
||||
|
||||
.attachment-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 75px;
|
||||
}
|
||||
.image-placeholder:after {
|
||||
content: 'No Image';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background:
|
||||
green;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
/*height: 100%;*/
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
font-size: 1.2em;
|
||||
line-height: 1.3em;
|
||||
/*margin: 0 0 10px;*/
|
||||
padding: 10px 0 10px;
|
||||
|
||||
color: var(--text_color);
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
/*transition: height 0.4s ease; This breaks the resize */
|
||||
}
|
||||
.link {
|
||||
font-size: 1.4em;
|
||||
margin: 20px 0 20px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow:hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
.flip-out {
|
||||
animation: flip-out-hor-top 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
max-height: 0;
|
||||
}
|
||||
@keyframes flip-out-hor-top {
|
||||
0% {
|
||||
transform: rotateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(70deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="attachment-display-card" :class="{ 'flip-out':!unfolded }" v-if="visible">
|
||||
<div class="ui stackable grid">
|
||||
|
||||
<!-- image and text -->
|
||||
<div class="six wide center aligned middle aligned column">
|
||||
<a :href="linkUrl" target="_blank" >
|
||||
<img v-if="item.file_location" class="attachment-image"
|
||||
onerror="
|
||||
this.onerror=null;
|
||||
this.src='/api/static/assets/marketing/void.svg';
|
||||
this.classList.add('image-placeholder');
|
||||
this.insertAdjacentText('afterend', 'Image not found');
|
||||
"
|
||||
:src="`/api/static/thumb_${item.file_location}`">
|
||||
<span v-else>
|
||||
<img class="image-placeholder" loading="lazy" src="/api/static/assets/marketing/void.svg">
|
||||
No Image
|
||||
</span>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="ten wide column">
|
||||
<textarea ref="edit" class="text" v-on:blur="saveIt()" v-on:keyup="checkKeyup"
|
||||
v-model="text"
|
||||
v-on:focus="showSave = true"
|
||||
></textarea>
|
||||
|
||||
<div v-if="showSave" class="ui green button">Save</div>
|
||||
|
||||
<!-- link -->
|
||||
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
<div v-if="!item.note_id" class="ui small compact basic disabled button">
|
||||
<i class="angle double up icon"></i>
|
||||
Pushed from Web
|
||||
</div>
|
||||
|
||||
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
|
||||
:class="{ 'disabled':this.searchParams.noteId }">
|
||||
<i class="folder open outline icon"></i>
|
||||
Note Files
|
||||
</div>
|
||||
<div class="ui small compact basic icon button" v-on:click="deleteAttachment">
|
||||
<i v-if="!working" class="trash alternate outline icon"></i>
|
||||
<i v-if="working" class="purple spinner loading icon icon"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
|
||||
props: [ 'item', 'searchParams' ],
|
||||
data: function(){
|
||||
return {
|
||||
text: '',
|
||||
type: null,
|
||||
|
||||
linkText: 'Link',
|
||||
linkUrl:null,
|
||||
|
||||
unfolded:true,
|
||||
visible: true,
|
||||
showSave: false,
|
||||
|
||||
working: false,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
mounted: function(){
|
||||
this.text = this.item.text
|
||||
this.type = this.item.attachment_type
|
||||
|
||||
|
||||
//1 = URL, 2 = image, >= 3 files
|
||||
if(this.type == 1 && this.item.url != null){
|
||||
this.linkText = this.item.url
|
||||
this.linkUrl = this.item.url
|
||||
}
|
||||
if(this.type == 2){
|
||||
this.linkText = 'Download'
|
||||
this.linkUrl = `/api/static/${this.item.file_location}`
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.checkKeyup()
|
||||
})
|
||||
},
|
||||
updated: function(){
|
||||
this.checkKeyup()
|
||||
},
|
||||
methods: {
|
||||
checkKeyup(){
|
||||
let elm = this.$refs.edit
|
||||
if(elm){
|
||||
elm.style.height = '0'
|
||||
elm.style.height = elm.scrollHeight +'px'
|
||||
}
|
||||
},
|
||||
openNote(){
|
||||
const noteId = this.item.note_id
|
||||
this.$router.push('/notes/open/'+noteId)
|
||||
},
|
||||
openEditAttachments(){
|
||||
const noteId = this.item.note_id
|
||||
this.$router.push('/attachments/note/'+noteId)
|
||||
},
|
||||
deleteAttachment(){
|
||||
|
||||
//No double clicks
|
||||
if(this.working){ return }
|
||||
|
||||
this.working = true
|
||||
axios.post('/api/attachment/delete', {'attachmentId':this.item.id})
|
||||
.then( ({data}) => {
|
||||
if(data){
|
||||
this.unfolded = false
|
||||
setTimeout( () => {
|
||||
this.visible = false
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
}, 600)
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to delete attachment') })
|
||||
},
|
||||
saveIt(){
|
||||
|
||||
this.showSave = false
|
||||
|
||||
//Don't save text if it didn'th change
|
||||
if(this.item.text == this.text){
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
'attachmentId': this.item.id,
|
||||
'updatedText': this.text,
|
||||
'noteId': this.item.note_id
|
||||
}
|
||||
|
||||
//Save it, and don't think about it.
|
||||
axios.post('/api/attachment/update', data)
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Save') })
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="ui sixteen wide column">
|
||||
<div class="ui dividing header">
|
||||
Reset Background Color and Icon
|
||||
</div>
|
||||
<div class="ui labeled basic icon button" v-on:click="clearStyles">
|
||||
<i class="refresh icon"></i>
|
||||
Reset
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column rounded" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||
<div class="ui dividing header" :style="{ 'color':allStyles['noteText']}">
|
||||
<i class="fill drip icon"></i>
|
||||
Background Color
|
||||
</div>
|
||||
<div v-for="color in colors"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chosenColor(color)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui dividing header">
|
||||
<span v-if="allStyles.noteIcon" >
|
||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</span>
|
||||
Note Icon
|
||||
</div>
|
||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
||||
<i :class="`large ${icon} icon`"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui dividing header">
|
||||
<span v-if="allStyles.noteIcon" >
|
||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</span>
|
||||
Icon Color
|
||||
</div>
|
||||
<div v-for="color in getReducedColors()"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chooseIconColor(color)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ColorPicker',
|
||||
props: [ 'location', 'styleObject' ],
|
||||
data () {
|
||||
return {
|
||||
allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
|
||||
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
|
||||
colors: [null,
|
||||
'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'],
|
||||
icons: ['cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down']
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
styleObject: function(updatedStyles){
|
||||
this.allStyles = updatedStyles
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
this.allStyles = this.styleObject
|
||||
},
|
||||
methods: {
|
||||
getReducedColors(){
|
||||
|
||||
let reduced = []
|
||||
|
||||
this.colors.forEach((color,i) => {
|
||||
if(i < 20 || i > 69){
|
||||
reduced.push(color)
|
||||
}
|
||||
})
|
||||
|
||||
return reduced
|
||||
},
|
||||
clearStyles(){
|
||||
this.$emit('changeColor', this.blankStyle)
|
||||
},
|
||||
closeThisBitch(){
|
||||
this.$emit('close')
|
||||
},
|
||||
chosenColor(inColor){
|
||||
|
||||
//Set not background to color that was chosen
|
||||
this.allStyles.noteBackground = inColor
|
||||
|
||||
if(inColor == null){
|
||||
this.$emit('changeColor', this.allStyles)
|
||||
return
|
||||
}
|
||||
|
||||
//Automatically select note text color
|
||||
|
||||
//If you are using hex colors, use this
|
||||
// Convert hex color to RGB - http://gist.github.com/983661
|
||||
// let color = +("0x" + inColor.slice(1).replace(inColor.length < 5 && /./g, '$&$&'));
|
||||
// let r = color >> 16;
|
||||
// let g = color >> 8 & 255;
|
||||
// let b = color & 255;
|
||||
|
||||
const set = inColor.match(/\d+/g)
|
||||
const r = parseInt(set[0])
|
||||
const g = parseInt(set[1])
|
||||
const b = parseInt(set[2])
|
||||
|
||||
//Convert RGB to HSP
|
||||
const hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
|
||||
|
||||
//If it has a BG color, default to sold black text
|
||||
this.allStyles.noteText = '#000'
|
||||
if(hsp < 127.5){
|
||||
this.allStyles.noteText = '#FFF' //If color is dark, we need brighter text
|
||||
}
|
||||
|
||||
this.$emit('changeColor', this.allStyles)
|
||||
},
|
||||
chosenIcon(inIcon){
|
||||
|
||||
this.allStyles.noteIcon = inIcon
|
||||
|
||||
this.$emit('changeColor', this.allStyles)
|
||||
},
|
||||
chooseIconColor(inColor){
|
||||
|
||||
this.allStyles.iconColor = inColor
|
||||
|
||||
this.$emit('changeColor', this.allStyles)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.icon-button, .color-button {
|
||||
height: 40px;
|
||||
width: calc(15% - 1px);
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
border: 1px solid grey;
|
||||
text-align: center;
|
||||
padding: 5px 0px 0 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 1px 3px 0px #3e3e3e;
|
||||
margin: 2px 2px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.color-button {
|
||||
width: calc(10% - 4px);
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<span v-on:mouseover="open = true" v-on:mouseleave="open = false" class="relative clickable filter-header" :class="{'filter-active':open}">
|
||||
{{displayString()}} <i class="grey caret down icon"></i>
|
||||
<span class="filter-menu" v-if="open">
|
||||
<span v-for="(filter,label) in filterOptions" class="filter-option" v-on:click="updateFilter(filter, label)">{{label}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FastFilters',
|
||||
data () {
|
||||
return {
|
||||
click: 0,
|
||||
open: false,
|
||||
filter: {},
|
||||
orderString: 'Order by Last Edited',
|
||||
filterOptions:{
|
||||
'Order by Last Edited' :'lastEdited',
|
||||
'Order by Last Opened' :'lastOpened',
|
||||
'Order by Last Created' :'lastCreated',
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
// this.$bus.$on('reset_fast_filters', () => {
|
||||
// this.orderString = 'Order by Last Edited'
|
||||
// })
|
||||
},
|
||||
methods:{
|
||||
displayString(){
|
||||
return this.orderString.replace('Order by','').replace('Only Show','')
|
||||
},
|
||||
updateFilter(option, label){
|
||||
|
||||
this.orderString = label
|
||||
this.open = false
|
||||
|
||||
let filter = {}
|
||||
filter[option] = 1
|
||||
|
||||
// this.$bus.$emit('update_fast_filters', filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.filter-header {
|
||||
width: 200px;
|
||||
padding: 0 0 0 10px;
|
||||
border: 1px solid rgba(0,0,0,0);
|
||||
border-bottom: none;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
margin: 0 0 0 -11px;
|
||||
float: right;
|
||||
}
|
||||
.filter-menu {
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
|
||||
|
||||
border: 1px solid;
|
||||
border-top: none;
|
||||
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 200px;
|
||||
left: -1px;
|
||||
z-index: 10;
|
||||
padding-top: 10px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.filter-active {
|
||||
border: 1px solid;
|
||||
border-bottom: none;
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.filter-option {
|
||||
font-size: 17px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 0 0 5px 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<style type="text/css" scoped>
|
||||
.hidden-up {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: -50000px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<form data-tooltip="Upload File" data-inverted>
|
||||
<label :for="`upfile-${noteId}`" class="clickable">
|
||||
<!-- <nm-button icon="upload" :text="uploadStatusText"/> -->
|
||||
<i class="file upload icon"></i>
|
||||
</label>
|
||||
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
|
||||
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
export default {
|
||||
name: 'FileUploadButton',
|
||||
props: [ 'noteId' ],
|
||||
components: {
|
||||
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
file: null,
|
||||
uploadStatusText: 'Upload',
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
// console.log(this.noteId)
|
||||
},
|
||||
methods: {
|
||||
uploadFileToServer() {
|
||||
let formData = new FormData();
|
||||
formData.append('file', this.file);
|
||||
formData.append('noteId', this.noteId)
|
||||
console.log('>> formData >> ', formData);
|
||||
|
||||
// You should have a server side REST API
|
||||
axios.post('/api/attachment/upload',
|
||||
formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress: ( progressEvent ) => {
|
||||
this.uploadStatusText = parseInt(
|
||||
Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) )
|
||||
}
|
||||
}
|
||||
).then(results => {
|
||||
this.uploadStatusText = 'Working'
|
||||
this.file = null
|
||||
|
||||
// console.log('File upload results')
|
||||
// console.log(results.data)
|
||||
|
||||
const name = results.data.fileName
|
||||
const location = results.data.goodFileName
|
||||
|
||||
this.$bus.$emit('notification', 'Processing Upload')
|
||||
|
||||
if(name && location){
|
||||
|
||||
this.uploadStatusText = 'Upload File'
|
||||
|
||||
const imageCode = `<img alt="image" src="/api/static/thumb_${location}">`
|
||||
|
||||
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
}
|
||||
})
|
||||
.catch(results => {
|
||||
this.uploadStatusText = 0
|
||||
this.$bus.$emit('notification', 'Failed to Upload')
|
||||
})
|
||||
},
|
||||
handleFileUpload() {
|
||||
//Grab file and push note id to into data
|
||||
this.file = this.$refs.file.files[0]
|
||||
|
||||
console.log('>>>> 1st element in files array >>>> ')
|
||||
console.log(this.file)
|
||||
|
||||
if(this.file){
|
||||
this.uploadFileToServer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,171 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.popup-body {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
min-height: 50px;
|
||||
min-width: 200px;
|
||||
max-width: calc(100% - 30px);
|
||||
z-index: 1020;
|
||||
|
||||
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
|
||||
border-radius: 4px;
|
||||
|
||||
color: white;
|
||||
background-color: var(--main-accent);
|
||||
}
|
||||
.popup-row {
|
||||
padding: 1em 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.popup-row > p {
|
||||
/*width: calc(100% - 50px);*/
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px 0;
|
||||
font-size: 1.25em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.popup-row + .popup-row {
|
||||
border-top: 1px solid #FFF;
|
||||
}
|
||||
|
||||
.slide-in-bottom {
|
||||
animation: slide-in-bottom 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
@keyframes slide-in-bottom {
|
||||
0% {
|
||||
transform: translateY(-1000px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.color-fade {
|
||||
animation: color-fade-animation 0.7s ease-out both;
|
||||
}
|
||||
@keyframes color-fade-animation {
|
||||
0% {
|
||||
color: black;
|
||||
background-color: black;
|
||||
}
|
||||
50% {
|
||||
color: black;
|
||||
background-color: black;
|
||||
}
|
||||
100% {
|
||||
color: white;
|
||||
background-color: #21ba45;
|
||||
}
|
||||
}
|
||||
|
||||
.meter {
|
||||
height: 2px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.meter span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: white;
|
||||
animation: progressBar 3s linear;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
.time-display {
|
||||
display: inline-block;
|
||||
width: calc(100% - 25px);
|
||||
/*text-align: right;*/
|
||||
color: white;
|
||||
font-size: 0.7em;
|
||||
margin: 0 0 0 25px;
|
||||
}
|
||||
.text-display {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes progressBar {
|
||||
0% { width: 0; }
|
||||
100% { width: 100%; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="popup-body slide-in-bottom" v-on:click="dismiss" v-if="notifications.length > 0">
|
||||
<div class="popup-row" v-for="item in notifications">
|
||||
<div class="meter">
|
||||
<span><span class="progress"></span></span>
|
||||
</div>
|
||||
<p class="text-display">
|
||||
<i class="small info circle icon"></i>
|
||||
{{ item.text }}
|
||||
<span class="time-display">{{ item.time }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'GlobalNotificationComponent',
|
||||
data () {
|
||||
return {
|
||||
notifications: [],
|
||||
totalTimeout: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.$bus.$on('notification', notificationText => {
|
||||
this.displayNotification(notificationText)
|
||||
})
|
||||
},
|
||||
mounted(){
|
||||
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed Login did not succeed')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed your life is exposed to the internet')
|
||||
// this.$bus.$emit('notification', 'Password Protection Removed everyone can see everything')
|
||||
|
||||
},
|
||||
methods: {
|
||||
displayNotification(notificationText){
|
||||
|
||||
const date = new Date()
|
||||
const time = date.toLocaleTimeString()
|
||||
|
||||
const notification = {
|
||||
text: notificationText,
|
||||
time: time
|
||||
}
|
||||
|
||||
this.notifications.unshift(notification)
|
||||
clearTimeout(this.totalTimeout)
|
||||
this.totalTimeout = setTimeout(() => {
|
||||
this.dismiss()
|
||||
}, 3000)
|
||||
},
|
||||
dismiss(){
|
||||
this.notifications = []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,541 @@
|
||||
<style scoped>
|
||||
.slotholder {
|
||||
height: 100vh;
|
||||
width: 180px;
|
||||
display: block;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
.global-menu {
|
||||
width: 180px;
|
||||
/* background: #221f2b; */
|
||||
background: #221f2b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 900;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.menu-logo-display {
|
||||
width: 27px;
|
||||
margin: 5px 0 0 55px;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: #fff;
|
||||
padding: 9px 10px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 1.1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.menu-item i.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.sub {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.menu-section {}
|
||||
.menu-section + .menu-section {
|
||||
/* border-top: 1px solid #534c68; */
|
||||
border-top: 1px solid #534c68e3;
|
||||
}
|
||||
.menu-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu-button:hover {
|
||||
background-color: #534c68;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.router-link-active {
|
||||
background-color: #534c68;
|
||||
}
|
||||
|
||||
.shade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
z-index: 899;
|
||||
cursor: pointer;
|
||||
}
|
||||
.top-menu-bar {
|
||||
/*color: var(--text_color);*/
|
||||
/*width: 100%;*/
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: var(--small_element_bg_color);
|
||||
/*padding: 5px 1rem 5px;*/
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100vw;
|
||||
border-top: 1px solid var(--dark_border_color);
|
||||
display: flex;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.place-holder {
|
||||
width: 100%;
|
||||
/*height: 40px;*/
|
||||
height: 0;
|
||||
}
|
||||
.logo-display {
|
||||
width: 27px;
|
||||
height: auto;
|
||||
}
|
||||
.version-display {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -20px;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
color: #8c80ae;
|
||||
cursor: pointer;
|
||||
background-color: var(--menu-background);
|
||||
}
|
||||
|
||||
.mobile-button {
|
||||
padding: 5px 0 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 0.6em;
|
||||
color: var(--menu-text);
|
||||
text-align: center;
|
||||
flex-basis: 100%;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
.mobile-button + .mobile-button {
|
||||
border-left: 1px solid var(--dark_border_color);
|
||||
}
|
||||
.mobile-button i {
|
||||
font-size: 2em;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.mobile-button svg {
|
||||
margin: 0 46% 0;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
}
|
||||
.mobile-button:active, .mobile-button:focus, .mobile-button:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.mobile-button.active {
|
||||
background-color: transparent;
|
||||
}
|
||||
.single-line-text {
|
||||
width: calc(100%);
|
||||
/*margin: 5px 10px;*/
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
}
|
||||
.faded {
|
||||
color: var(--dark_border_color);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="place-holder" v-if="collapsed && !menuOpen"></div>
|
||||
|
||||
<!-- collapsed menu, appears as a bar -->
|
||||
<div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
|
||||
|
||||
<!-- logo -->
|
||||
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<logo class="logo-display" color="var(--main-accent)" />
|
||||
Notes
|
||||
</router-link>
|
||||
|
||||
<!-- new note -->
|
||||
<div v-if="loggedIn" class="mobile-button">
|
||||
<span v-if="!disableNewNote" @click="createNote">
|
||||
<i class="green plus icon"></i>
|
||||
New Note
|
||||
</span>
|
||||
<span v-if="disableNewNote">
|
||||
<i class="grey plus icon"></i>
|
||||
Working
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- open straight to note -->
|
||||
<router-link
|
||||
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
|
||||
exact-active-class="active"
|
||||
class="mobile-button"
|
||||
:to="`/notes/open/${$store.getters.totals['quickNote']}`">
|
||||
<i class="green sticky note outline icon"></i>
|
||||
Scratch Pad
|
||||
</router-link>
|
||||
|
||||
<!-- create new and redirect to new note id -->
|
||||
<a
|
||||
v-if="loggedIn && $store.getters.totals && !$store.getters.totals['quickNote']"
|
||||
v-on:click="newQuickNote()"
|
||||
exact-active-class="active"
|
||||
class="mobile-button">
|
||||
<i class="green sticky note outline icon"></i>
|
||||
Scratch Pad
|
||||
</a>
|
||||
|
||||
|
||||
<router-link v-if="loggedIn" class="mobile-button" exact-active-class="active" to="/attachments">
|
||||
<i class="green open folder outline icon"></i>
|
||||
Files
|
||||
</router-link>
|
||||
|
||||
<!-- menu -->
|
||||
<div class="mobile-button" v-on:click="collapseMenu">
|
||||
<i class="green link bars icon" ></i>
|
||||
Menu
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="shade" v-if="mobile && !collapsed" v-on:click="collapseMenu"></div>
|
||||
|
||||
<div class="slotholder" v-if="!collapsed && !mobile">
|
||||
</div>
|
||||
|
||||
<div class="global-menu" v-if="!collapsed" v-on:click="menuClicked">
|
||||
|
||||
<div class="menu-section" v-on:click="collapseMenu">
|
||||
<i class="white angle left icon"></i>
|
||||
<logo class="menu-logo-display" color="var(--main-accent)" />
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
|
||||
<div class="ui green fluid compact button">
|
||||
<i class="plus icon"></i>New Note
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
|
||||
<div class="ui basic fluid compact button">
|
||||
<i class="plus loading icon"></i>New Note
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<i class="file outline icon"></i>Notes
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['totalNotes']" class="float-right" number-id="totalNotes" />
|
||||
</router-link>
|
||||
<div>
|
||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(3)" v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)">
|
||||
<i class="grey paper plane outline icon"></i>Shared
|
||||
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']" class="float-right" number-id="sharedToNotes" />
|
||||
</div>
|
||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
|
||||
<i class="grey archive icon"></i>Archived
|
||||
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['archivedNotes']" class="float-right" number-id="archivedNotes" />
|
||||
</div>
|
||||
<div class="menu-item menu-button sub" v-on:click="updateFastFilters(4)" v-if="$store.getters.totals && $store.getters.totals['trashedNotes'] > 0">
|
||||
<i class="grey trash alternate outline icon"></i>Trashed
|
||||
|
||||
<counter v-if="$store.getters.totals && $store.getters.totals['trashedNotes']" class="float-right" number-id="trashedNotes" />
|
||||
</div>
|
||||
<!-- <div class="menu-item sub">Show Only <i class="caret down icon"></i></div> -->
|
||||
<!-- <div v-on:click="updateFastFilters(0)" class="menu-item menu-button sub"><i class="grey linkify icon"></i>Links</div> -->
|
||||
<!-- <div v-on:click="updateFastFilters(1)" class="menu-item menu-button sub"><i class="grey tags icon"></i>Tags</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn && $store.getters.totals && $store.getters.totals['totalFiles']">
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/attachments">
|
||||
<i class="open folder outline icon"></i>Files
|
||||
<counter class="float-right" number-id="totalFiles" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
|
||||
|
||||
<!-- open straight to note -->
|
||||
<router-link
|
||||
v-if="loggedIn && $store.getters.totals && $store.getters.totals['quickNote']"
|
||||
exact-active-class="active"
|
||||
class="menu-item menu-button"
|
||||
:to="`/notes/open/${$store.getters.totals['quickNote']}`">
|
||||
<i class="sticky note outline icon"></i>Scratch Pad
|
||||
</router-link>
|
||||
|
||||
<!-- create new and redirect to new note id -->
|
||||
<a
|
||||
v-if="loggedIn && $store.getters.totals && !$store.getters.totals['quickNote']"
|
||||
v-on:click="newQuickNote()"
|
||||
exact-active-class="active"
|
||||
class="menu-item menu-button">
|
||||
<i class="sticky note outline icon"></i>Scratch Pad
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="!loggedIn">
|
||||
<router-link v-if="!loggedIn" class="menu-item menu-button" exact-active-class="active" to="/">
|
||||
<i class="home icon"></i>Welcome
|
||||
</router-link>
|
||||
|
||||
<router-link exact-active-class="active" class="menu-item menu-button" to="/login">
|
||||
<i class="plug icon"></i>Login
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
<div v-on:click="toggleNightMode" class="menu-item menu-button">
|
||||
<span v-if="$store.getters.getIsNightMode == 0">
|
||||
<i class="moon outline icon"></i>Black Theme</span>
|
||||
<span v-if="$store.getters.getIsNightMode == 1">
|
||||
<i class="moon outline icon"></i>Light Theme</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/settings">
|
||||
<i class="cog icon"></i>Settings
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/metrictrack">
|
||||
<i class="calendar check outlin icon"></i>Metric Track
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
<router-link class="menu-item menu-button" exact-active-class="active" to="/help">
|
||||
<i class="question circle outline icon"></i>Help
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div class="menu-item menu-button" v-on:click="logout()">
|
||||
<i class="log out icon"></i>Log Out
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="menu-section" v-if="gotTags()">
|
||||
<div class="menu-item">
|
||||
<i class="green tags icon"></i>
|
||||
Tags
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="gotTags()">
|
||||
<div class="menu-section"
|
||||
v-for="(data, tag) in $store.getters.totals['tags']">
|
||||
<router-link class="menu-item menu-button" :to="`/search/tags/${tag}`">
|
||||
<span class="single-line-text">
|
||||
<!-- <i class="small grey tag icon"></i> -->
|
||||
<span class="float-right">{{ data.uses }}</span>
|
||||
<span class="faded"> #</span> {{ tag }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-on:click="reloadPage" class="version-display" v-if="version != 0" >
|
||||
<i :class="`${getVersionIcon()} icon`"></i> {{ version }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
version: '0',
|
||||
username: '',
|
||||
collapsed: false,
|
||||
mobile: false,
|
||||
disableNewNote: false,
|
||||
menuOpen: true,
|
||||
userIcon: true,
|
||||
resizeDebounce: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
window.addEventListener('resize', this.resizeEventHandler)
|
||||
},
|
||||
beforeUnmount(){
|
||||
window.removeEventListener('resize', this.resizeEventHandler)
|
||||
},
|
||||
mounted: function(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
this.collapsed = this.$store.getters.getIsUserOnMobile
|
||||
|
||||
if(this.mobile){
|
||||
this.menuOpen = false
|
||||
}
|
||||
|
||||
if(this.loggedIn){
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
this.version = localStorage.getItem('currentVersion')
|
||||
}
|
||||
|
||||
this.resizeEventHandler() //Trigger resize event
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
//Map logged in from state
|
||||
return this.$store.getters.getLoggedIn
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
gotTags(){
|
||||
|
||||
if(this.loggedIn && this.$store.getters.totals && this.$store.getters.totals.tags
|
||||
&& Object.keys(this.$store.getters.totals.tags).length
|
||||
){
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
logout() {
|
||||
|
||||
this.$router.push('/')
|
||||
axios.post('/api/user/logout')
|
||||
|
||||
setTimeout(() => {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
}, 200)
|
||||
},
|
||||
newQuickNote(){
|
||||
|
||||
axios.post('/api/quick-note/get')
|
||||
.then( ({data}) => {
|
||||
|
||||
this.$router.push({'path':'/notes/open/'+data.noteId})
|
||||
})
|
||||
|
||||
},
|
||||
resizeEventHandler(e) {
|
||||
clearTimeout(this.resizeDebounce)
|
||||
this.resizeDebounce = setTimeout(() => {
|
||||
|
||||
this.mobile = false
|
||||
this.menuOpen = false
|
||||
this.collapsed = false
|
||||
|
||||
if(window.innerWidth < 700){
|
||||
|
||||
this.collapsed = true
|
||||
this.mobile = true
|
||||
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
menuClicked(){
|
||||
//Collapse menu when item is clicked in mobile
|
||||
if(this.mobile && !this.collapsed){
|
||||
this.collapsed = true
|
||||
this.menuOpen = false
|
||||
}
|
||||
},
|
||||
collapseMenu(){
|
||||
this.collapsed = !this.collapsed
|
||||
|
||||
if(!this.collapsed){
|
||||
this.menuOpen = true
|
||||
} else {
|
||||
this.menuOpen = false
|
||||
}
|
||||
|
||||
},
|
||||
createNote(event){
|
||||
|
||||
this.disableNewNote = true
|
||||
|
||||
axios.post('/api/note/create', {title:''})
|
||||
.then(response => {
|
||||
|
||||
if(response.data && response.data.id){
|
||||
|
||||
//Push new note to url and it will open
|
||||
this.$router.push('/notes/open/'+response.data.id)
|
||||
|
||||
this.disableNewNote = false
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
|
||||
},
|
||||
toggleNightMode(){
|
||||
this.$store.commit('toggleNightMode')
|
||||
},
|
||||
ucWords(str){
|
||||
return (str + '')
|
||||
.replace(/^(.)|\s+(.)/g, function ($1) {
|
||||
return $1.toUpperCase()
|
||||
})
|
||||
},
|
||||
emitReloadEvent(){
|
||||
//Reloads note page to initial state
|
||||
this.$bus.$emit('note_reload')
|
||||
},
|
||||
updateFastFilters(filterIndex){
|
||||
|
||||
//A little hacky, brings user to notes page then filters on click
|
||||
if(this.$route.name != 'Note Page'){
|
||||
this.$router.push('/notes')
|
||||
setTimeout( () => {
|
||||
this.$bus.$emit('update_fast_filters', filterIndex)
|
||||
}, 500 )
|
||||
} else {
|
||||
this.$bus.$emit('update_fast_filters', filterIndex)
|
||||
}
|
||||
},
|
||||
reloadPage(){
|
||||
location.reload(true)
|
||||
},
|
||||
getVersionIcon(){
|
||||
if(!this.version){
|
||||
return 'radiation alternate'
|
||||
}
|
||||
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation', 'ghost', 'dna', 'burn', 'brain', 'moon', 'torii gate']
|
||||
const index = ( parseInt(String(this.version).replace(/\./g,'')) % (icons.length))
|
||||
return icons[index]
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="loading-container">
|
||||
<svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
|
||||
<rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="0.5s"
|
||||
from="0 50 50"
|
||||
to="180 50 50"
|
||||
type="rotate"
|
||||
id="strokeBox"
|
||||
attributeType="XML"
|
||||
begin="rectBox.end"/>
|
||||
</rect>
|
||||
<rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'var(--main-accent)'" width="50" height="50">
|
||||
<animate
|
||||
attributeName="height"
|
||||
dur="1.3s"
|
||||
attributeType="XML"
|
||||
from="50"
|
||||
to="0"
|
||||
id="rectBox"
|
||||
fill="freeze"
|
||||
begin="0s;strokeBox.end"/>
|
||||
</rect>
|
||||
</svg>
|
||||
<div class="loading-message" v-if="message">{{ message }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'LoadingIcon',
|
||||
props:[ 'message' ],
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
/*min-height: 100px;*/
|
||||
margin: 20px 0;
|
||||
/*padding: 40px;*/
|
||||
border-radius: 7px;
|
||||
background-color: var(--small_element_bg_color);
|
||||
}
|
||||
.loading-container svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.loading-message {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
|
||||
<template>
|
||||
|
||||
<div>
|
||||
|
||||
<!-- thicc form display -->
|
||||
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="require2FA">
|
||||
<div class="ui input">
|
||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide field">
|
||||
<div class="ui fluid buttons">
|
||||
|
||||
|
||||
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
|
||||
<i class="plug icon"></i>
|
||||
Sign Up
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column">
|
||||
<span class="small-terms">
|
||||
By signing up you agree to Solid Scribe's
|
||||
<router-link to="/terms">
|
||||
Terms of Use
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thin form display -->
|
||||
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
|
||||
|
||||
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<div v-on:click="register" class="ui green button">
|
||||
<i class="plug icon"></i>
|
||||
Sign Up Now!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field"><!-- hide this field if someone is logging in with 2FA -->
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
Or Login
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equal width fields">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="require2FA">
|
||||
<div class="ui input">
|
||||
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-on:click="login" class="ui fluid button">
|
||||
<i class="power icon"></i>
|
||||
Login
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="small-terms">
|
||||
By signing up you agree to Solid Scribe's
|
||||
<router-link to="/terms">
|
||||
Terms of Use
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
props:[ 'thin' ],
|
||||
mounted() {
|
||||
|
||||
//Focus on login form on desktop
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
this.$refs.nameForm.focus()
|
||||
}
|
||||
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
authToken: '',
|
||||
require2FA: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
finalizeLogin(data){
|
||||
|
||||
//Destroy local data if there is an error
|
||||
if(data == false){
|
||||
this.$store.commit('destroyLoginToken')
|
||||
return
|
||||
}
|
||||
|
||||
//Login user if we have a valid token
|
||||
if(data && data.token && data.token.length > 0){
|
||||
|
||||
//Set username to local session
|
||||
this.$store.commit('setUsername', this.username)
|
||||
|
||||
const token = data.token
|
||||
|
||||
//Setup socket io after user logs in
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
this.$io.emit('user_connect', token)
|
||||
localStorage.setItem('loginToken', token)
|
||||
|
||||
//Redirect user to notes section after login
|
||||
this.$router.push('/notes')
|
||||
}
|
||||
},
|
||||
register(){
|
||||
|
||||
let error = false
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
|
||||
|
||||
this.$bus.$emit('notification', 'All fields are required.')
|
||||
error = true
|
||||
}
|
||||
|
||||
if( this.password !== this.password2 ){
|
||||
|
||||
this.$bus.$emit('notification', 'Passwords must be identical.')
|
||||
error = true
|
||||
}
|
||||
|
||||
if(error){
|
||||
//Login section
|
||||
this.$router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('/api/public/register', {'username': this.username, 'password': this.password})
|
||||
.then(({data}) => {
|
||||
|
||||
if(data == false){
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
||||
}
|
||||
|
||||
this.finalizeLogin(data)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', 'Unable to Sign Up - Username already in use')
|
||||
})
|
||||
},
|
||||
login(){
|
||||
|
||||
if( this.username.length == 0 || this.password.length == 0 ){
|
||||
this.$bus.$emit('notification', 'Unable to Login - Username and Password Required')
|
||||
return
|
||||
}
|
||||
|
||||
axios.post('/api/public/login', {'username': this.username, 'password': this.password, 'authToken':this.authToken })
|
||||
.then(({data}) => {
|
||||
|
||||
//Enable 2FA on form
|
||||
if(data.success == false && data.verificationRequired == true && this.require2FA == false){
|
||||
this.$bus.$emit('notification', data.message)
|
||||
this.require2FA = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.authForm.focus()
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if(data.success == false){
|
||||
this.$bus.$emit('notification', data.message)
|
||||
return
|
||||
}
|
||||
|
||||
if(data.success){
|
||||
this.finalizeLogin(data)
|
||||
return
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/css" scoped="true">
|
||||
.small-terms {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 132.29166 132.29167"
|
||||
height="500"
|
||||
width="500">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="translate(0,-164.70832)"
|
||||
id="layer1">
|
||||
<path
|
||||
class="darken-accent"
|
||||
id="path3813-4"
|
||||
d="m 56.22733,165.36641 -55.56249926,15.875 8e-7,63.5 47.62499846,11.90625 v 27.78125 l -47.76066333,-13.9757 0.13566407,10.00695 55.56249926,15.875 v -47.625 l -47.6249985,-11.90625 -8e-7,-47.625 47.7606633,-13.94121 c 0.135664,-2.30629 -0.135664,-9.87129 -0.135664,-9.87129 z"
|
||||
:style="`fill:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth};stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
|
||||
<path
|
||||
class="brighten-accent"
|
||||
id="path4563"
|
||||
d="m 20.508581,220.92891 c 15.265814,-14.23899 27.809717,-7.68002 39.687499,3.96875 v -7.9375 C 51.75093,200.8366 37.512584,206.01499 20.508581,205.05391 Z"
|
||||
:style="`fill:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`" />
|
||||
<path
|
||||
class="brighten-accent"
|
||||
id="path4563-6"
|
||||
d="m 111.78985,220.92891 c -15.265834,-14.23899 -27.809737,-7.68002 -39.68752,3.96875 v -7.9375 c 8.445151,-16.12356 22.683497,-10.94517 39.68752,-11.90625 z"
|
||||
:style="`display:inline;fill:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth}px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1`" />
|
||||
<path
|
||||
class="brighten-accent"
|
||||
id="path3813-4-2"
|
||||
d="m 76.07108,165.36641 55.5625,15.875 v 63.5 l -47.625,11.90625 v 27.78125 l 47.76067,-13.9757 -0.13567,10.00695 -55.5625,15.875 v -47.625 l 47.625,-11.90626 V 189.17891 L 75.93542,175.2377 c -0.13567,-2.30629 0.13566,-9.87129 0.13566,-9.87129 z"
|
||||
:style="`display:inline;fill:${displayColor};fill-opacity:1;stroke:${strokeColor};stroke-width:${strokeWidth};stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1`" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'LoadingIcon',
|
||||
props:[
|
||||
'color', // hex value for setting colorr
|
||||
'stroke' // enable or disable stroke
|
||||
],
|
||||
data(){
|
||||
return {
|
||||
displayColor: '#21BA45', //Default green color
|
||||
strokeWidth: '0.5',
|
||||
strokeColor: 'none',
|
||||
}
|
||||
},
|
||||
beforeCreate(){
|
||||
|
||||
|
||||
},
|
||||
created(){
|
||||
|
||||
if(this.stroke){
|
||||
this.strokeWidth = 0.4
|
||||
this.strokeColor = 'rgba(0,0,0,0.9)'
|
||||
}
|
||||
|
||||
//Set color if passed
|
||||
if(this.color){
|
||||
this.displayColor = this.color
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.darken-accent {
|
||||
filter: brightness(62%);
|
||||
-webkit-filter: brightness(62%);
|
||||
}
|
||||
.brighten-accent {
|
||||
filter: saturate(145%);
|
||||
-webkit-filter: saturate(145%);
|
||||
}
|
||||
g > path {
|
||||
filter: drop-shadow(1px 1px 1px black);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,433 @@
|
||||
<style type="text/css" scoped>
|
||||
.an-graph {
|
||||
background: #fefefe;
|
||||
}
|
||||
.inactive.segment {
|
||||
|
||||
}
|
||||
.active.segment {
|
||||
outline: 4px solid cyan;
|
||||
outline-offset: -5px;
|
||||
outline-style: dashed;
|
||||
max-height: 2000px;
|
||||
}
|
||||
.not-padded {
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
margin-bottom: -10px;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.sticky-boy {
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
right: 10px;
|
||||
z-index: 100;
|
||||
width: 70%;
|
||||
background: orange;
|
||||
}
|
||||
.animate-height {
|
||||
transition: max-height 0.8s linear;
|
||||
max-height: 450px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
|
||||
<div class="sixteen wide column" v-if="!editGraphs">
|
||||
<div class="ui basic padded segment">
|
||||
<!-- Just a space to keep things clickable -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column">
|
||||
<dix class="ui basic segment" v-if="!editGraphs">
|
||||
<div class="ui button" v-on:click="toggleEditGraphs">
|
||||
<i class="edit icon"></i>
|
||||
<span>Add/Edit Graphs</span>
|
||||
</div>
|
||||
</dix>
|
||||
|
||||
|
||||
<div v-if="editGraphs">
|
||||
<div class="ui green button" v-on:click="addGraph()">
|
||||
<i class="plus icon"></i>
|
||||
New Graph
|
||||
</div>
|
||||
<div class="ui basic button" v-on:click="toggleEditGraphs">
|
||||
<i class="check circle icon"></i>
|
||||
Done Editing Graphs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment animate-height`">
|
||||
|
||||
<!-- Edit options -->
|
||||
<div class="ui small header" v-if="editGraphs">
|
||||
<div class="ui grid">
|
||||
<div class="eight wide column">
|
||||
<b>Graph #{{ index+1 }}</b>
|
||||
</div>
|
||||
<div class="eight wide right aligned column">
|
||||
<span class="ui tiny compact inverted red button" v-on:click="removeGraph(index)">
|
||||
Remove Graph
|
||||
<i class="close icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="ui center aligned dividing header">
|
||||
{{ getGraphTitle(graph) }}
|
||||
</h3>
|
||||
|
||||
<div v-if="graph?.type == PILL_CALENDAR">
|
||||
<PillCalendarGraph
|
||||
:graph="graph"
|
||||
:tempChartDays="tempChartDays"
|
||||
:userFields="userFields"
|
||||
:cycleData="cycleData"
|
||||
:edit-graphs="editGraphs"
|
||||
:showZeroValues="graph?.options?.showZeroValues"
|
||||
:showTextValues="graph?.options?.showTextValues"
|
||||
:connectDays="graph?.options?.connectDays"
|
||||
:hideValues="graph?.options?.hideValues"
|
||||
:hideIcons="graph?.options?.hideIcons"
|
||||
/>
|
||||
|
||||
<div v-if="editGraphs" class="ui segment">
|
||||
<p>Calendar Graph Toggles</p>
|
||||
<div v-on:click="toggelValue(index, 'hideIcons')"class="ui button">
|
||||
<span v-if="graph?.options?.hideIcons">Show</span><span v-else>Hide</span> Icons
|
||||
</div>
|
||||
<div v-on:click="toggelValue(index, 'hideValues')"class="ui button">
|
||||
<span v-if="graph?.options?.hideValues">Show</span><span v-else>Hide</span> Values
|
||||
</div>
|
||||
<div v-on:click="toggelValue(index, 'showZeroValues')"class="ui button">
|
||||
<span v-if="!graph?.options?.showZeroValues">Show</span><span v-else>Hide</span> Lowest Value
|
||||
</div>
|
||||
<div v-on:click="toggelValue(index, 'showTextValues')"class="ui button">
|
||||
<span v-if="!graph?.options?.showTextValues">Show</span><span v-else>Hide</span> Text Value
|
||||
</div>
|
||||
<div v-on:click="toggelValue(index, 'connectDays')"class="ui button">
|
||||
<span v-if="!graph?.options?.connectDays">Connect</span><span v-else>Disconnect</span> Days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="graph?.type == LAST_DONE">
|
||||
Last done not implemented
|
||||
</div>
|
||||
<div v-if="!graph.fieldIds || graph.fieldIds && graph.fieldIds.length == 0">
|
||||
<h5>Blank Graph</h5>
|
||||
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
|
||||
Select Graph type and Metrics to display
|
||||
</div>
|
||||
<div v-if="graph?.type == undefined && graph.fieldIds && graph.fieldIds.length > 0">
|
||||
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="ui segment" v-if="editGraphs">
|
||||
|
||||
<!-- change graph type -->
|
||||
<div v-for="(graphType, graphId) in graphTypesDef" class="ui buttons">
|
||||
<div class="ui tiny button" v-on:click="changeGraphType(index, graphId)" :class="{'green':(String(graphId) == String(graph?.type))}">
|
||||
{{ graphType }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="fieldId in fields">
|
||||
<span v-if="graph.fieldIds && graph.fieldIds.includes(fieldId)" v-on:click="toggleGraphField(fieldId, index)">
|
||||
<i class="green check square icon"></i>
|
||||
</span>
|
||||
<span v-else v-on:click="toggleGraphField(fieldId, index)">
|
||||
<i class="square outline icon"></i>
|
||||
</span>
|
||||
<i :class="`${getFieldColor(fieldId)} ${getFieldIcon(fieldId)} icon`"></i>
|
||||
<b>{{ userFields[fieldId]?.label }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
|
||||
<div class="sixteen wide column" v-if="!editGraphs">
|
||||
<div class="ui basic padded segment">
|
||||
<!-- Just a space to keep things clickable -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column">
|
||||
<dix class="ui basic segment" v-if="!editGraphs">
|
||||
<div class="ui button" v-on:click="toggleEditGraphs">
|
||||
<i class="edit icon"></i>
|
||||
<span>Add/Edit Graphs</span>
|
||||
</div>
|
||||
</dix>
|
||||
|
||||
|
||||
<div v-if="editGraphs">
|
||||
<div class="ui green button" v-on:click="addGraph()">
|
||||
<i class="plus icon"></i>
|
||||
New Graph
|
||||
</div>
|
||||
<div class="ui basic button" v-on:click="toggleEditGraphs">
|
||||
<i class="check circle icon"></i>
|
||||
Done Editing Graphs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anchor for scrolling to the bottom of graphs -->
|
||||
<div ref="anchor"></div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
const PILL_CALENDAR = 'pillCalendar'
|
||||
const LAST_DONE = 'lastDone'
|
||||
|
||||
export default {
|
||||
inject: ['getFieldColor', 'getFieldIcon'],
|
||||
name: 'MetricTrackingGraphs',
|
||||
props: [
|
||||
'tempChartDays', // Number of days to display
|
||||
'fields', // field IDs for display/order
|
||||
'userFields', // field values defined by user
|
||||
'graphs', // Graph data defined by user
|
||||
'cycleData', // ALL user data
|
||||
'calendar', // Date data for currently open day
|
||||
'editGraphs' // boolean for edit or not edit graphs
|
||||
],
|
||||
components: {
|
||||
'PillCalendarGraph':defineAsyncComponent(() => import('@/components/Metrictracking/PillCalendarGraph.vue')),
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
graphTypesDef:{
|
||||
// [LAST_DONE]: 'Last Done',
|
||||
'undefined':'Line Graph (Default)',
|
||||
[PILL_CALENDAR]:'Calendar Graph',
|
||||
},
|
||||
localGraphData:[],
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
// Constants
|
||||
this.PILL_CALENDAR = PILL_CALENDAR
|
||||
this.LAST_DONE = LAST_DONE
|
||||
|
||||
// Include JS libraries
|
||||
let graphsScript = document.createElement('script')
|
||||
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
|
||||
document.head.appendChild(graphsScript)
|
||||
},
|
||||
mounted(){
|
||||
this.localGraphData = this.graphs
|
||||
|
||||
this.graphCurrentData()
|
||||
},
|
||||
updated(){
|
||||
// update graphs here? Or watch graphs prop
|
||||
},
|
||||
watch: {
|
||||
// whenever question changes, this function will run
|
||||
userFields(newFields, oldFields) {
|
||||
// console.log([newFields, oldFields])
|
||||
if( JSON.stringify(oldFields) == "{}" ){
|
||||
this.graphCurrentData()
|
||||
}
|
||||
},
|
||||
tempChartDays(newDays, oldDays){
|
||||
if( newDays != oldDays ){
|
||||
this.graphCurrentData()
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveGraphs(){
|
||||
this.$emit('saveGraphs', this.localGraphData)
|
||||
},
|
||||
toggleEditGraphs(){
|
||||
|
||||
setTimeout(() => {
|
||||
// scroll last graph into view
|
||||
this.$refs.anchor.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center'
|
||||
})
|
||||
}, 800)
|
||||
|
||||
this.$emit('toggleEditGraphs')
|
||||
},
|
||||
changeGraphType(index, newType){
|
||||
console.log(index + ' change to ' + newType)
|
||||
this.localGraphData[index]['type'] = newType
|
||||
this.saveGraphs()
|
||||
},
|
||||
addGraph(){
|
||||
this.localGraphData.push({})
|
||||
this.saveGraphs()
|
||||
},
|
||||
removeGraph(index){
|
||||
this.localGraphData.splice(index, 1)
|
||||
this.saveGraphs()
|
||||
},
|
||||
toggelValue(graphIndex, optionName){
|
||||
|
||||
if(!this.localGraphData[graphIndex].options){
|
||||
this.localGraphData[graphIndex].options = {}
|
||||
}
|
||||
|
||||
if(this.localGraphData[graphIndex].options[optionName]){
|
||||
this.localGraphData[graphIndex].options[optionName] = false
|
||||
}
|
||||
|
||||
else {
|
||||
this.localGraphData[graphIndex].options[optionName] = true
|
||||
}
|
||||
|
||||
console.log(this.localGraphData[graphIndex].options[optionName])
|
||||
|
||||
this.saveGraphs()
|
||||
|
||||
},
|
||||
toggleGraphField(fieldId, graphIndex){
|
||||
|
||||
if(!Array.isArray(this.localGraphData[graphIndex].fieldIds)){
|
||||
this.localGraphData[graphIndex].fieldIds = []
|
||||
}
|
||||
|
||||
const inSetCheck = this.localGraphData[graphIndex]?.fieldIds.indexOf(fieldId)
|
||||
|
||||
if(inSetCheck == -1){
|
||||
this.localGraphData[graphIndex]?.fieldIds.push(fieldId)
|
||||
}
|
||||
if(inSetCheck > -1){
|
||||
this.localGraphData[graphIndex]?.fieldIds.splice(inSetCheck,1)
|
||||
}
|
||||
|
||||
this.saveGraphs()
|
||||
|
||||
},
|
||||
getGraphTitle(graph){
|
||||
|
||||
const graphFields = graph?.fieldIds || []
|
||||
let fieldTitles = []
|
||||
graphFields.forEach(fieldId => {
|
||||
fieldTitles.push(this.userFields[fieldId]?.label)
|
||||
})
|
||||
|
||||
// console.log(fieldTitles)
|
||||
const title = fieldTitles.join(', ')
|
||||
|
||||
return title
|
||||
},
|
||||
graphCurrentData(){
|
||||
|
||||
// try again if dygraphs isn't loaded
|
||||
if( typeof(window.Dygraph) != 'function' ){
|
||||
setTimeout(() => {
|
||||
this.graphCurrentData()
|
||||
}, 100)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const graphOptions = {
|
||||
interactionModel: {},
|
||||
// pointClickCallback: function(e, pt){
|
||||
// console.log(e)
|
||||
// console.log(pt)
|
||||
// console.log(this.getValue(pt.idx, 0))
|
||||
// }
|
||||
}
|
||||
|
||||
// Excel date format YYYYMMDD
|
||||
const convertToExcelDate = (dateCode) => {
|
||||
return dateCode
|
||||
.split('.')
|
||||
.reverse()
|
||||
.map(item => String(item).padStart(2,0))
|
||||
.join('')
|
||||
}
|
||||
|
||||
// Generate set of keys for graph length
|
||||
let dataKeys = Object.keys(this.cycleData)
|
||||
dataKeys = dataKeys.splice(0, this.tempChartDays)
|
||||
console.log(dataKeys)
|
||||
|
||||
|
||||
// build CSV data for each graph
|
||||
this.graphs.forEach((graph,index) => {
|
||||
|
||||
// only chart line graphs with dygraphs
|
||||
if( graph.type != undefined ){
|
||||
return
|
||||
}
|
||||
if( !graph.fieldIds ){
|
||||
return
|
||||
}
|
||||
|
||||
// CSV or path to a CSV file.
|
||||
let dataString = ""
|
||||
|
||||
// Lookup graph field titles
|
||||
let graphLabels = ['Date']
|
||||
graph.fieldIds.forEach(fieldId => {
|
||||
const graphLabel = this.userFields[fieldId]?.label
|
||||
const escapedLabel = graphLabel.replaceAll(',','')
|
||||
graphLabels.push(escapedLabel)
|
||||
})
|
||||
dataString += graphLabels.join(',') + '\n'
|
||||
|
||||
|
||||
// build each row, for each day
|
||||
for (var i = 0; i < dataKeys.length; i++) {
|
||||
|
||||
let nextFragment = []
|
||||
// push date code to first column
|
||||
nextFragment.push(convertToExcelDate(dataKeys[i]))
|
||||
|
||||
graph.fieldIds.forEach(fieldId => {
|
||||
|
||||
const currentEntry = this.cycleData[dataKeys[i]]
|
||||
let currentValue = currentEntry[fieldId]
|
||||
|
||||
// setup correct float graphing
|
||||
if(fieldId == 'BT'){
|
||||
// parse temp to fixed length float 00.00
|
||||
currentValue = parseFloat(currentValue).toFixed(2)
|
||||
}
|
||||
|
||||
if( currentValue == undefined ){
|
||||
currentValue = -1
|
||||
}
|
||||
|
||||
nextFragment.push(currentValue)
|
||||
|
||||
})
|
||||
|
||||
dataString += nextFragment.join(',') + "\n"
|
||||
}
|
||||
|
||||
|
||||
let graphDiv = document.getElementById("graphdiv"+index)
|
||||
const g = new Dygraph(graphDiv, dataString ,graphOptions)
|
||||
|
||||
})
|
||||
|
||||
return
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,552 @@
|
||||
<style type="text/css" scoped>
|
||||
div.calendar {
|
||||
width: calc(100% - 4px);
|
||||
min-height: 350px;
|
||||
display: flex;
|
||||
margin: 5px 8px 15px;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.day {
|
||||
flex: 0 0 calc(14.28% - 2px);
|
||||
min-height: 50px;
|
||||
border: 1px solid var(--border_color);
|
||||
font-size: 1.2em;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
line-height: 1em;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.today {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.active-entry {
|
||||
outline: #07f4f4;
|
||||
outline-style: none;
|
||||
outline-width: medium;
|
||||
outline-style: none;
|
||||
outline-offset: -1px;
|
||||
outline-style: solid;
|
||||
outline-width: 3px;
|
||||
}
|
||||
.day ~ .has-data {
|
||||
|
||||
}
|
||||
.day ~ .no-data {
|
||||
background: #c7c7c787;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.day > .number {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
z-index: 10;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.day > .sex {
|
||||
font-size: 0.7em;
|
||||
border-radius: 5px;
|
||||
background: rgba(249, 0, 0, 0.15);
|
||||
color: white;
|
||||
padding: 0 0 0 4px;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 26px;
|
||||
}
|
||||
.day > .period {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
height: 5px;
|
||||
background: red;
|
||||
z-index: 10;
|
||||
}
|
||||
.day > .mucus {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
background: #abecff7d;
|
||||
z-index: 2;
|
||||
}
|
||||
.day > .notes {
|
||||
|
||||
}
|
||||
|
||||
.pill-container {
|
||||
width: 100%;
|
||||
}
|
||||
.pill {
|
||||
width: calc(100% - 8px);
|
||||
min-height: 2px;
|
||||
margin: 0 4px;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
background: rgb(50 218 255 / 44%);
|
||||
border-radius: 40px;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
position: relative;
|
||||
color: white;
|
||||
font-size: 0.7em;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pill.did-last {
|
||||
margin-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
width: calc(100% - 5px);
|
||||
}
|
||||
.pill.did-next {
|
||||
margin-right: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
width: calc(100% - 5px);
|
||||
}
|
||||
.pill.did-next.did-last {
|
||||
width: 100%;
|
||||
}
|
||||
/* .last-high:after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 15px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-left: 10px solid rgb(50 218 255 / 44%);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -13px;
|
||||
}
|
||||
.next-high:before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 15px solid transparent;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-right: 10px solid rgb(50 218 255 / 44%);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -13px;
|
||||
}*/
|
||||
.big-day {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
min-height: 2px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.zero-day {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.icon-spacer {
|
||||
display: inline-block;
|
||||
background-color: greenyellow;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.past-entries {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
/* padding: 0 10px;*/
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.past-entry {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
border: 1px solid;
|
||||
border-color: var(--dark_border_color);
|
||||
color: var(--text_color);
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
margin: 5px 0 10px;
|
||||
line-height: 2.3em;
|
||||
}
|
||||
|
||||
.day-list {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background-color: green;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.day-list-item {
|
||||
flex-grow: 1;
|
||||
border: 1px solid black;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.pill.red { background-color: #db2828 }
|
||||
.pill.orange { background-color: #f2711c }
|
||||
.pill.yellow { background-color: #fbbd08 }
|
||||
.pill.olive { background-color: #b5cc18 }
|
||||
.pill.green { background-color: #21ba45 }
|
||||
.pill.teal { background-color: #00b5ad }
|
||||
.pill.blue { background-color: #2185d0 }
|
||||
.pill.violet { background-color: #6435c9 }
|
||||
.pill.purple { background-color: #a333c8 }
|
||||
.pill.pink { background-color: #e03997 }
|
||||
.pill.brown { background-color: #a5673f }
|
||||
.pill.grey { background-color: #767676 }
|
||||
.pill.black { background-color: #1b1c1d }
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="calendar">
|
||||
|
||||
<div v-for="day in calendar.weekdays" class="day">
|
||||
{{ day }}
|
||||
</div>
|
||||
<div v-for="day in calendar.days" class="day"
|
||||
:class="{
|
||||
'today':day == calendar.today,
|
||||
'active-entry':calendar.dateCode == `${day}.${calendar.month}.${calendar.year}`,
|
||||
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
|
||||
'no-data':showDayDataColor(day),
|
||||
}">
|
||||
<!-- v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)" -->
|
||||
<span class="number">{{ day }}</span>
|
||||
<!-- {{ `${day}.${calendar.month}.${calendar.year}` }} -->
|
||||
|
||||
|
||||
|
||||
<template v-for="(entry, dateCode) in getChartData" :key="dateCode">
|
||||
<span class="pill-container" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
|
||||
<template v-for="(dayData, fieldId) in entry" :key="fieldId">
|
||||
<span
|
||||
v-if="showZeroValuesCheck(dayData.value, fieldId)"
|
||||
class="pill"
|
||||
:class="[getFieldColor(fieldId), {
|
||||
'did-next':dayData.didNext,
|
||||
'did-last':dayData.didLast,
|
||||
'last-high':dayData.lastHigh,
|
||||
'next-high':dayData.nextHigh,
|
||||
}]">
|
||||
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
|
||||
<!-- <i v-if="dayData.value != 0" :class="`tiny ${getFieldColor(fieldId)} ${getFieldIcon(fieldId)} icon`"></i> -->
|
||||
<!-- <span v-else class="icon-spacer"></span>
|
||||
:style="{height:(Math.round(dayData.value*5)+'px')}"
|
||||
|
||||
-->
|
||||
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
|
||||
<i v-if="!hideIcons" :class="`tiny white ${getFieldIcon(fieldId)} icon`"></i>
|
||||
<span v-if="!hideValues">
|
||||
{{ getDayValue(fieldId, dayData.value) }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
// let chartData = {}
|
||||
|
||||
export default {
|
||||
inject: ['getFieldColor', 'getFieldIcon'],
|
||||
props: [
|
||||
'graph', // options associated with this graph
|
||||
'userFields', // all field attributes
|
||||
'tempChartDays', // number of days to display
|
||||
'cycleData', // all users metric data
|
||||
'editGraphs', // display additional edit options
|
||||
// Graph options
|
||||
'showZeroValues', // Hide graph data with value of zero
|
||||
'showTextValues', // Show button text or button value
|
||||
'connectDays', // Calculates next and previous day connections.
|
||||
'hideValues', // Hide all values on the graph
|
||||
'hideIcons', // option to hide icons
|
||||
],
|
||||
data: function(){
|
||||
return {
|
||||
openModel:true,
|
||||
calendar: {
|
||||
dateObject: null,
|
||||
dateCode: null,
|
||||
monthName: '',
|
||||
dayName:'',
|
||||
daysAgo:0,
|
||||
month: '',
|
||||
year: '',
|
||||
days: [],
|
||||
weekdays: ['S','M','T','W','T','F','S'],
|
||||
today: 0,
|
||||
},
|
||||
chartDateCodes: [], // array of date codes in chart
|
||||
listDateCodes: [],
|
||||
dayList: true,
|
||||
lowestGraphValue: 0,
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
this.setupCalendar(new Date())
|
||||
},
|
||||
computed: {
|
||||
getChartData(){
|
||||
|
||||
let chartData = {}
|
||||
let chartValues = []
|
||||
|
||||
// iterate every day in month by day code
|
||||
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
|
||||
|
||||
// lookup data for that day
|
||||
const cycleDayData = this.cycleData[chartDayCode]
|
||||
|
||||
// if chart data is set for this day
|
||||
if( cycleDayData && Object.keys(cycleDayData).length > 0){
|
||||
chartData[chartDayCode] = {}
|
||||
|
||||
// go over each field to be displayed on graph
|
||||
this.graph.fieldIds.forEach((graphFieldId) => {
|
||||
|
||||
if( cycleDayData[graphFieldId] == undefined ){
|
||||
return
|
||||
}
|
||||
|
||||
// track all chart values
|
||||
chartValues.push(cycleDayData[graphFieldId])
|
||||
|
||||
chartData[chartDayCode][graphFieldId] = {
|
||||
didLast: false,
|
||||
lastHigh: false,
|
||||
didNext: false,
|
||||
nextHigh: false,
|
||||
value: cycleDayData[graphFieldId]
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
this.lowestGraphValue = Math.min(...chartValues)
|
||||
|
||||
// determine next and previous states for display
|
||||
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
|
||||
if(chartData[chartDayCode] && this.connectDays){
|
||||
|
||||
const previousDateCode = this.chartDateCodes[codeIndex-1]
|
||||
const nextDateCode = this.chartDateCodes[codeIndex+1]
|
||||
|
||||
Object.keys(chartData[chartDayCode]).forEach((graphFieldId) => {
|
||||
|
||||
const currentValue = chartData[chartDayCode][graphFieldId].value
|
||||
|
||||
// check for previous entry
|
||||
if( chartData[previousDateCode] && chartData[previousDateCode][graphFieldId] ){
|
||||
|
||||
chartData[chartDayCode][graphFieldId].didLast = true
|
||||
|
||||
// set low value flag
|
||||
const lastHigh = chartData[previousDateCode][graphFieldId].value > 0
|
||||
chartData[chartDayCode][graphFieldId].lastHigh = lastHigh && currentValue == 0
|
||||
}
|
||||
|
||||
// check for next entry
|
||||
if( chartData[nextDateCode] && chartData[nextDateCode][graphFieldId] ){
|
||||
|
||||
chartData[chartDayCode][graphFieldId].didNext = true
|
||||
|
||||
// set low value flag
|
||||
const nextHigh = chartData[nextDateCode][graphFieldId].value > 0
|
||||
chartData[chartDayCode][graphFieldId].nextHigh = nextHigh && currentValue == 0
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// console.log(chartData)
|
||||
|
||||
return chartData
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showZeroValuesCheck(dayValue, fieldId){
|
||||
|
||||
// if graph type is boolean or there are two options
|
||||
let isBooleanField = this.userFields[fieldId].type == 'boolean'
|
||||
if(this.userFields[fieldId].customOptions){
|
||||
let options = this.userFields[fieldId].customOptions
|
||||
|
||||
isBooleanField = options.split(',').length == 2
|
||||
}
|
||||
|
||||
if(isBooleanField && !this.showZeroValues){
|
||||
|
||||
const parsedValue = this.getDayValue(fieldId, dayValue)
|
||||
if(parsedValue == 'Yes'){
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return this.showZeroValues || dayValue > this.lowestGraphValue
|
||||
},
|
||||
getDayValue(fieldId, value){
|
||||
|
||||
if( !this.showTextValues ){
|
||||
return value
|
||||
}
|
||||
|
||||
let options = 'error, Yes, No'
|
||||
|
||||
if(this.userFields[fieldId].customOptions){
|
||||
options = this.userFields[fieldId].customOptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
const values = options.split(',')
|
||||
const selection = String(values[value]).trim()
|
||||
|
||||
return selection
|
||||
},
|
||||
displayDayFromCode(dateCode){
|
||||
|
||||
const parts = dateCode.split('.')
|
||||
return `${parts[0]}`
|
||||
},
|
||||
showDayDataColor(day){
|
||||
// Determine if day has any data set
|
||||
if(day == ''){
|
||||
return false
|
||||
}
|
||||
return !(this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`])
|
||||
},
|
||||
generateDateCode(date){
|
||||
|
||||
const dateSetup = [
|
||||
date.getDate(), // 1-31 (Day)
|
||||
date.getMonth()+1, // 0-11 (Month)
|
||||
date.getFullYear(), // 1888-2022 (Year)
|
||||
]
|
||||
|
||||
return dateSetup.join('.')
|
||||
},
|
||||
setupCalendar(date){
|
||||
|
||||
// visualize each day change
|
||||
this.working = true
|
||||
setTimeout(() => {
|
||||
this.working = false
|
||||
}, 500)
|
||||
|
||||
if(!date && this.dateObject){
|
||||
date = this.dateObject
|
||||
}
|
||||
if(!date){
|
||||
date = new Date()
|
||||
}
|
||||
|
||||
this.calendar.dateObject = date
|
||||
|
||||
this.calendar.dateCode = this.generateDateCode(date)
|
||||
|
||||
// calculate days ago since current date
|
||||
const now = new Date()
|
||||
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
|
||||
const dayInterval = diffSeconds / 86400 // seconds in a day
|
||||
this.calendar.daysAgo = Math.floor(dayInterval)
|
||||
|
||||
|
||||
|
||||
// ------------
|
||||
// setup calendar display
|
||||
var y = date.getFullYear()
|
||||
var m = date.getMonth()
|
||||
|
||||
var firstDay = new Date(y, m, 1);
|
||||
var lastDay = new Date(y, m + 1, 0);
|
||||
|
||||
function getDaysInMonth(year, month) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth() + 1;
|
||||
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
|
||||
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
|
||||
this.calendar.year = currentYear
|
||||
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
|
||||
|
||||
const monthStartDay = firstDay.getDay()
|
||||
let days = Array(monthStartDay).fill(""); // Pad days to start on correct weekday
|
||||
for (let i = 0; i < daysInCurrentMonth; i++) {
|
||||
days.push(i+1)
|
||||
}
|
||||
this.calendar.days = days
|
||||
|
||||
// set today
|
||||
this.calendar.today = date.getDate()
|
||||
this.calendar.month = date.getMonth()+1
|
||||
|
||||
// setup date codes for key matching on calendar
|
||||
this.calendar.days.forEach((day) => {
|
||||
if( day !== "" ){
|
||||
let dateDay = new Date(y, m, day);
|
||||
let dayCode = this.generateDateCode(dateDay)
|
||||
this.chartDateCodes.push(dayCode)
|
||||
}
|
||||
})
|
||||
|
||||
// generate past date codes for list
|
||||
for (let i = 0; i < this.tempChartDays; i++) {
|
||||
|
||||
const now = new Date()
|
||||
const pastDate = now.setDate(now.getDate() - i)
|
||||
const pastDateObj = new Date(pastDate)
|
||||
const newCode = this.generateDateCode(pastDateObj)
|
||||
this.listDateCodes.push(newCode)
|
||||
}
|
||||
|
||||
|
||||
// return codes.reverse()
|
||||
|
||||
|
||||
/*
|
||||
October 2022
|
||||
S M T W T F S
|
||||
1 2 3 4 5 6
|
||||
7 8 9
|
||||
*/
|
||||
|
||||
// -------
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,164 @@
|
||||
<style type="text/css" scoped>
|
||||
.modal-content {
|
||||
position: fixed;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
/* bring your own prefixes */
|
||||
transform: translate(-50%, -40%);
|
||||
z-index: 300;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
width: 50%;
|
||||
max-height: 100%;
|
||||
/*overflow: hidden;*/
|
||||
overflow-y: scroll;
|
||||
font-weight: normal;
|
||||
}
|
||||
.modal-content.fullscreen {
|
||||
width: 96%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.close-container {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
z-index: 320;
|
||||
}
|
||||
|
||||
/* Shrink button text for mobile */
|
||||
@media only screen and (max-width: 740px) {
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
/* padding-bottom: 55px;*/
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content.right-side {
|
||||
width: 60%;
|
||||
max-height: none;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
left: auto;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
.close-container-right-side {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
left: calc(60% + 2px);
|
||||
z-index: 320;
|
||||
}
|
||||
|
||||
.shade {
|
||||
position: fixed;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #0000007d;
|
||||
z-index: 299;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.fade-out-top {
|
||||
animation: fade-out-top 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
animation: fade-out 0.3s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes fade-out-top {
|
||||
0% {
|
||||
/*transform: translate(-50%, -50%);*/
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
/*transform: translate(-50%, -70%);*/
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
/*animation: fade-in 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;*/
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
transform: translate(-50%, -70%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div v-if="openModel">
|
||||
<div class="modal-content" :class="{ 'fade-out-top':(animateOut), 'fade-in':(!animateOut), 'fullscreen':(fullscreen)}">
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
<!-- full screen close button -->
|
||||
<div class="close-container" v-if="fullscreen && clickOutClose !== false">
|
||||
<div class="ui green icon button" v-on:click="closeModel">
|
||||
<i class="close icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shade" v-on:click="closeModel" v-on:mouseenter=" hoverOutClose?closeModel():null " :class="{ 'fade-out':(animateOut) }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'fullscreen', //Make the model really big
|
||||
'clickOutClose', //Set to false to prevent closing of modal by clicking out
|
||||
'hoverOutClose', //Close if cursor leaves modal
|
||||
],
|
||||
data: function(){
|
||||
return {
|
||||
openModel:true,
|
||||
animateOut:false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeModel(){
|
||||
|
||||
//Don't allow closing by clicking out
|
||||
if(this.clickOutClose === false){
|
||||
return
|
||||
}
|
||||
|
||||
//Set stups to close model, animate out
|
||||
this.animateOut = true
|
||||
setTimeout( () => {
|
||||
this.openModel = false
|
||||
this.$emit('close')
|
||||
|
||||
//Once close event is sent, reset to default state
|
||||
this.animateOut = false
|
||||
this.openModel = true
|
||||
|
||||
}, 800)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<span>
|
||||
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete Forever" data-inverted="" data-position="top right">
|
||||
<i class="trash alternate icon"></i>
|
||||
</span>
|
||||
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted="">
|
||||
<i class="red trash alternate icon"></i>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'NoteTitleDisplayCard',
|
||||
props: [ 'noteId', 'displayText' ],
|
||||
data () {
|
||||
return {
|
||||
click: 0,
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
confirmDelete(){
|
||||
this.click++
|
||||
},
|
||||
actuallyDelete(){
|
||||
|
||||
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
|
||||
if(response.data == true){
|
||||
this.$bus.$emit('note_deleted', this.noteId)
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Delete Note') })
|
||||
},
|
||||
reset(){
|
||||
this.click = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="nm-button" :class="moreClass" :data-tooltip="tip" data-inverted :data-position=" bottomTip?'bottom center':'top center'">
|
||||
<!-- Display Icon and text -->
|
||||
<i v-if="icon" :class="`${icon} icon`"></i>
|
||||
<span v-if="(text && mobile) || (text && showText)">{{text}}</span>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/*
|
||||
Menu button
|
||||
Single Icon View
|
||||
Single Icon With small text on mobile
|
||||
Tooltips on desktop
|
||||
Tooltip above or below
|
||||
|
||||
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'NoteMenuButtonComponent',
|
||||
props: [ 'icon', 'text', 'tooltip', 'moreClass', 'showText', 'tip', 'bottomTip'],
|
||||
data () {
|
||||
return {
|
||||
files: [],
|
||||
mobile: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
},
|
||||
mounted(){
|
||||
// console.log('Im a button')
|
||||
},
|
||||
methods: {
|
||||
onFileClick(file){
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<!-- hover over view -->
|
||||
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd">
|
||||
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h2><i class="green tags icon"></i>Edit Tags</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h3>All Tags</h3>
|
||||
<h4 v-if="allTags.length == 0">No tags yet, add a tag.</h4>
|
||||
<div v-if="allTags.length > 0">
|
||||
<div class="ui icon large label clickable" v-for="tag in allTags" :class="{ 'green':isTagOnNote(tag.id) }" v-on:click="toggleTag(tag.text, tag.id, tag.entryId)">
|
||||
{{ ucWords(tag.text) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h3>Tags on Note</h3>
|
||||
<div v-if="allTags.length > 0 && noteTagIds.length > 0">
|
||||
<div class="ui icon large label" v-for="tag in noteTagIds">
|
||||
{{ getTagTextById(tag['tagId']) }} <i class="delete icon" v-on:click="removeTag(tag['entryId'])"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- tag input and suggestion popup -->
|
||||
<div class="ui form">
|
||||
<input
|
||||
placeholder="Tag name. Press (Up) to select suggestion. Press (Enter) to submit."
|
||||
v-model="newTagInput"
|
||||
v-on:keydown="tagInput"
|
||||
v-on:keyup="onKeyup"
|
||||
v-on:blur="onBlur"
|
||||
v-on:focus="onFocus"
|
||||
/>
|
||||
<div class="suggestion-box" v-if="suggestions.length > 0">
|
||||
<div class="suggestion-item" v-for="(item, index) in suggestions" :class="{ 'active':(index == selection) }" v-on:click="onSuggestionClick(index)">
|
||||
{{ucWords(item.text)}} <span class="suggestion-tip" v-if="index == selection">Press Enter to add</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'NoteTagEdit',
|
||||
props: [ 'noteId' ],
|
||||
data () {
|
||||
return {
|
||||
tags: [],
|
||||
newTagInput: '',
|
||||
typeDebounce: null,
|
||||
|
||||
allTags: [],
|
||||
noteTagIds: [],
|
||||
suggestions: [],
|
||||
selection: 0,
|
||||
fullTagEdit: true,
|
||||
loaded: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.getTags()
|
||||
},
|
||||
methods: {
|
||||
getTags(){
|
||||
//Get Note Tags -> /api/tags/get
|
||||
axios.post('/api/tag/get', {'noteId': this.noteId})
|
||||
.then( ({data}) => {
|
||||
|
||||
this.loaded = true
|
||||
|
||||
this.allTags = data.allTags
|
||||
this.noteTagIds = data.noteTagIds
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Tags') })
|
||||
},
|
||||
isTagOnNote(id){
|
||||
for (let i = 0; i < this.noteTagIds.length; i++) {
|
||||
const current = this.noteTagIds[i]
|
||||
if(current && current['tagId'] == id){
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
getTagTextById(id){
|
||||
let tag = this.getTagById(id)
|
||||
if(tag && tag.text){
|
||||
return this.ucWords( tag.text )
|
||||
}
|
||||
},
|
||||
getTagById(id){
|
||||
for (let i = 0; i < this.allTags.length; i++) {
|
||||
const current = this.allTags[i]
|
||||
if(current && current['id'] == id){
|
||||
return current
|
||||
}
|
||||
}
|
||||
},
|
||||
tagInput(event){
|
||||
let vm = this
|
||||
|
||||
//Cancel any of the following key events
|
||||
const code = event.keyCode
|
||||
if([38, 40, 13, 9].includes(code)){
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
//Up - 38 - Go Up suggestion
|
||||
if(code == 40){
|
||||
this.selection ++
|
||||
if(this.selection >= this.suggestions.length){
|
||||
this.selection = -1 //No selection made
|
||||
}
|
||||
return
|
||||
}
|
||||
//Down - 40 - Go Down Suggestion
|
||||
if(code == 38){
|
||||
this.selection --
|
||||
if(this.selection < -1){
|
||||
this.selection = this.suggestions.length -1 //No selection made
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//Enter - 13 - Execute addTag() function
|
||||
if(code == 13){
|
||||
//If an item from list is selected, make that the text
|
||||
if(this.selection > -1 && this.suggestions[vm.selection]){
|
||||
this.newTagInput = this.suggestions[vm.selection].text
|
||||
}
|
||||
this.addTag()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//Tab - 9 - Fill space with suggestion
|
||||
|
||||
//Anything else, perform search
|
||||
clearTimeout(this.typeDebounce)
|
||||
this.typeDebounce = setTimeout( () => {
|
||||
if(event.target.value.length > 0){
|
||||
|
||||
const postData = {
|
||||
'tagText':vm.newTagInput,
|
||||
'noteId':vm.noteId
|
||||
}
|
||||
|
||||
axios.post('/api/tag/suggest', postData)
|
||||
.then(response => {
|
||||
|
||||
vm.suggestions = response.data
|
||||
vm.selection = -1 //Nothing selected
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
onSuggestionClick(index){
|
||||
this.newTagInput = this.suggestions[index].text
|
||||
this.addTag()
|
||||
},
|
||||
toggleTag(tagText, id){
|
||||
|
||||
//remove tag
|
||||
if(this.isTagOnNote(id)){
|
||||
|
||||
//Find database ID for tag
|
||||
let entryId = null
|
||||
this.noteTagIds.forEach(tag => {
|
||||
if(tag.tagId == id){
|
||||
entryId = tag.entryId
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
//Submit database entry to be removed
|
||||
if(entryId){
|
||||
this.removeTag(entryId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Add Tag
|
||||
this.newTagInput = tagText
|
||||
this.addTag()
|
||||
return
|
||||
|
||||
|
||||
|
||||
},
|
||||
addTag(){
|
||||
|
||||
//Don't add blank or empty tags
|
||||
if(this.newTagInput == ''){
|
||||
return
|
||||
}
|
||||
|
||||
let postData = {
|
||||
'tagText':this.newTagInput.trim(),
|
||||
'noteId':this.noteId
|
||||
}
|
||||
let vm = this
|
||||
axios.post('/api/tag/addtonote', postData)
|
||||
.then(response => {
|
||||
vm.newTagInput = ''
|
||||
vm.clearSuggestions()
|
||||
vm.getTags()
|
||||
//Trigger focus event to reload tag suggestions
|
||||
vm.onFocus()
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Add Tag') })
|
||||
},
|
||||
onFocus(){
|
||||
return
|
||||
//Show suggested tags
|
||||
let vm = this
|
||||
let postData = {
|
||||
'noteId':this.noteId
|
||||
}
|
||||
axios.post('/api/tag/latest', postData)
|
||||
.then(response => {
|
||||
|
||||
vm.suggestions = response.data
|
||||
vm.selection = -1 //Nothing selected
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Latest Tags') })
|
||||
},
|
||||
onKeyup(){
|
||||
|
||||
const code = event.keyCode
|
||||
|
||||
//Backspace - clear old suggestions, load latest suggestions
|
||||
if(code == 8 && this.newTagInput.length == 0){
|
||||
//Fetch suggestions
|
||||
this.onFocus()
|
||||
}
|
||||
},
|
||||
onBlur(){
|
||||
let vm = this
|
||||
setTimeout(i => {
|
||||
vm.clearSuggestions()
|
||||
}, 200)
|
||||
},
|
||||
removeTag(tagId){
|
||||
|
||||
let postData = {
|
||||
'tagId':tagId,
|
||||
'noteId':this.noteId
|
||||
}
|
||||
|
||||
axios.post('/api/tag/removefromnote', postData)
|
||||
.then(response => {
|
||||
this.getTags()
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Remove Tag') })
|
||||
},
|
||||
clearSuggestions(){
|
||||
this.suggestions = []
|
||||
this.selection = -1 //No selections
|
||||
},
|
||||
ucWords(str){
|
||||
return (str + '')
|
||||
.replace(/^(.)|\s+(.)/g, function ($1) {
|
||||
return $1.toUpperCase()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
|
||||
/* note tag edit area */
|
||||
.full-tag-area {
|
||||
/*padding: 15px;*/
|
||||
/*border: 1px solid;*/
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.full-tag-area .delete-tag-display {
|
||||
/*margin-top: 15px;*/
|
||||
}
|
||||
.full-tag-area .ui.label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simple-tag-display {
|
||||
width: calc(100% - 0px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 35px;
|
||||
color: var(--text_color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* tag suggestion box styles */
|
||||
.suggestion-box {
|
||||
position: absolute;
|
||||
bottom: 38px;
|
||||
width: 300px;
|
||||
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
|
||||
}
|
||||
.suggestion-item {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
background-color: var(--small_element_bg_color);
|
||||
color: var(--text_color);
|
||||
}
|
||||
.suggestion-item.active {
|
||||
background: green;
|
||||
color: white;
|
||||
}
|
||||
.suggestion-item + .suggestion-item {
|
||||
border-top: 1px solid #DDD;
|
||||
}
|
||||
.suggestion-tip {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,768 @@
|
||||
<template>
|
||||
<div class="note-title-display-card"
|
||||
:style="{'background-color':color, 'color':fontColor, 'border-color':color }"
|
||||
:class="{
|
||||
'currently-open':(currentlyOpen || showWorking),
|
||||
'ring':triggerClosedAnimation,
|
||||
'title-view':titleView
|
||||
}">
|
||||
|
||||
|
||||
<!-- Show title and snippet below it -->
|
||||
<div class="overflow-hidden note-card-text" @click.stop="cardClicked" v-if="!titleView">
|
||||
|
||||
<span v-if="note.title == '' && note.subtext == ''">
|
||||
Empty Note
|
||||
</span>
|
||||
|
||||
<span v-if="noteIcon" class="badge">
|
||||
<i :class="`large ${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
||||
</span>
|
||||
|
||||
<!-- Title display -->
|
||||
<span v-if="note.title.length > 0"
|
||||
class="big-text"><p>{{ note.title }}</p></span>
|
||||
|
||||
<span class="tags" v-if="note.tags">
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click.stop="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}</span>
|
||||
<br>
|
||||
</span>
|
||||
|
||||
<!-- Shared Details -->
|
||||
<span class="subtext" v-if="note.shared == 2">
|
||||
<i class="green paper plane outline icon"></i> Shared
|
||||
<span v-if="note.updated/1000 > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
|
||||
Updated
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="subtext" v-if="note.shareUsername">
|
||||
<i class="green paper plane outline icon"></i> Shared by {{ note.shareUsername }}
|
||||
|
||||
<span v-if="note.opened == null && !beenClicked" class="ui tiny green compact right floated button">
|
||||
New
|
||||
</span>
|
||||
<span v-else-if="note.updated/1000 > note.opened && !beenClicked" class="ui tiny green compact right floated basic button">
|
||||
Updated
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Sub text display -->
|
||||
<span v-if="note.subtext.length > 0"
|
||||
class="small-text"
|
||||
v-html="note.subtext"></span>
|
||||
|
||||
|
||||
<!-- Not indexed warning -->
|
||||
<!-- <span v-if="note.indexed != 1">
|
||||
<span class="green label">Not Indexed</span>
|
||||
</span> -->
|
||||
|
||||
|
||||
<!-- <div class="ui fluid basic button" v-if="note.encrypted == 1">
|
||||
<i class="green lock icon"></i>
|
||||
Locked
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- slim card view -->
|
||||
<div v-if="titleView" class="thin-container" @click="cardClicked">
|
||||
|
||||
<!-- icon -->
|
||||
<span v-if="noteIcon" class="thin-icon">
|
||||
<i :class="`${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
||||
</span>
|
||||
|
||||
<!-- title -->
|
||||
<span class="thin-title" v-if="note.title.length > 0">{{ note.title }}</span>
|
||||
|
||||
<!-- snippet -->
|
||||
<span class="thick-sub" v-if="note.subtext.length > 0 && note.title.length == 0">
|
||||
{{ removeHtml(note.subtext) }}
|
||||
</span>
|
||||
<span class="thin-sub" v-else-if="note.subtext.length > 0">
|
||||
{{ removeHtml(note.subtext) }}
|
||||
</span>
|
||||
<span v-else-if="note.title.length == 0 && removeHtml(note.subtext).length == 0">
|
||||
Empty Note
|
||||
</span>
|
||||
|
||||
<!-- tags -->
|
||||
<span v-if="note.tags" class="thin-tags" >
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag" v-on:click="$emit('tagClick', tag.split(':')[1] )">#{{ tag.split(':')[0] }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- edited -->
|
||||
<span class="thin-right">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
|
||||
<i class="green link ellipsis vertical icon"></i>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toolbar on the bottom -->
|
||||
<div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
|
||||
|
||||
<div v-if="getThumbs.length > 0">
|
||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||
<img v-for="thumb in getThumbs"
|
||||
class="tiny-thumb"
|
||||
:src="`/api/static/thumb_${thumb}`"
|
||||
onerror="
|
||||
this.onerror=null;
|
||||
this.src='/api/static/assets/marketing/void.svg';
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="icon-bar" :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }">
|
||||
|
||||
<span class="time-ago-display">
|
||||
{{$helpers.timeAgo( note.updated )}}
|
||||
</span>
|
||||
|
||||
<span class="teeny-buttons">
|
||||
|
||||
<span v-if="!note.trashed">
|
||||
|
||||
<i class="teeny-button" data-tooltip="Tags" data-inverted v-on:click="toggleTags(true)">
|
||||
<i class="tags icon"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Archive"
|
||||
:data-tooltip="note.archived ? 'Un-Archive':'Archive' "
|
||||
data-inverted v-on:click="archiveNote">
|
||||
<i class="archive icon" :class="{'green':note.archived}"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
:data-tooltip="note.pinned ? 'Un-Pin':'Pin' "
|
||||
data-inverted v-on:click="pinNote">
|
||||
<i class="pin icon" :class="{'green':note.pinned}"></i>
|
||||
</i>
|
||||
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Move to Trash"
|
||||
data-inverted v-on:click="trashNote()">
|
||||
<i class="trash icon"></i>
|
||||
</i>
|
||||
</span>
|
||||
|
||||
<!-- Trash note options -->
|
||||
<span v-if="note.trashed">
|
||||
<i class="teeny-button"
|
||||
data-tooltip="Un-Trash"
|
||||
data-inverted v-on:click="trashNote()">
|
||||
<i class="reply icon"></i>
|
||||
</i>
|
||||
<delete-button class="teeny-button" :note-id="note.id" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- tag edit menu -->
|
||||
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
|
||||
<div class="ui basic segment">
|
||||
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
|
||||
</div>
|
||||
</side-slide-menu>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'NoteTitleDisplayCard',
|
||||
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
|
||||
components: {
|
||||
'delete-button': defineAsyncComponent(() => import('@/components/NoteDeleteButtonComponent.vue')),
|
||||
'note-tag-edit': defineAsyncComponent(() => import('@/components/NoteTagEdit.vue')),
|
||||
'side-slide-menu': defineAsyncComponent(() => import('@/components/SideSlideMenuComponent.vue')),
|
||||
},
|
||||
methods:{
|
||||
cardClicked(){
|
||||
// console.log(this.note)
|
||||
this.beenClicked = true
|
||||
this.onClick(this.note.id)
|
||||
},
|
||||
removeHtml(string){
|
||||
if(string == undefined || string == null || string.length == 0){
|
||||
return ''
|
||||
}
|
||||
|
||||
return string
|
||||
.replace(/&[[#A-Za-z0-9]+A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
|
||||
.replace(/\s+/g, ' ') //Remove all whitespace
|
||||
.trim()
|
||||
},
|
||||
cleanHighlight(text){
|
||||
//Basically just remove whitespace
|
||||
let updated = text.replace(/ /g, '').replace(/<br>/g,'')
|
||||
.replace(/<p><\/p>/g,'').replace(/<p> <\/p>/g,'')
|
||||
|
||||
return updated
|
||||
},
|
||||
splitTags(text){
|
||||
return text.split(',')
|
||||
},
|
||||
openEditAttachment(){
|
||||
this.$router.push('/attachments/note/'+this.note.id)
|
||||
},
|
||||
pinNote(){ //togglePinned() <- old name
|
||||
this.showWorking = true
|
||||
this.note.pinned = this.note.pinned == 1 ? 0:1
|
||||
let postData = {'pinned': this.note.pinned, 'noteId':this.note.id}
|
||||
axios.post('/api/note/setpinned', postData)
|
||||
.then(data => {
|
||||
this.showWorking = false
|
||||
// this event is triggered by the server after note is saved
|
||||
// this.$bus.$emit('update_single_note', this.note.id)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Pin Note') })
|
||||
},
|
||||
archiveNote(){ //toggleArchived() <- old name
|
||||
|
||||
this.showWorking = true
|
||||
|
||||
let postData = {'archived': !this.note.archived, 'noteId':this.note.id}
|
||||
axios.post('/api/note/setarchived', postData)
|
||||
.then(data => {
|
||||
|
||||
//Show message so no one worries where note went
|
||||
let message = 'Moved to Archive'
|
||||
if(postData.archived != 1){
|
||||
message = 'Moved out of Archive'
|
||||
}
|
||||
this.$bus.$emit('notification', message)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Archive Note') })
|
||||
},
|
||||
trashNote(){ //toggleArchived() <- old name
|
||||
|
||||
this.showWorking = true
|
||||
|
||||
let postData = {'trashed': !this.note.trashed, 'noteId':this.note.id}
|
||||
axios.post('/api/note/settrashed', postData)
|
||||
.then(data => {
|
||||
|
||||
//Show message so no one worries where note went
|
||||
let message = 'Moved to Trash'
|
||||
if(postData.trashed == 0){
|
||||
message = 'Moved out of Trash'
|
||||
}
|
||||
this.$bus.$emit('notification', message)
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Trash Note') })
|
||||
},
|
||||
toggleTags(state){
|
||||
|
||||
this.showTagSlideMenu = state
|
||||
|
||||
if(state == false){
|
||||
this.$bus.$emit('update_single_note', this.note.id)
|
||||
}
|
||||
|
||||
},
|
||||
justClosed(){
|
||||
|
||||
// Dont do anything when not is closed.
|
||||
// Its already saved, this will make interface feel snappy
|
||||
|
||||
// Scroll note into view
|
||||
// this.$el.scrollIntoView({
|
||||
// behavior: 'smooth',
|
||||
// block: 'center',
|
||||
// inline: 'center'
|
||||
// })
|
||||
|
||||
// this.$bus.$emit('notification','Note Saved')
|
||||
|
||||
// //After scroll, trigger green outline animation
|
||||
// setTimeout(() => {
|
||||
|
||||
// this.triggerClosedAnimation = true
|
||||
// setTimeout(()=>{
|
||||
// //After 3 seconds, hide it
|
||||
// this.triggerClosedAnimation = false
|
||||
// }, 1500)
|
||||
|
||||
// }, 500)
|
||||
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
note: null,
|
||||
color: null,
|
||||
fontColor: null,
|
||||
noteIcon: null,
|
||||
iconColor: null,
|
||||
beenClicked: false,
|
||||
showTagSlideMenu: false,
|
||||
triggerClosedAnimation: false, //Show just closed animation
|
||||
showWorking: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getThumbs(){
|
||||
if(!this.note.thumbs){
|
||||
return []
|
||||
}
|
||||
|
||||
let notDisplaying = []
|
||||
|
||||
//Remove images displaying in text from the thumbnails
|
||||
this.note.thumbs.forEach( path => {
|
||||
|
||||
const titleLocation = String(this.note.title).indexOf(path)
|
||||
const subtextLocation = String(this.note.subtext).indexOf(path)
|
||||
|
||||
if(titleLocation != -1 || subtextLocation != -1){
|
||||
return
|
||||
}
|
||||
|
||||
notDisplaying.push(path)
|
||||
})
|
||||
|
||||
return notDisplaying
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
this.note = this.data
|
||||
|
||||
if(this.note.color != null){
|
||||
|
||||
const styles = JSON.parse(this.note.color)
|
||||
|
||||
//Set background color
|
||||
if(styles.noteBackground){
|
||||
this.color = styles.noteBackground
|
||||
}
|
||||
|
||||
//set text color
|
||||
if(styles.noteText){
|
||||
this.fontColor = styles.noteText
|
||||
}
|
||||
|
||||
if(styles.noteIcon){
|
||||
this.noteIcon = styles.noteIcon
|
||||
}
|
||||
|
||||
if(styles.iconColor){
|
||||
this.iconColor = styles.iconColor
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
|
||||
.teeny-buttons {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.time-ago-display {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tags {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.teeny-button {
|
||||
border: 1px solid var(--border_color);
|
||||
border-radius: 5px;
|
||||
padding: 0px 0px 0px 4px;
|
||||
text-align: center;
|
||||
margin: 0 0 3px 5px;
|
||||
display: inline-block;
|
||||
min-width: 30px;
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
}
|
||||
.subtext {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*Strict font sizes for card display*/
|
||||
.small-text {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||
/*font-size: 1.0em !important;*/
|
||||
font-size: 14px !important;
|
||||
}
|
||||
.small-text > p, , .small-text > h1, .small-text > h2 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.big-text > p:first-child,
|
||||
.big-text > h1, .big-text > h2 {
|
||||
/*font-size: 1.3em !important;*/
|
||||
font-size: 20px !important;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.big-text > p, .big-text > h1, .big-text > h2 {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.note-title-display-card h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.term-usage {
|
||||
/*border-bottom: 1px solid #DDD;*/
|
||||
/*padding-bottom: 10px;*/
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.term-usage em {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
font-style: normal;
|
||||
}
|
||||
.usage-row + .usage-row {
|
||||
padding: 8px 0 0;
|
||||
border-top: 1px solid #DDD;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
.note-title-display-card {
|
||||
position: relative;
|
||||
background-color: var(--small_element_bg_color);
|
||||
|
||||
/*The subtle shadow*/
|
||||
box-shadow: 2px 2px 6px 0 rgba(0,0,0,.15);
|
||||
transition: box-shadow, border-color ease 0.5s, transform linear 0.5s;
|
||||
margin: 5px;
|
||||
/*padding: 0.7em 1em;*/
|
||||
border-radius: .28571429rem;
|
||||
border: 1px solid transparent;
|
||||
border-color: var(--border_color);
|
||||
/*width: calc(33.333% - 10px);*/
|
||||
width: calc(25% - 10px);
|
||||
/*min-width: 190px;*/
|
||||
/*min-height: 130px;*/
|
||||
/*transition: box-shadow 0.3s;*/
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 1.8rem;
|
||||
letter-spacing: 0.05rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
|
||||
min-height: 100px;
|
||||
max-height: 450px;
|
||||
}
|
||||
.note-title-display-card:hover {
|
||||
box-shadow: 0 8px 15px rgba(0,0,0,0.3);
|
||||
border-color: var(--main-accent);
|
||||
}
|
||||
.note-title-display-card.title-view {
|
||||
width: 100%;
|
||||
min-height: 20px;
|
||||
max-width: none;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
/*overflow: hidden;*/
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
/*box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);*/
|
||||
}
|
||||
.title-view + .title-view {
|
||||
border-top: 1px solid var(--border_color);
|
||||
}
|
||||
|
||||
.thin-container.single-line-text {
|
||||
width: calc(100% - 25px);
|
||||
/*margin: 5px 10px;*/
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.thin-container .thin-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.thin-container .thin-sub {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.thin-container .thick-sub {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.thin-container .thin-tags {
|
||||
float: left;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.thin-container .thin-right {
|
||||
float: right;
|
||||
color: var(--dark_border_color);
|
||||
}
|
||||
.thin-container .thin-icon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.icon-bar {
|
||||
display: inline-block;
|
||||
padding: 5px 10px 0;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
background-color: rgba(200, 200, 200, 0.2);
|
||||
}
|
||||
.hover-hide {
|
||||
opacity: 0.0;
|
||||
transition: opacity ease 0.6s;
|
||||
}
|
||||
.little-tag {
|
||||
font-size: 0.7em;
|
||||
padding: 5px 5px;
|
||||
margin: 0 3px 5px 0;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
max-width: 175px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
line-height: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
color: var(--main-accent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tiny-thumb-box {
|
||||
max-height: 70px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
background-color: rgba(200, 200, 200, 0.2);
|
||||
white-space: nowrap;
|
||||
overflow-x: scroll;
|
||||
border: 1px solid var(--border_color);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tiny-thumb {
|
||||
max-height: 70px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note-title-display-card:hover .icon-bar {
|
||||
opacity: 1;
|
||||
}
|
||||
.note-title-display-card:hover .hover-hide {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.note-card-text {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
flex-grow: 1;
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
.tool-bar {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
align-self: flex-end;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
|
||||
.one-column .note-title-display-card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
/*margin: 0px -5px 10px -5px;*/
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
.overflow-hidden p, .overflow-hidden h3 {
|
||||
word-break: break-word;
|
||||
}
|
||||
.max-height {
|
||||
height: calc(100% + 30px);
|
||||
}
|
||||
.currently-open:after {
|
||||
content: '...';
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #000000b0;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #cecece;
|
||||
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Break points determine when display cards shrink */
|
||||
@media only screen and (max-width: 700px) {
|
||||
.note-title-display-card {
|
||||
width: calc(100% + 10px);
|
||||
/*margin: 0px -5px 10px -5px;*/
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 700px) and (max-width: 900px) {
|
||||
.note-title-display-card {
|
||||
width: calc(50% - 10px);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 900px) and (max-width: 1100px) {
|
||||
.note-title-display-card {
|
||||
width: calc(33.33333% - 10px);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1100px) and (max-width: 1300px) {
|
||||
.note-title-display-card {
|
||||
width: calc(25% - 10px);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1300px) and (max-width: 1800px) {
|
||||
.note-title-display-card {
|
||||
width: calc(20% - 10px);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1800px) {
|
||||
.note-title-display-card {
|
||||
width: calc(16.66666% - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*Animations for cool border effects*/
|
||||
@keyframes bgin {
|
||||
0% {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
|
||||
/*Initial state, no BG*/
|
||||
background-size: 0 4px, 4px 0, 0 4px, 4px 0;
|
||||
}
|
||||
10% {
|
||||
/*Middre state, some filled */
|
||||
background-size: 100% 4px, 4px 0, 100% 4px, 4px 0;
|
||||
}
|
||||
20% {
|
||||
/*final state, all filled */
|
||||
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
|
||||
}
|
||||
30% {
|
||||
background-size: 100% 4px, 4px 100%, 100% 4px, 4px 100%;
|
||||
background-image:
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* TopLeft to Right */
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%), /* TopRight to Bottom */
|
||||
linear-gradient(to right, var(--main-accent) 50%, var(--main-accent) 100%), /* BottomLeft to Right*/
|
||||
linear-gradient(to bottom, var(--main-accent) 50%, var(--main-accent) 100%); /* TopLeft to Bottom */
|
||||
}
|
||||
100% {
|
||||
background-image:
|
||||
linear-gradient(to right, transparent 50%, transparent 100%), /* TopLeft to Right */
|
||||
linear-gradient(to bottom, transparent 50%, transparent 100%), /* TopRight to Bottom */
|
||||
linear-gradient(to right, transparent 50%, transparent 100%), /* BottomLeft to Right*/
|
||||
linear-gradient(to bottom, transparent 50%, transparent 100%); /* TopLeft to Bottom */
|
||||
}
|
||||
}
|
||||
|
||||
.bgboy {
|
||||
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 0, 0 100%, 100% 0, 0 100%;
|
||||
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
|
||||
animation: bgin 4s cubic-bezier(0.19, 1, 0.22, 1) 1;
|
||||
}
|
||||
|
||||
/*switch between ring or BG boy to change save animation*/
|
||||
|
||||
.ring {
|
||||
position: relative;
|
||||
}
|
||||
.ring::after {
|
||||
content: '';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
border: 6px solid #00FFCB;
|
||||
position: absolute;
|
||||
z-index: 800;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: ring 1.5s 1;
|
||||
}
|
||||
|
||||
@keyframes ring {
|
||||
0% {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="button-fix">
|
||||
<div class="ui right floated basic shrinking icon button" v-on:click="showPasteInputArea">
|
||||
<i class="green paste icon"></i>
|
||||
Paste
|
||||
</div>
|
||||
<div class="shade" v-if="showPasteArea" @click.prevent="close">
|
||||
<div class="ui stackable grid full-height" @click.prevent="close">
|
||||
<div class="four wide column"></div>
|
||||
<div class="eight wide middle aligned center aligned column">
|
||||
<div class="ui raised segment">
|
||||
<div class="ui dividing header">
|
||||
<i class="green paste icon"></i>
|
||||
Paste & automatically Save
|
||||
</div>
|
||||
<div class="ui fluid action input">
|
||||
<input
|
||||
id="pastetextarea"
|
||||
type="text"
|
||||
ref="pastearea"
|
||||
@paste.prevent="onPaste"
|
||||
@keyup.enter.prevent="onEnter"
|
||||
placeholder="Paste Here">
|
||||
<button class="ui green labeled icon button" @click.prevent="onEnter">
|
||||
<i class="save icon"></i>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="four wide column"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'PasteButton',
|
||||
props: {},
|
||||
data () {
|
||||
return {
|
||||
showPasteArea: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close(){
|
||||
this.showPasteArea = false
|
||||
},
|
||||
onEnter(e){
|
||||
|
||||
const text = this.$refs.pastearea.value
|
||||
this.saveText(text)
|
||||
|
||||
},
|
||||
onPaste(e){
|
||||
|
||||
// Get pasted data via clipboard API
|
||||
const clipboardData = e.clipboardData || window.clipboardData
|
||||
const pastedData = String(clipboardData.getData('Text')).trim()
|
||||
|
||||
this.saveText(pastedData)
|
||||
|
||||
},
|
||||
saveText(text){
|
||||
|
||||
this.showPasteArea = false
|
||||
if(!text){
|
||||
this.$bus.$emit('notification', 'Nothing to save.')
|
||||
return;
|
||||
}
|
||||
|
||||
axios.post('/api/quick-note/update', { 'pushText':text } )
|
||||
.then( response => {
|
||||
|
||||
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||
})
|
||||
.catch(error => {
|
||||
this.$bus.$emit('notification', 'Failed to Save')
|
||||
})
|
||||
|
||||
},
|
||||
showPasteInputArea(){
|
||||
|
||||
// Show text area and focus its contents
|
||||
this.showPasteArea = true
|
||||
this.$nextTick(() => {
|
||||
const aux = document.getElementById('pastetextarea')
|
||||
aux.focus();
|
||||
})
|
||||
|
||||
// auto hide after 1 Minute
|
||||
setTimeout(() => {
|
||||
this.showPasteArea = false
|
||||
}, 60*1000)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.paste-text-container {
|
||||
background-color: green;
|
||||
position: absolute;
|
||||
width: 50vw;
|
||||
height: 80vh;
|
||||
display: inline-block;
|
||||
}
|
||||
.full-height {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<style type="text/css" scoped>
|
||||
.fixed-search {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
.floating-button {
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
.floating-note-options {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
z-index: 2;
|
||||
}
|
||||
.floating-note-options > .segment {
|
||||
border-top: none;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<span>
|
||||
|
||||
<div class="ui form" v-on:mouseenter="extraMenuHover = true" v-on:mouseleave="extraMenuHover = false">
|
||||
<!-- normal search menu -->
|
||||
<div class="ui left icon fluid input">
|
||||
<input ref="desktopSearch" v-on:blur="focused = false" v-on:focus="focused = true" v-model="searchTerm" @keydown="onKeyDown" @keyup="onKeyUp" placeholder="Search or Start Typing New Note" />
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
|
||||
<div class="floating-button" v-if="searchTerm.length > 0">
|
||||
<i class="big link grey close icon" v-on:click="clear()"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="floating-note-options"
|
||||
v-if="(searchTerm.length > 0 || tagSuggestions.length > 0) && (extraMenuHover)">
|
||||
<div class="ui segment">
|
||||
<div class="ui very compact grid">
|
||||
<div class="five wide column">
|
||||
<div class="ui mini green fluid compact shrinking button" v-on:click="search()">
|
||||
<i class="search icon"></i>Search
|
||||
</div>
|
||||
<span class="button-sub">
|
||||
( Enter )
|
||||
</span>
|
||||
</div>
|
||||
<div class="five wide middle aligned column">
|
||||
<div class="ui mini green fluid compact shrinking button" v-on:click="pushToNewNote()">
|
||||
<i class="plus icon"></i>A New Note
|
||||
</div>
|
||||
<span class="button-sub">
|
||||
( Tab )
|
||||
</span>
|
||||
</div>
|
||||
<div class="six wide right aligned column">
|
||||
<div class="ui mini green fluid compact shrinking button" v-on:click="pushToQuickNote()">
|
||||
<i class="sticky note outline icon"></i>The Scratch Pad
|
||||
</div>
|
||||
<span class="button-sub">
|
||||
( CTRL + ENTER )
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
|
||||
data: function(){
|
||||
return {
|
||||
searchTerm:'',
|
||||
searched: false,
|
||||
|
||||
tagSuggestions: [],
|
||||
tagSearchDebounce: null,
|
||||
|
||||
extraMenuHover: false,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
beforeMount(){
|
||||
//search clear
|
||||
this.$bus.$on('reset_fast_filters', () => {
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
})
|
||||
},
|
||||
beforeUnmount(){
|
||||
this.$bus.$off('reset_fast_filters')
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
tagClick(tagId){
|
||||
|
||||
this.$emit('tagClick', tagId)
|
||||
this.tagSuggestions = []
|
||||
this.searchTerm = ''
|
||||
|
||||
},
|
||||
clear(){
|
||||
this.searched = false
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
this.$refs.desktopSearch.focus()
|
||||
}
|
||||
this.$bus.$emit('note_reload')
|
||||
},
|
||||
pushToQuickNote(){
|
||||
|
||||
const text = this.searchTerm
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
|
||||
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
|
||||
.then( response => {
|
||||
|
||||
this.$bus.$emit('notification', 'Saved To Scratch Pad')
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
|
||||
},
|
||||
pushToNewNote(){
|
||||
|
||||
const text = this.searchTerm
|
||||
this.searchTerm = ''
|
||||
this.tagSuggestions = []
|
||||
|
||||
axios.post('/api/note/create', { text })
|
||||
.then(response => {
|
||||
|
||||
if(response.data && response.data.id){
|
||||
this.$router.push('/notes/open/'+response.data.id)
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
|
||||
},
|
||||
onKeyUp(event){
|
||||
//Search Tags
|
||||
const postData = {
|
||||
'tagText':this.searchTerm.trim()
|
||||
}
|
||||
|
||||
clearTimeout(this.tagSearchDebounce)
|
||||
|
||||
// if(this.searchTerm.length == 0){
|
||||
// this.tagSuggestions = []
|
||||
// return
|
||||
// }
|
||||
|
||||
// this.tagSearchDebounce = setTimeout(() => {
|
||||
// this.tagSuggestions = []
|
||||
// axios.post('/api/tag/suggest', postData)
|
||||
// .then( response => {
|
||||
|
||||
// this.tagSuggestions = response.data
|
||||
// })
|
||||
// .catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
|
||||
// }, 800)
|
||||
},
|
||||
onKeyDown(event){
|
||||
|
||||
//Tab
|
||||
if(event.keyCode == 9){
|
||||
this.pushToNewNote()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
//Commant + Enter
|
||||
if((event.metaKey || event.ctrlKey) && event.keyCode == 13 ){
|
||||
this.pushToQuickNote()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
|
||||
//Enter
|
||||
if(event.keyCode == 13){
|
||||
this.search()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
},
|
||||
search(){
|
||||
|
||||
if(this.searchTerm.length == 0){
|
||||
this.clear()
|
||||
return
|
||||
}
|
||||
|
||||
this.searched = true
|
||||
|
||||
this.$refs.desktopSearch.focus()
|
||||
//Blur after search on mobile
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.$refs.desktopSearch.blur()
|
||||
}
|
||||
|
||||
this.$bus.$emit('update_search_term', this.searchTerm)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,176 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="ui grid" v-if="shareUsername == null">
|
||||
|
||||
<div v-if="!isNoteShared" class="sixteen wide column">
|
||||
<div class="ui button" v-on:click="makeShared()">Enable Sharing</div>
|
||||
<ul>
|
||||
<li>Shared notes can be read and edited by you and all shared users.</li>
|
||||
<li>Shared notes can only be shared by the creator of the note.</li>
|
||||
<li>If you turn off sharing, no one else can read the note.</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="isNoteShared" class="sixteen wide column">
|
||||
<p>Generating a shared URL will expose the password of this note.</p>
|
||||
<div class="ui button" v-on:click="removeShared()">Remove Shared</div>
|
||||
|
||||
<div class="ui button" v-on:click="getSharedUrl()">Get Shareable URL</div>
|
||||
</div>
|
||||
<div class="sixteen wide column" v-if="isNoteShared && sharedUrl.length > 0">
|
||||
<p>Public Link - this link can be disabled by turning off sharing</p>
|
||||
<a target="_blank" :href="sharedUrl">{{ sharedUrl }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui grid" v-if="shareUsername == null">
|
||||
|
||||
<div class="row">
|
||||
<div class="eight wide column">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<input type="text" placeholder="Share with someone" v-model="shareUserInput" v-on:keyup="onKeyup">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui disabled button" v-if="shareUserInput.length == 0">
|
||||
Share
|
||||
</div>
|
||||
<div class="ui green button" v-if="shareUserInput.length > 0" v-on:click="onSubmitClick">
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column" v-if="sharedWithUsers.length > 0">
|
||||
<h3>Users who can edit this note</h3>
|
||||
</div>
|
||||
<div class="row" v-for="item in sharedWithUsers">
|
||||
<div class="eight wide middle aligned column">
|
||||
<h3><i class="green user circle icon"></i>{{item.username}}</h3>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui basic compact button" v-on:click="onRevokeAccess(item.noteId)">Remove Access</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ui grid" v-if="shareUsername != null">
|
||||
<div class="sixteen wide column">
|
||||
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'ShareNoteComponent',
|
||||
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
|
||||
data () {
|
||||
return {
|
||||
isNoteShared: false,
|
||||
sharedWithUsers: [],
|
||||
shareUserInput: '',
|
||||
debounce: null,
|
||||
enableSubmitShare: false,
|
||||
sharedUrl: '',
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
|
||||
// this.isNoteShared = this.noteShared
|
||||
|
||||
if(this.shareUsername == null){
|
||||
this.loadShareList()
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
loadShareList(){
|
||||
axios.post('/api/note/getshareinfo', {'noteId':this.noteId, 'rawTextId':this.rawTextId })
|
||||
.then( ({data}) => {
|
||||
|
||||
this.isNoteShared = (data.shareStatus == 2)
|
||||
this.sharedWithUsers = data.shareUsers
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Shared') })
|
||||
},
|
||||
makeShared(){
|
||||
axios.post('/api/note/enableshare', {'noteId':this.noteId })
|
||||
.then( ({data}) => {
|
||||
this.isNoteShared = true
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
|
||||
},
|
||||
removeShared(){
|
||||
axios.post('/api/note/disableshare', {'noteId':this.noteId })
|
||||
.then( ({data}) => {
|
||||
this.isNoteShared = false
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to remove share status') })
|
||||
},
|
||||
getSharedUrl(){
|
||||
axios.post('/api/note/getsharekey', {'noteId':this.noteId })
|
||||
.then( ({data}) => {
|
||||
|
||||
const encodedKey = encodeURIComponent(data)
|
||||
|
||||
this.sharedUrl = `${window.location.protocol}//${window.location.hostname}/#/public/note/${this.noteId}/${encodedKey}`
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to fetch Shared URL') })
|
||||
},
|
||||
onRevokeAccess(sharedNoteId){
|
||||
|
||||
const postData = {
|
||||
'noteId': this.noteId,
|
||||
'shareUserNoteId': sharedNoteId
|
||||
}
|
||||
|
||||
axios.post('/api/note/shareremoveuser', postData)
|
||||
.then( ({data}) => {
|
||||
console.log(data)
|
||||
if(data == true){
|
||||
this.loadShareList()
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Remove Share User') })
|
||||
},
|
||||
onKeyup(event){
|
||||
if(event.keyCode == 13){
|
||||
this.onSubmitClick()
|
||||
return
|
||||
}
|
||||
},
|
||||
onSubmitClick(){
|
||||
|
||||
axios.post('/api/note/shareadduser', {'noteId':this.noteId, 'rawTextId':this.rawTextId, 'username':this.shareUserInput })
|
||||
.then( ({data}) => {
|
||||
if(data == true){
|
||||
this.shareUserInput = ''
|
||||
this.loadShareList()
|
||||
} else {
|
||||
this.$bus.$emit('notification', 'User not found')
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Add User') })
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,162 @@
|
||||
<style type="text/css" scoped>
|
||||
.slide-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1020;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--small_element_bg_color);
|
||||
}
|
||||
.slide-content {
|
||||
box-sizing: border-box;
|
||||
/*padding: 1em 1.5em;*/
|
||||
height: calc(100% - 43px);
|
||||
border-right: 1px solid var(--menu-border);
|
||||
/*background-color: var(--small_element_bg_color);*/
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.slide-shadow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: red;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
/*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/
|
||||
z-index: 1019;
|
||||
overflow: hidden;
|
||||
/*cursor: pointer;*/
|
||||
}
|
||||
|
||||
.slide-shadow.full-shadow {
|
||||
right: 0;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.note-menu {
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 740px) {
|
||||
.slide-shadow {
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.slide-content {
|
||||
height: calc(100% - 55px);
|
||||
}
|
||||
.slide-container {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.note-menu {
|
||||
height: 55px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.fade-enter-active {
|
||||
animation: fade-in .3s;
|
||||
}
|
||||
.fade-leave-active {
|
||||
animation: fade-in .1s reverse;
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<!-- <transition name="fade"> -->
|
||||
<div>
|
||||
|
||||
<div class="slide-container" :style="{ 'background-color':bgColor, 'color':textColor}">
|
||||
|
||||
<!-- close menu on bottom -->
|
||||
<div class="note-menu">
|
||||
<nm-button more-class="right" icon="close" text="close" :show-text="true" v-on:click.native="close" />
|
||||
</div>
|
||||
|
||||
<!-- content of the editor -->
|
||||
<div class="slide-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div class="slide-shadow" :class="{'full-shadow':fullShadow}" v-on:click="close"></div> -->
|
||||
|
||||
</div>
|
||||
<!-- </transition> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
export default {
|
||||
name: 'SideSlideMenu',
|
||||
props: [ 'name', 'styleObject', 'fullShadow', 'skipHistory' ],
|
||||
components: {
|
||||
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
items: [],
|
||||
bgColor: null,
|
||||
textColor: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
//Other panels will tell this one to close
|
||||
this.$bus.$on('destroy_all_other_side_panels', (name) => {
|
||||
if(this.name != name){
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUnmount(){
|
||||
},
|
||||
mounted(){
|
||||
|
||||
//If note style object is set, use that on the slide menu
|
||||
if(this.styleObject && this.styleObject.noteText){
|
||||
this.textColor = this.styleObject.noteText
|
||||
}
|
||||
|
||||
if(this.styleObject && this.styleObject.noteBackground){
|
||||
this.bgColor = this.styleObject.noteBackground
|
||||
}
|
||||
|
||||
|
||||
//Close all other panels that are not this one
|
||||
this.$nextTick( () => {
|
||||
// this.$bus.$emit('destroy_all_other_side_panels', this.name)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
if(this.skipHistory != true){
|
||||
this.$router.go(-1)
|
||||
}
|
||||
|
||||
this.$emit('close'); //Close menu via event
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,123 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.img-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.img-row {
|
||||
height: 20vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.img-row:last-child {
|
||||
/* There's no science in using "10" here. In all my testing, this delivered the best results. */
|
||||
flex-grow: 10;
|
||||
}
|
||||
|
||||
.img-row > img {
|
||||
max-height: calc(100% - 10px);
|
||||
min-width: calc(100% - 10px);
|
||||
max-width: calc(100% - 10px);
|
||||
object-fit: cover;
|
||||
vertical-align: bottom;
|
||||
/*padding: 5px;*/
|
||||
box-shadow: 0px 2px 2px 1px rgba(34,36,38,0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div v-if="uploadedToNote.length > 0">
|
||||
<h2>Images Uploaded to Note</h2>
|
||||
<div class="ui fluid green button" v-on:click="$router.push('/attachments/note/'+noteId)">
|
||||
Manage Files on this Note
|
||||
<i class="chevron circle right icon"></i>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="img-container">
|
||||
<div v-for="file in uploadedToNote" class="img-row" v-on:click="onFileClick(file)">
|
||||
<img :src="`/api/static/thumb_${file.file_location}`">
|
||||
</div>
|
||||
|
||||
<!-- extra row helps it display properly -->
|
||||
<div class="img-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Add images to note -->
|
||||
<div v-if="files.length > 0">
|
||||
<h2>All other Images</h2>
|
||||
<div class="ui fluid green button" v-on:click="$router.push('/attachments')">
|
||||
Manage All Files
|
||||
<i class="chevron circle right icon"></i>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="img-container">
|
||||
<div v-for="file in files" class="img-row" v-on:click="onFileClick(file)">
|
||||
<img :src="`/api/static/thumb_${file.file_location}`">
|
||||
</div>
|
||||
|
||||
<!-- extra row helps it display properly -->
|
||||
<div class="img-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'SimpleAttachmentNoteComponent',
|
||||
props: [ 'noteId', 'squireEditor' ],
|
||||
data () {
|
||||
return {
|
||||
files: [],
|
||||
uploadedToNote: [],
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
this.loadImages()
|
||||
},
|
||||
methods: {
|
||||
loadImages(){
|
||||
axios.post('/api/attachment/search', {'attachmentType':'files', 'setSize':1000})
|
||||
.then( ({data}) => {
|
||||
|
||||
//Sort files into two categories
|
||||
data.forEach(file => {
|
||||
if(file['note_id'] == this.noteId){
|
||||
this.uploadedToNote.push(file)
|
||||
} else {
|
||||
this.files.push(file)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Load Attachments') })
|
||||
},
|
||||
onFileClick(file){
|
||||
|
||||
const imageCode = `<img alt="image" src="/api/static/thumb_${file.file_location}">`
|
||||
|
||||
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
|
||||
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
window.history.back();
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="svg-holder" v-if="svg[file]" v-html="svg[file]" :alt="alt"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props:[ 'file','alt' ],
|
||||
data() {
|
||||
return {
|
||||
//Remove title, remove id from each SVG
|
||||
svg: {
|
||||
'idea':'<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="1013.02965" height="844.99456" viewBox="0 0 1013.02965 844.99456"><path d="M1050.48389,297.81272c0,35.17-8.33,64.12-22.7799,88.92a177.67758,177.67758,0,0,1-16.19,23.25c-53.12,64.97-151.88,100.53-241.14,158.13-263.48,170.02-292.02-102.42-280.12-270.3,4.8-67.74,31.18-129.79,73.09-177.3a278.22062,278.22062,0,0,1,28.45-27.97c47.58-40.52,109.05-65.04,178.58-65.04,119.29,0,221.16,71.95,261.55,173.34a261.84957,261.84957,0,0,1,18.55993,96.97Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M830.054,88.4227l-266.71,32.09a278.22062,278.22062,0,0,1,28.45-27.97Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1027.704,386.7327a177.67758,177.67758,0,0,1-16.19,23.25l-228.46-27.48Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1041.2439,228.72269l-387.18994-50.22,377.87,22.34A260.439,260.439,0,0,1,1041.2439,228.72269Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M516.51853,99.50486c0,.69-.02,1.37-.08,2.04a27.99706,27.99706,0,1,1-52.68-15.11,25.73956,25.73956,0,0,1,2.06-3.31,27.9889,27.9889,0,0,1,50.6,14.1C516.4885,97.97483,516.51853,98.73484,516.51853,99.50486Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M516.51853,99.50486c0,.69-.02,1.37-.08,2.04l-41.92-5.04,41.9.72C516.4885,97.97483,516.51853,98.73484,516.51853,99.50486Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M506.51853,113.61484l-38.78,4.67a28.05694,28.05694,0,0,1-2.96-3.95Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M495.51853,82.61484l-31.76,3.82a25.73956,25.73956,0,0,1,2.06-3.31Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M900.25143,603.63055c-.32346.60949-.65991,1.20075-1.027,1.76443a27.99706,27.99706,0,1,1-39.45-38.042,25.73938,25.73938,0,0,1,3.37127-1.9581,27.98891,27.98891,0,0,1,38.08621,36.17477C900.94214,602.265,900.61239,602.95037,900.25143,603.63055Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M900.25143,603.63055c-.32346.60949-.65991,1.20075-1.027,1.76443L864.5584,581.29206l36.67354,20.27763C900.94214,602.265,900.61239,602.95037,900.25143,603.63055Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M884.80385,611.4064l-36.44426-14.054a28.05745,28.05745,0,0,1-.76292-4.87668Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M889.61936,578.867l-29.8449-11.514a25.73938,25.73938,0,0,1,3.37127-1.9581Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1098.56381,120.95526c.48176.494.9422.99477,1.367,1.5163a27.99707,27.99707,0,1,1-48.26355,25.96387,25.73851,25.73851,0,0,1-.83628-3.80793,27.9889,27.9889,0,0,1,46.06931-25.23471C1097.47405,119.88088,1098.02619,120.404,1098.56381,120.95526Z" transform="translate(-93.48517 -27.50272)" fill="var(--main-accent)"/><path d="M1098.56381,120.95526c.48176.494.9422.99477,1.367,1.5163l-33.52955,25.66038,30.499-28.73915C1097.47405,119.88088,1098.02619,120.404,1098.56381,120.95526Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1101.25637,138.03864l-24.50216,30.41946a28.0572,28.0572,0,0,1-4.87694-.76118Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M1071.73725,123.52584l-20.07,24.90959a25.73851,25.73851,0,0,1-.83628-3.80793Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M573.26553,636.95591s61.44557,2.56024,61.44557,17.92163-69.12627,0-69.12627,0Z" transform="translate(-93.48517 -27.50272)" fill="#a0616a"/><path d="M481.09717,872.49728l-115.21045-7.6807L360.76625,803.371l-25.60232,53.76488-112.65022-15.3614c2.56023-97.28882,43.524-192.01741,46.08418-204.81858s8.67923-65.69564,8.67923-65.69564c11.1882-23.73325,69.81727-25.96064,115.77362-23.81013,19.04829.87055,35.94576,2.509,46.36577,3.68674,6.88688.768,10.95787,1.33137,10.95787,1.33137,18.35667,18.92015,30.18524,44.70155,37.45621,73.50417C514.04734,729.73871,481.09717,872.49728,481.09717,872.49728Z" transform="translate(-93.48517 -27.50272)" fill="#2f2e41"/><circle cx="259.60038" cy="187.01487" r="64.00581" fill="#a0616a"/><path d="M309.56161,229.879S291.64,309.24619,263.47743,332.28828s110.09,0,110.09,0-30.72278-51.20465-7.68069-74.24674S309.56161,229.879,309.56161,229.879Z" transform="translate(-93.48517 -27.50272)" fill="#a0616a"/><path d="M440.13345,557.58871,397.659,562.29945,278.83882,575.51034s-.56318-1.51061-1.5617-4.25007c-8.628-23.52854-50.18049-138.022-54.76341-172.406-5.12046-38.40349,48.64441-84.48767,48.64441-84.48767s51.20465-10.24093,66.566-10.24093,76.807,23.04209,76.807,23.04209l22.914,206.12433,1.97143,17.84474Z" transform="translate(-93.48517 -27.50272)" fill="#d0cde1"/><path d="M487.83059,625.97242,401.73,575.51034s-1.51076-4.91576-4.071-13.21089c-1.28011-4.14756-2.8415-9.16551-4.60823-14.84931-9.93371-31.90055-26.60085-84.53892-39.96518-122.9935-20.48186-58.88534,79.36719,104.96952,79.36719,104.96952lLine truncated
|
||||
'onboarding':`<svg id="70bb2b30-be60-4de4-9e4b-86e23973444e" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1143.11" height="875.78" viewBox="0 0 1143.11 875.78"><defs><linearGradient id="e5da6213-051c-4da8-b1b3-926fff123e76" x1="539.33" y1="763.29" x2="539.33" y2="57.83" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="052f7d93-657d-41ac-9ec0-ae0276bba283" x1="1034.54" y1="812.84" x2="1034.54" y2="197.81" gradientTransform="matrix(-1, 0, 0, 1, 1347.17, 0)" xlink:href="#e5da6213-051c-4da8-b1b3-926fff123e76"/></defs><title>onboarding</title><ellipse cx="849.67" cy="829.19" rx="115.8" ry="28.95" fill="var(--main-accent)" opacity="0.1"/><path d="M63.7,616.77c-13.1,4.49-29,13.65-26.32,28.21,1.34,7.38,7.32,12.85,13,17.47a368.3,368.3,0,0,0,79.36,49.15c13.57,6.16,28,11.81,38.25,23.17s15,27.24,18.33,42.55a345.86,345.86,0,0,1,5.72,110.41c40.91.78,86.21-.14,115.16-29.61,17.33-17.65,26.62-43.83,47-57.49,13.59-9.11,30.34-11.37,46.52-13.27q114.87-13.43,230.93-16.26c32.42-.79,65.55-1.07,95.82-12.37s57.75-36,63.9-69.49c.95-5.16,1.57-10.85,5.1-14.45,3.15-3.23,7.87-4,12.28-4.57l265.89-36c14.56-2,29.63-4.13,41.89-12.36,11.86-8,19.86-20.78,27.51-33.24,17.53-28.52,35.72-63,23.58-95.61-9.71-26.06-35.81-41.07-60.75-50.39s-51.79-15.87-72.06-34.14c-30.84-27.8-37.68-73.95-46.37-115.36a675.43,675.43,0,0,0-33-109.89C943,151.89,927.85,121,905.27,96.61S851.62,54.93,820,56.17C778.7,57.81,742.37,90,701,86c-34.94-3.35-63.37-31.35-94.83-48.77C550.32,6.28,484.4,9,423.29,18.65c-64.8,10.21-130,28.08-182.5,68.27-12,9.2-23.48,19.82-30.53,33.66-10.8,21.23-9.87,47.1-15.28,70.69-21.8,95-138.68,134.63-162.71,228.94C23.73,453.69,30.05,490,45.72,520.94c16.85,33.31,50.7,47.51,68.05,76.1C102.17,607.63,79.06,611.5,63.7,616.77Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)" opacity="0.1"/><path d="M898.6,849s-10.48-72.2,21-119.72a110.5,110.5,0,0,0,18.68-67.38,185.42,185.42,0,0,0-5.1-33.61" transform="translate(-28.45 -12.11)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="2"/><path d="M952.92,598.2c-1.21,8.49-20.11,30.91-20.11,30.91S921,602.28,922.18,593.79a15.53,15.53,0,1,1,30.74,4.41Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M971.6,648.87c-5.64,6.46-33.69,15-33.69,15s4.64-29,10.27-35.42a15.53,15.53,0,1,1,23.42,20.41Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M959,729.07c-8.13,2.74-36.66-4-36.66-4s18.62-22.66,26.75-25.39A15.53,15.53,0,0,1,959,729.07Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M935.76,787.61c-7.55,4.07-36.81,2.2-36.81,2.2s14.52-25.47,22.07-29.54a15.53,15.53,0,0,1,14.74,27.34Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M905.4,660.9c5.24,6.79,32.74,17,32.74,17s-2.92-29.18-8.16-36a15.53,15.53,0,0,0-24.58,19Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M879.12,729.7c7,4.91,36.31,6.41,36.31,6.41s-11.5-27-18.54-31.88a15.53,15.53,0,1,0-17.77,25.47Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M862.75,800.08c6.1,6,34.71,12.45,34.71,12.45S890.68,784,884.57,778a15.53,15.53,0,1,0-21.82,22.1Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M952.92,598.2c-1.21,8.49-20.11,30.91-20.11,30.91S921,602.28,922.18,593.79a15.53,15.53,0,1,1,30.74,4.41Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M971.6,648.87c-5.64,6.46-33.69,15-33.69,15s4.64-29,10.27-35.42a15.53,15.53,0,1,1,23.42,20.41Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M959,729.07c-8.13,2.74-36.66-4-36.66-4s18.62-22.66,26.75-25.39A15.53,15.53,0,0,1,959,729.07Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M935.76,787.61c-7.55,4.07-36.81,2.2-36.81,2.2s14.52-25.47,22.07-29.54a15.53,15.53,0,0,1,14.74,27.34Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M905.4,660.9c5.24,6.79,32.74,17,32.74,17s-2.92-29.18-8.16-36a15.53,15.53,0,0,0-24.58,19Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M879.12,729.7c7,4.91,36.31,6.41,36.31,6.41s-11.5-27-18.54-31.88a15.53,15.53,0,1,0-17.77,25.47Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M862.75,800.08c6.1,6,34.71,12.45,34.71,12.45S890.68,784,884.57,778a15.53,15.53,0,1,0-21.82,22.1Z" transform="translate(-28.45 -12.11)" opacity="0.25"/><path d="M899.46,846.71s-38.84-61.76-29.27-118A110.5,110.5,0,0,0,860,659.56a185.49,185.49,0,0,0-18.29-28.66" transform="translate(-28.45 -12.11)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="2"/><path d="M847.51,595.43c2.32,8.26-5.87,36.41-5.87,36.41s-21.7-19.72-24-28a15.53,15.53,0,1,1,29.9-8.44Z" transform="translate(-28.45 -12.11)" fill="var(--main-accent)"/><path d="M885.11,634.19c-2.53,8.19-24.71,27.37-24.71,27.37sLine truncated
|
||||
'cloud':`<svg id="b75ffde3-65ff-4761-a8e9-ebaeba35e9db" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="863.67584" height="787.45556" viewBox="0 0 863.67584 787.45556"><title>cloud_docs</title><rect x="674.64346" y="434.39412" width="2" height="140" fill="#f2f2f2"/><rect x="674.64346" y="281.39412" width="2" height="140" fill="#f2f2f2"/><rect x="674.64346" y="87.39412" width="2" height="140" fill="#f2f2f2"/><circle cx="675.64346" cy="434.39412" r="25" fill="#f2f2f2"/><circle cx="675.64346" cy="267.39412" r="47" fill="#f2f2f2"/><circle cx="675.64346" cy="87.39412" r="63" fill="#f2f2f2"/><circle cx="650.64346" cy="59.39412" r="10" fill="#d0cde1"/><circle cx="690.64346" cy="81.39412" r="6" fill="#d0cde1"/><circle cx="675.64346" cy="113.39412" r="6" fill="#d0cde1"/><path d="M558.5736,732.72005a443.16545,443.16545,0,0,1-168.1455-33.12793A356.65064,356.65064,0,0,1,194.67956,234.2889,285.75854,285.75854,0,0,1,567.183,77.58041,228.807,228.807,0,0,1,692.65905,375.84359,183.245,183.245,0,0,1,453.788,476.33431a146.79491,146.79491,0,0,1-80.502-191.35742,117.77062,117.77062,0,0,1,153.34668-64.51123,94.41622,94.41622,0,0,1,51.71875,122.938A75.64668,75.64668,0,0,1,479.7406,384.888a60.71786,60.71786,0,0,1-33.29786-79.14941A48.831,48.831,0,0,1,510.02331,278.991a39.264,39.264,0,0,1,21.50781,51.125,31.612,31.612,0,0,1-41.16064,17.31641,25.4898,25.4898,0,0,1-13.9624-33.19,20.59,20.59,0,0,1,26.812-11.27929,16.67345,16.67345,0,0,1,9.1333,21.71,13.5396,13.5396,0,0,1-17.62891,7.4165,11.03155,11.03155,0,0,1-6.043-14.36377,9.02471,9.02471,0,0,1,11.752-4.94384,7.42021,7.42021,0,0,1,4.06445,9.66211,6.13434,6.13434,0,0,1-7.99023,3.36132,5.10967,5.10967,0,0,1-2.79883-6.65283,4.283,4.283,0,0,1,5.583-2.34863,3.62962,3.62962,0,0,1,1.98877,4.72705,3.10075,3.10075,0,0,1-4.043,1.70019,2.67971,2.67971,0,0,1-1.46924-3.49462,2.34588,2.34588,0,0,1,.52344-.78467,2.2689,2.2689,0,0,0-.73145.95556,3.10864,3.10864,0,0,0,1.70215,4.04639,4.13478,4.13478,0,0,0,5.38379-2.26465,5.41865,5.41865,0,0,0-2.96826-7.05566,7.02632,7.02632,0,0,0-9.145,3.84717,9.0309,9.0309,0,0,0,4.94629,11.75732,11.54031,11.54031,0,0,0,15.02246-6.31982,14.67283,14.67283,0,0,0-8.03662-19.10352,18.58956,18.58956,0,0,0-24.20557,10.18262,23.4616,23.4616,0,0,0,12.86572,30.583,29.61209,29.61209,0,0,0,38.5542-16.21924,37.26339,37.26339,0,0,0-20.41113-48.51856,46.83052,46.83052,0,0,0-60.97412,25.65088A58.718,58.718,0,0,0,480.49548,383.036a73.64788,73.64788,0,0,0,96.00439-40.38721A92.41557,92.41557,0,0,0,525.8778,222.31722a115.77022,115.77022,0,0,0-150.74023,63.41455,144.79736,144.79736,0,0,0,79.40527,188.751,181.24485,181.24485,0,0,0,236.26465-99.394A226.80605,226.80605,0,0,0,566.4281,79.43246C421.5448,20.35384,255.60974,90.161,196.53112,235.04378,122.63122,416.27523,209.952,623.84017,391.183,697.74056c226.666,92.42676,486.269-16.78515,578.69678-243.45215l1.85156.75586A446.43912,446.43912,0,0,1,564.39,732.68294C562.45153,732.70736,560.50915,732.72005,558.5736,732.72005ZM497.93982,320.27718a.3519.3519,0,0,0-.13428.02735.34584.34584,0,0,0-.18555.1875.6803.6803,0,0,0,.37305.8872,1.10164,1.10164,0,0,0,1.436-.603,1.62634,1.62634,0,0,0-.7207-2.03858l-.63916,1.56543A.33875.33875,0,0,0,497.93982,320.27718Z" transform="translate(-168.16208 -56.27222)" fill="#3f3d56"/><circle cx="489.56553" cy="232.87527" r="49.53794" fill="#2f2e41"/><polygon points="544.143 380.894 550.143 399.894 472.143 411.894 473.143 391.894 544.143 380.894" fill="#a0616a"/><path d="M621.30554,585.16634l-3,61s-17,31-15,75,1,45,1,45l-1,16,16,2v-21s23-58,25-87l23-87Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M734.30554,585.16634l3,61s17,31,15,75-1,45-1,45l1,16-16,2v-21s-23-58-25-87l-23-87Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M718.30554,450.16634s-72,11-83,8l-36,125s33,13,73,9a34.86506,34.86506,0,0,1,24,4c12,7,48-12,48-12l25-5Z" transform="translate(-168.16208 -56.27222)" fill="#2f2e41"/><rect x="496.64953" y="407.40309" width="80" height="91" fill="var(--main-accent)"/><rect x="484.63738" y="417.38516" width="80" height="91" fill="#f2f2f2"/><path d="M605.30554,389.16634l-1,14,18,62s7,44,21,70c0,0,8,40,23,35s-7-44-7-44l-16-60-19-79Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><circle cx="493.64346" cy="250.39412" r="30" fill="#a0616a"/><path d="M644.30554,324.16634s9,27-9,32-36,16-32,34l2,4,125-30a27.43931,27.43931,0,0,0-27-13s-27.807-.73647-25.9035-18.86823l-3.0965-7.13177Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M726.30554,362.16634s5,0,18,17,22,26,22,26,12,6,5,20-32,28-32,28-25,30-28,15,21-23,21-23,18-17,17-27l-41-34Z" transform="translate(-168.16208 -56.27222)" fill="#a0616a"/><path d="M601.30554,387.16634s124-29,127-29l6.29076,9.74735s-1.29076,7.25265-16.29076,9.25265c0,0,3,15-3,30v37s-70,8-76,14l-10.77042-53.41383s-14.22958-8.58617-24.22958-1.58617C604.30554,403.16634,600.30554,390.16634,601.30554,387.16634Z" transform="translate(-168.16208 -56.27222)" fill="#d0cde1"/><path d="M621.30554,780.16634sLine truncated
|
||||
'secure':'<svg id="4687a0c2-bd42-4093-8bbc-92d10a889ded" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="866" height="720.37" viewBox="0 0 866 720.37"><defs><linearGradient id="01871234-c547-4dad-9a9e-117fcc2bb7d8" x1="660.5" y1="458.72" x2="660.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="705039c9-bbaf-489f-8754-0d88ff1d230d" x1="802.94" y1="120.14" x2="802.94" y2="40.02" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="f1b18cc7-88bb-4220-8574-cb67e65e6847" x1="802.94" y1="269.14" x2="802.94" y2="189.02" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="729136a4-2a29-4999-b297-e6b2d53009e3" x1="802.94" y1="417.14" x2="802.94" y2="337.02" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="7a22862c-2f66-4029-a70d-d91858435973" x1="475.1" y1="785.18" x2="475.1" y2="418.21" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="668716b8-0ce2-4b29-be6e-2467fe221975" x1="475.5" y1="810.19" x2="475.5" y2="750.5" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/><linearGradient id="032f3a0d-ef41-4ad6-8a52-90e2f3059f59" x1="459.55" y1="723.31" x2="459.55" y2="473.18" xlink:href="#01871234-c547-4dad-9a9e-117fcc2bb7d8"/></defs><title>secure server</title><g opacity="0.8"><rect x="455" width="411" height="458.72" fill="url(#01871234-c547-4dad-9a9e-117fcc2bb7d8)"/></g><rect x="461.79" y="306.27" width="397.41" height="142.46" fill="#fff"/><rect x="494.41" y="340.06" width="15.4" height="73" fill="#64ffda"/><g opacity="0.7"><rect x="524.81" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.6"><rect x="555.22" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="585.62" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="616.02" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.4"><rect x="646.42" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.3"><rect x="676.83" y="340.06" width="15.4" height="73" fill="#64ffda"/></g><circle cx="802.94" cy="376.09" r="37.16" fill="var(--main-accent)"/><rect x="461.79" y="158.26" width="397.41" height="142.46" fill="#fff"/><rect x="494.41" y="192.05" width="15.4" height="73" fill="#64ffda"/><g opacity="0.7"><rect x="524.81" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.6"><rect x="555.22" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="585.62" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="616.02" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.4"><rect x="646.42" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.3"><rect x="676.83" y="192.05" width="15.4" height="73" fill="#64ffda"/></g><circle cx="802.94" cy="228.09" r="37.16" fill="var(--main-accent)"/><rect x="461.79" y="10.25" width="397.41" height="142.46" fill="#fff"/><rect x="494.41" y="44.05" width="15.4" height="73" fill="#64ffda"/><g opacity="0.7"><rect x="524.81" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.6"><rect x="555.22" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="585.62" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.5"><rect x="616.02" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.4"><rect x="646.42" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><g opacity="0.3"><rect x="676.83" y="44.05" width="15.4" height="73" fill="#64ffda"/></g><circle cx="802.94" cy="80.08" r="40.06" fill="url(#705039c9-bbaf-489f-8754-0d88ff1d230d)"/><circle cx="802.94" cy="229.08" r="40.06" fill="url(#f1b18cc7-88bb-4220-8574-cb67e65e6847)"/><circle cx="802.94" cy="377.08" r="40.06" fill="url(#729136a4-2a29-4999-b297-e6b2d53009e3)"/><circle cx="802.94" cy="80.08" r="37.16" fill="var(--main-accent)"/><g opacity="0.8"><path d="M750.13,785.18H200.07V434a15.8,15.8,0,0,1,15.8-15.8H734.32a15.8,15.8,0,0,1,15.8,15.8Z" transform="translate(-167 -89.81)" fill="url(#7a22862c-2f66-4029-a70d-d91858435973)"/></g><path d="M56.45,334.85h503.3a15.8,15.8,0,0,1,15.8,15.8V695.37a0,0,0,0,1,0,0H40.65a0,0,0,0,1,0,0V350.65A15.8,15.8,0,0,1,56.45,334.85Z" fill="#fff"/><rect x="57.87" y="355.82" width="500.46" height="328.25" fill="#f5f5f5"/><rect x="59.79" y="371.76" width="498.34" height="295.32" fill="#e0e0e0" opacity="0.8"/><rect x="59.79" y="356.07" width="498.34" height="15.69" fill="#f5f5f5"/><circle cx="70.47" cy="363.67" r="3.8" fill="#ff5252"/><circle cx="80.93" cy="363.67" r="3.8" fill="#ff0"/><circle cx="91.4" cy="363.67" r="3.8" fill="#69f0ae"/><text x="-167" y="-89.81"></text><g opacity="0.8"><path d="M765.52,808.77c-30.62,2.32-556.46,1.39-574.84,0C166.59,Line truncated
|
||||
'robot':`<svg id="fd25c83a-acc9-441d-8dce-2bbb37970907" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1076.4" height="733.47" viewBox="0 0 1076.4 733.47"><defs><linearGradient id="bc224463-365b-4973-a542-782a8bb75c72" x1="487.42" y1="814.54" x2="487.42" y2="138.95" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="f10c32a2-f99a-4cbc-971d-c4e860c06767" x1="875.73" y1="813.03" x2="875.73" y2="187.56" gradientTransform="matrix(-1, 0, 0, 1, 1751.04, 0)" xlink:href="#bc224463-365b-4973-a542-782a8bb75c72"/></defs><title>robotics</title><path d="M1130.38,364.35h-91.94V475.79h85.22c.22,1.15.44,2.3.68,3.45h-85.9v89.85l.43-.14c2.43-.85,4.84-1.75,7.23-2.72l1.55-.62q3.66-1.53,7.23-3.23c.84-.39,1.68-.8,2.51-1.22a150,150,0,0,0,36.17-25c.31-.26.6-.55.89-.82,1.46-1.41,2.9-2.85,4.3-4.32.52-.54,1-1.08,1.54-1.64,1.65-1.79,3.24-3.64,4.79-5.5.23-.28.45-.56.68-.82A115.92,115.92,0,0,0,1121.34,498c.13-.26.24-.55.36-.82,1.26-2.82,2.39-5.68,3.37-8.59.41-1.22.81-2.45,1.16-3.68.13-.37.23-.74.34-1.1,5.09-17.84,5.68-36.4,5.68-54.85q0-3.23,0-6.45c0-7.45-.11-14.95-.24-22.44,0-1.94-.1-3.88-.14-5.83-.13-4.94-.3-9.9-.54-14.84-.06-1.37-.13-2.75-.21-4.11,0-.79-.08-1.58-.14-2.37Q1130.71,368.61,1130.38,364.35Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1038.44,240.27h50.44a343.41,343.41,0,0,0-26.83-32.84c-7.49-8.18-15.37-16.16-23.61-23.84Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1038.44,243.72V360.9h91.65c0-.34,0-.69-.09-1-.08-.93-.16-1.85-.25-2.77-.09-1.08-.21-2.17-.32-3.25s-.25-2.35-.39-3.52-.26-2.35-.41-3.52-.29-2.34-.45-3.5-.31-2.33-.48-3.49c-.14-1-.28-1.93-.43-2.89-.21-1.37-.43-2.73-.66-4.09,0-.2-.07-.41-.1-.6a253.15,253.15,0,0,0-6.75-29.42A201.46,201.46,0,0,0,1108.39,273a215,215,0,0,0-17.1-29.29Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M820.1,364.35V475.79h196.38V364.35Zm0,114.88V554.7c22.86-7.31,49-4.35,72.7,1.65,18.35,4.65,36.23,11,54.55,15.55a198.26,198.26,0,0,0,24.87,4.7l2.06.23,3,.27,3,.22a1.67,1.67,0,0,1,.31,0c.44,0,.89,0,1.34.06l.46,0c.54,0,1.08.06,1.62.06h0c.77,0,1.55,0,2.33.07h1c.77,0,1.55,0,2.33,0l1.71,0,1.38,0c.59,0,1.17,0,1.75-.06.94,0,1.88-.08,2.83-.14h0l1.75-.12c.72,0,1.45-.1,2.16-.17,1.53-.12,3-.27,4.55-.46.6-.07,1.21-.14,1.81-.23l.89-.11c1.51-.2,3-.43,4.51-.69,1.11-.17,2.22-.37,3.34-.58V479.23Zm146.06-239v3.44H820.1V360.9h196.38V243.71h-9.85v-3.44h9.85V164.54c-38.2-30.78-82.85-54.88-132.58-66a282.59,282.59,0,0,0-63.8-6.66V240.27Zm-449-.66.69.66H798.13V92.91c-49.9,4.15-98.56,20.91-137.79,48.68-18.85,13.33-35.78,29.14-56.93,39.36a89.15,89.15,0,0,1-35.6,9.2Zm-1.93-60.44v33.35l27.08-26.43C532.24,182.57,524.3,180.2,515.19,179.17Zm-.9,419.56.9,0V671.8q15.33-1.15,30.54-3.14A547.36,547.36,0,0,0,678,634.4h0q6.84-2.76,13.58-5.67,4.49-2,8.93-4,6.66-3.06,13.22-6.29A463.75,463.75,0,0,0,769.62,586c1.11-.76,2.23-1.52,3.33-2.29q3.31-2.3,6.59-4.64c1.09-.79,2.18-1.57,3.26-2.37l2.25-1.66c2.24-1.67,4.47-3.34,6.73-5h0c2.09-1.5,4.2-3,6.34-4.4v-86.4H572.94c.16-.5.31-1,.48-1.53-.18.51-.34,1-.49,1.53H515.19V598.67Zm.9-355V360.9H798.13V243.71Zm0,120.64V475.79H798.13V364.35ZM458.67,182.56l34.56,33.73V179Q475.86,180.08,458.67,182.56ZM218,364.35V475.79H493.23V364.35Zm66.52,275.24a511.15,511.15,0,0,0,129.73,30.78l.19,0q12.58,1.33,25.25,2.06l.31,0h0c17.69,1,35.45,1.23,53.18.57V479.23H218l1,128.32M141.79,475.79h54.28V364.35H174.6c-2.23,3.91-4.39,7.86-6.44,11.85-1.1,2.13-2.17,4.29-3.21,6.45C150.82,412,141.84,444.22,141.79,475.79Zm0,3.44c.44,22.64,5.54,44.87,16.73,65.4,9.3,17,22.22,32,37.51,45V479.23ZM176.61,360.9h19.46v-29A310.66,310.66,0,0,0,176.61,360.9ZM298.5,240.27H486.74l.68-.66-1-1h0L433.49,187A427.49,427.49,0,0,0,387,199.43c-1.44.47-2.88,1-4.31,1.45A422,422,0,0,0,298.5,240.27ZM218,305.64V360.9H459.38l0,0,0,0h33.83v-114l-3.86-3.14H293A378.08,378.08,0,0,0,218,305.64Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1038.44,302.83V360.9h91.65c0-.34,0-.69-.09-1-.08-.93-.16-1.85-.25-2.77-.09-1.08-.21-2.17-.32-3.25s-.25-2.35-.39-3.52-.26-2.35-.41-3.52-.29-2.34-.45-3.5-.31-2.33-.48-3.49c-.14-1-.28-1.93-.43-2.89-.21-1.37-.43-2.73-.66-4.09,0-.2-.07-.41-.1-.6a253.15,253.15,0,0,0-6.75-29.42Z" transform="translate(-61.56 -83.02)" fill="var(--main-accent)" opacity="0.1"/><path d="M1130.38,364.35h-91.94V475.79h85.22c.22,1.15.44,2.3.68,3.45h-85.9v89.85l.43-.14c2.43-.85,4.84-1.75,7.23-2.72l1.55-.62q3.66-1.53,7.23-3.23c.84-.39,1.68-.8,2.51-1.22a150,150,0,0,0,36.17-25c.31-.26.6-.55.89-.82,1.46-1.41,2.9-2.85,4.3-4.32.52-.54,1-1.08,1.54-1.64,1.65-1.79,3.24-3.64,4.79-5.5.23-.28.45-.56.68-.82A115.92,115.92,0,0,0,1121.34,498c.13-.26.24-.55.36-.82,1.26-2.82,2.39-5.68,3.37-8.59.41-1.22.81-2.45,1.16-3.68.13-.37.23-.74.34-1.1,5.09-17.84,5.68-36.4,5Line truncated
|
||||
'icecream':`<svg id="acfec7e7-ff55-4f51-a7da-3052482bb043" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="814.10215" height="785" viewBox="0 0 814.10215 785"><title>ice_cream</title><path d="M663.893,822.88894l-229.21475.7592c-60.81041-18.2721-110.6323-81.736-110.94745-176.88721a269.465,269.465,0,0,1,3.23812-42.81223L511.59074,562.471l142.42432-31.995a283.262,283.262,0,0,1,29.2563,30.4193L547.08072,632.80761,482.83355,666.7338,555.028,641.82691l149.84346-51.69428a363.37031,363.37031,0,0,1,24.91622,44.67247l-45.211,46.72484L735.721,648.04757q.32788.76935.653,1.54117C768.78735,726.53874,728.57068,822.67471,663.893,822.88894Z" transform="translate(-192.94892 -57.5)" fill="#f2f2f2"/><path d="M626.22411,508.93981c-34.66294,9.9552-77.27875,22.53336-121.09467,35.60691C441.778,563.45453,375.92052,583.40809,327.9742,597.99381l.0313-.171a65.89939,65.89939,0,0,1,12.15994-28.22451c35.51716-46.17886,80.74259-78.4385,131.07659-90.4187h.00442a188.90614,188.90614,0,0,1,43.20151-5.1543C554.49386,473.89268,592.42573,486.45281,626.22411,508.93981Z" transform="translate(-192.94892 -57.5)" fill="#f2f2f2"/><ellipse cx="284.72861" cy="762.5" rx="156" ry="22.5" fill="#3f3d56"/><polygon points="229.229 552.5 229.229 587.5 254.229 590.5 269.229 571.5 257.229 547.5 229.229 552.5" fill="#fbbebe"/><polygon points="324.229 696.5 337.229 731.5 361.229 723.5 343.229 694.5 324.229 696.5" fill="#fbbebe"/><path d="M487.17753,456s-130-13-126,23,61,139,61,139l38-7s-27-42-25-56-6-33-6-33l19,3,11,17,23,118,35,106,26-7s-14-44-10-58-19-68-19-68l16-94s31-9,22-40S487.17753,456,487.17753,456Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><path d="M433.17753,634s-15-12-14-7a35.88236,35.88236,0,0,1,0,11l-14,31s-34,31-3,32,47-31,47-31,43-32,38-37-31.24173-16.48346-31.24173-16.48346S447.17753,641,433.17753,634Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><path d="M529.17753,773s-14-13-15-7-2,14-2,14l-21,20s-30,29-4,30a50.27149,50.27149,0,0,0,40-17s38-8,36-14-12.02812-29.212-16.51406-30.106S541.17753,781,529.17753,773Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><circle cx="288.22861" cy="223.5" r="31" fill="#fbbebe"/><path d="M467.17753,302s13,35,12,38,41-25,41-25-18-4-18-20Z" transform="translate(-192.94892 -57.5)" fill="#fbbebe"/><path d="M581.17753,353s18,18,21,24,71,82,71,82,45,53,27,54-41-42-41-42-64-47-67-58l-37-38Z" transform="translate(-192.94892 -57.5)" fill="#fbbebe"/><path d="M487.17753,332h-11l-15,24s-15,13-4,40,20,61,20,61,32,4,38,14,40,42,40,42,6-21,4-52,0-78,0-78l-2-62s-38.62342-8.95238-41.31171-7.47619S487.17753,332,487.17753,332Z" transform="translate(-192.94892 -57.5)" fill="#575a89"/><path d="M542.17753,324l15-3s16,10,31,39c0,0-32,21-35,20S542.17753,324,542.17753,324Z" transform="translate(-192.94892 -57.5)" fill="#575a89"/><path d="M450.17753,392s-23,37-16,51,16,13,16,13,17-3,17-17,7-15,7-15,44-36,27-36-40,25-40,25l-1-23Z" transform="translate(-192.94892 -57.5)" fill="#fbbebe"/><path d="M465.17753,355s-15,4-18,20-2,24-2,24l20-2Z" transform="translate(-192.94892 -57.5)" fill="#575a89"/><path d="M437.17753,435s16-21,18-18Z" transform="translate(-192.94892 -57.5)" opacity="0.1"/><path d="M496.92557,243.47071s-16.4399-16.42593-32.26693-5.39712-23.19733,21.533-21.43077,28.66652,6.70064,16.9912,6.70064,16.9912-3.29287-14.78823,10.01317-16.23678,31.49853-10.75491,31.49853-10.75491l13.15752,26.28707s1.60057-6.21312,6.53114-4.57193S511.35516,245.06778,496.92557,243.47071Z" transform="translate(-192.94892 -57.5)" fill="#2f2e41"/><path d="M384.43046,793.39447c0,14.58087-8.6676,19.672-19.36056,19.672q-.37157,0-.742-.00826c-.49543-.01062-.98615-.0342-1.47095-.06724-9.6502-.683-17.14766-6.03594-17.14766-19.59647,0-14.03354,17.93326-31.7415,19.28034-33.052l.00236-.00236c.05191-.05072.07786-.07549.07786-.07549S384.43046,778.8136,384.43046,793.39447Z" transform="translate(-192.94892 -57.5)" fill="var(--main-accent)"/><path d="M364.36451,810.83584l7.08105-9.89437-7.09875,10.98077-.01886,1.13594c-.49543-.01062-.98615-.0342-1.47095-.06724l.7632-14.58794-.0059-.11325.013-.02123.072-1.37775-7.11645-11.00791,7.13886,9.97459.01653.29253.5768-11.02205-6.09255-11.37476,6.16687,9.44024.6004-22.851.00236-.07785v.07549l-.10026,18.01937,6.06544-7.14358-6.0902,8.69591-.16043,9.86842,5.6632-9.4709-5.68678,10.923-.08965,5.48625,8.22171-13.18188L364.56269,798.633Z" transform="translate(-192.94892 -57.5)" fill="#3f3d56"/><path d="M824.25561,277.1656s-6.15258,19.13-15.75329,46.61588c-3.10475,8.92836-6.58171,18.7294-10.33044,29.04561-5.22251,14.36534-10.97441,29.73228-16.99808,45.09922-3.70576,9.45775-7.51178,18.90109-11.36076,28.12986-20.68952,49.46327-42.72393,92.3161-57.161,92.91711-32.43645,1.345-101.91689-215.681-109.50028-239.66146-.44364-1.40225-.67244-2.14622-.67244-2.14622Z" transform="translate(-192.94892 -57.5)" fill="#3f3d56"/><path d="M808.50232,323.78148c-3.10475,8.92836-6.58171,18.7294-10.33044,29.04561L712.25139,277.1656h43.31079Z" transform="translate(-192.94892 -57.5)" opacity="0.1"/><path d="M78Line truncated
|
||||
'watching':`<svg id="bea18b66-86ba-42fe-9190-76147bd64da7" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1057" height="784.44" viewBox="0 0 1057 784.44"><defs><linearGradient id="e9b7614d-2d40-4533-bad7-5d0811b316bd" x1="890.27" y1="819.43" x2="890.27" y2="453.22" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient></defs><title>meditating</title><g opacity="0.2"><path d="M719.61,602a23.27,23.27,0,0,1-2.11,3.25Q707,597.9,696.12,590.78l.47-.93a20.53,20.53,0,0,1,7.27-7.92c4-2.46,8.51-3,12.22-1s5.78,6,6,10.67A20.6,20.6,0,0,1,719.61,602Z" transform="translate(-71.5 -57.78)" fill="#3f3d56"/><ellipse cx="717.85" cy="577.53" rx="17.05" ry="13.02" transform="translate(-200 883.82) rotate(-62.09)" fill="#3f3d56"/><ellipse cx="727.6" cy="559.12" rx="17.05" ry="13.02" transform="translate(-178.54 882.64) rotate(-62.09)" fill="#3f3d56"/><ellipse cx="737.35" cy="540.71" rx="17.05" ry="13.02" transform="translate(-157.08 881.47) rotate(-62.09)" fill="#3f3d56"/><ellipse cx="747.1" cy="522.3" rx="17.05" ry="13.02" transform="translate(-135.63 880.29) rotate(-62.09)" fill="#3f3d56"/><path d="M770.75,373.5a61.7,61.7,0,0,1-1-8.58L802.67,376l-32.83-17.08a62.45,62.45,0,0,1,22.08-44.23l31.81,46L799,309.49a62.37,62.37,0,1,1,58.36,109.89,62.4,62.4,0,0,1,1,13.39l-50-.4,49.28,8.12a62.37,62.37,0,0,1-36.29,47.05,62.37,62.37,0,1,1-86.65-45.89,62.39,62.39,0,0,1,36.1-68.15Z" transform="translate(-71.5 -57.78)" fill="var(--main-accent)"/><path d="M851.14,459.71a62.14,62.14,0,0,1-29.84,27.83,62.37,62.37,0,1,1-86.65-45.89C732.23,428.36,854.42,453.52,851.14,459.71Z" transform="translate(-71.5 -57.78)" opacity="0.1"/></g><g opacity="0.2"><path d="M288.48,716.67a43.27,43.27,0,0,0,2.39,7.05q23.11-8.85,46.75-17c-.14-.64-.28-1.3-.44-2a39.53,39.53,0,0,0-9.83-18.22c-6.29-6.48-14.45-9.64-22.3-7.72s-13.63,8.47-16.24,17.09A39.55,39.55,0,0,0,288.48,716.67Z" transform="translate(-71.5 -57.78)" fill="#3f3d56"/><ellipse cx="303.33" cy="671.75" rx="25.07" ry="32.81" transform="translate(-222.12 33.29) rotate(-13.72)" fill="#3f3d56"/><ellipse cx="293.82" cy="632.78" rx="25.07" ry="32.81" transform="translate(-213.16 29.93) rotate(-13.72)" fill="#3f3d56"/><ellipse cx="284.31" cy="593.82" rx="25.07" ry="32.81" transform="translate(-204.19 26.56) rotate(-13.72)" fill="#3f3d56"/><ellipse cx="274.8" cy="554.86" rx="25.07" ry="32.81" transform="translate(-195.22 23.2) rotate(-13.72)" fill="#3f3d56"/><path d="M300.88,266a118.65,118.65,0,0,0,5.81-15.56l-66.55,5.11,69.33-16.38a120.32,120.32,0,0,0-20.33-93L208.05,217l70.26-83.83A120.07,120.07,0,1,0,117.52,310.72a119.33,119.33,0,0,0-8.11,24.52L203,358.12,107.18,350A120.14,120.14,0,0,0,152.72,455a120.07,120.07,0,1,0,183.37-44.75A120.08,120.08,0,0,0,300.88,266Z" transform="translate(-71.5 -57.78)" fill="var(--main-accent)"/><path d="M110.16,388.94a119.51,119.51,0,0,0,42.56,66,120.07,120.07,0,1,0,183.37-44.75C346.88,386.55,107,375.85,110.16,388.94Z" transform="translate(-71.5 -57.78)" opacity="0.1"/></g><ellipse cx="532" cy="711.44" rx="525" ry="73" fill="var(--main-accent)" opacity="0.1"/><rect x="138" width="568" height="684" rx="19.8" fill="#464353"/><rect x="187" y="69" width="470" height="546" fill="var(--main-accent)" opacity="0.1"/><circle cx="422" cy="651" r="20" fill="#fff"/><circle cx="422" cy="40" r="9.5" fill="#fff"/><path d="M493.5,88.78a9,9,0,1,1-9,9,9,9,0,0,1,9-9m0-1a10,10,0,1,0,10,10,10,10,0,0,0-10-10Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M393.5,517.67V499.6h2.06v8.67l9.16-8.48a.79.79,0,0,1,.52-.19.86.86,0,0,1,.87.87V516.8a.86.86,0,0,1-.87.87.8.8,0,0,1-.54-.21l-9.14-8.41v8.62Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M405.24,500.09a.36.36,0,0,1,.37.37V516.8a.37.37,0,0,1-.58.29l-8.3-7.63-.87-.81.88-.8,8.29-7.68a.37.37,0,0,1,.21-.08m-10.18,0v17.07H394V500.09h1.07m10.18-1a1.33,1.33,0,0,0-.86.32l-8.32,7.7V499.6a.51.51,0,0,0-.51-.51H393.5a.51.51,0,0,0-.51.51v18a.51.51,0,0,0,.51.51h2.05a.51.51,0,0,0,.51-.51v-7.46l8.32,7.66a1.32,1.32,0,0,0,.86.31,1.35,1.35,0,0,0,1.37-1.36V500.46a1.36,1.36,0,0,0-1.37-1.37Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M591.46,517.67V509l-9.16,8.48a.82.82,0,0,1-.53.19.86.86,0,0,1-.6-.25.82.82,0,0,1-.27-.62V500.46a.87.87,0,0,1,1.42-.67l9.14,8.42V499.6h2l0,18.06,0,0Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M581.76,500.09a.32.32,0,0,1,.21.08l8.3,7.63.87.8-.87.81-8.3,7.68a.31.31,0,0,1-.21.07.36.36,0,0,1-.37-.36V500.46a.37.37,0,0,1,.37-.37m11.26,0v17.07H592V500.09H593m-11.26-1a1.36,1.36,0,0,0-1.37,1.37V516.8a1.36,1.36,0,0,0,1.37,1.36,1.33,1.33,0,0,0,.87-.31l8.32-7.71v7.51a.51.51,0,0,0,.51.51h2a.51.51,0,0,0,.52-.51v-18a.51.51,0,0,0-.52-.51h-2a.51.51,0,0,0-.51.51v7.47l-8.32-7.66a1.34,1.34,0,0,0-.87-.32Z" transform="translate(-71.5 -57.78)" fill="#fff"/><path d="M496.9,518a1.54,1.54,0,0,1-1.Line truncated
|
||||
'':``,
|
||||
'':``,
|
||||
'':``,
|
||||
'':``,
|
||||
'':``,
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
onMouseUp(){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.svg-holder :deep(svg) {
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="button-fix">
|
||||
|
||||
|
||||
<!-- Dropdown Button -->
|
||||
<span v-if="activeTags.length == 0" v-on:click="openMenu()" class="ui basic button shrinking">
|
||||
<i class="green tags icon"></i>
|
||||
Tags
|
||||
<i class="caret down icon"></i>
|
||||
</span>
|
||||
<!-- Remove Tag button -->
|
||||
<span v-if="activeTags.length > 0" v-on:click="openMenu()" class="ui basic button shrinking">
|
||||
<i class="green tag icon"></i>
|
||||
{{ getActiveTag() }}
|
||||
<i class="caret right icon"></i>
|
||||
</span>
|
||||
|
||||
<!-- hidden dropdown menu -->
|
||||
<div class="dropdown-menu" v-if="menuOpen">
|
||||
<div class="ui raised segment">
|
||||
<div class="ui very tight grid">
|
||||
<div class="fourteen wide column">
|
||||
<h2 class="ui header"><i class="small green tags icon"></i>Tags</h2>
|
||||
</div>
|
||||
<div class="two wide middle aligned center aligned column" v-on:click="closeMenu()">
|
||||
<i class="link grey close icon"></i>
|
||||
</div>
|
||||
<div class="sixteen wide middle aligned column" v-if="loadedTags.length == 0">
|
||||
Tags added to Notes will appear here.
|
||||
</div>
|
||||
<div class="row hover-row" v-for="tag in loadedTags" v-on:click="onClick(tag.id)" :class="{'green':(activeTags[0] == tag.id)}">
|
||||
<div class="two wide center aligned column">
|
||||
<i class="grey tag icon"></i>
|
||||
</div>
|
||||
<div class="twelve wide column">
|
||||
{{ ucWords(tag.text) }}
|
||||
</div>
|
||||
<div class="two wide center aligned column">
|
||||
{{tag.usages}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shade" v-if="menuOpen" v-on:click="closeMenu()"></div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
export default {
|
||||
name: 'TagDisplay',
|
||||
props: [ 'activeTags' ],
|
||||
data () {
|
||||
return {
|
||||
loadedTags: [],
|
||||
menuOpen: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
},
|
||||
methods:{
|
||||
closeMenu(){
|
||||
this.menuOpen = false
|
||||
},
|
||||
openMenu(){
|
||||
this.menuOpen = true
|
||||
axios.post('/api/tag/usertags')
|
||||
.then( ({data}) => {
|
||||
this.loadedTags = data
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Tags') })
|
||||
},
|
||||
toggleActive(){
|
||||
this.menuOpen = false
|
||||
const current = this.activeTags[0]
|
||||
this.onClick( current )
|
||||
},
|
||||
onClick(tagId){
|
||||
this.menuOpen = false
|
||||
this.$emit('tagClick', tagId)
|
||||
},
|
||||
ucWords(str){
|
||||
return (str + '')
|
||||
.replace(/^(.)|\s+(.)/g, function ($1) {
|
||||
return $1.toUpperCase()
|
||||
})
|
||||
},
|
||||
getActiveTag(){
|
||||
let text = 'Tags'
|
||||
|
||||
if(this.activeTags.length == 0){
|
||||
return text
|
||||
}
|
||||
|
||||
this.loadedTags.forEach(tag => {
|
||||
if( this.activeTags.includes(tag.id) ){
|
||||
text = this.ucWords(tag.text)
|
||||
}
|
||||
})
|
||||
|
||||
return text
|
||||
},
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.button-fix {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.hover-row:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--menu-accent);
|
||||
}
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
/*width: 70vw;*/
|
||||
top: 50px;
|
||||
z-index: 1005;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
/*min-width: 200px;*/
|
||||
/*max-width: 100%;*/
|
||||
width: 340px;
|
||||
text-align: left;
|
||||
}
|
||||
.dropdown-menu .button {
|
||||
margin: 0 5px 5px 0;
|
||||
}
|
||||
.shade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1004;
|
||||
background-color: #0000008a;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<style type="text/css" scoped>
|
||||
.colors {
|
||||
position: fixed;
|
||||
z-index: 1023;
|
||||
top: 35px;
|
||||
/*height: 100px;*/
|
||||
width: 400px;
|
||||
left: 20%;
|
||||
}
|
||||
.colors-container {
|
||||
/*max-width: 360px;*/
|
||||
display: flex;
|
||||
/*flex-direction: column;*/
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
}
|
||||
.dot {
|
||||
/*display: inline-block;*/
|
||||
|
||||
border-radius: 30px;
|
||||
box-shadow: 0px 0px 0px 1px inset #3e3e3e;
|
||||
margin: 0 0 2px 2px;
|
||||
cursor: pointer;
|
||||
flex-basis: 9%;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.dot > i {
|
||||
margin: 9px 0 0 0;
|
||||
color: white;
|
||||
text-shadow:
|
||||
1px 1px 2px #3e3e3e,
|
||||
1px -1px 2px #3e3e3e,
|
||||
-1px 1px 2px #3e3e3e,
|
||||
-1px -1px 2px #3e3e3e
|
||||
;
|
||||
}
|
||||
.shade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1022;
|
||||
background-color: transparent;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.big-shadow {
|
||||
box-shadow: 0px 4px 5px 1px #a8a8a8;
|
||||
}
|
||||
@media only screen and (max-width: 740px) {
|
||||
.colors {
|
||||
position: fixed;
|
||||
left: 5px;
|
||||
right: -5px;
|
||||
top: 5px;
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="colors">
|
||||
<div class="ui segment big-shadow">
|
||||
<h3>Select Text Color</h3>
|
||||
<div class="colors-container">
|
||||
<span
|
||||
v-for="(color,index) in colors"
|
||||
class="dot"
|
||||
v-on:click="onColorClick(index)"
|
||||
:style="`background-color: ${color};`">
|
||||
<i v-if="lastUsedColor == color" class="check icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shade" v-on:click="close"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
|
||||
export default {
|
||||
components:{
|
||||
'nm-button':defineAsyncComponent(() => import('@/components/NoteMenuButtonComponent.vue'))
|
||||
},
|
||||
props: [ 'lastUsedColor' ],
|
||||
data: function(){
|
||||
return {
|
||||
hover: false,
|
||||
colors: [null,'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'
|
||||
],
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
mounted: function(){
|
||||
},
|
||||
methods: {
|
||||
close(){
|
||||
this.$emit('close')
|
||||
},
|
||||
onColorClick(index){
|
||||
|
||||
this.$emit('color',this.colors[index])
|
||||
this.$nextTick( () => {
|
||||
this.close()
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './stores/mainStore'
|
||||
|
||||
import 'fomantic-ui-css/components/reset.min.css'
|
||||
import 'fomantic-ui-css/components/site.css'
|
||||
import 'fomantic-ui-css/components/button.min.css'
|
||||
import 'fomantic-ui-css/components/container.min.css'
|
||||
import 'fomantic-ui-css/components/form.min.css'
|
||||
import 'fomantic-ui-css/components/grid.min.css'
|
||||
import 'fomantic-ui-css/components/header.min.css'
|
||||
import 'fomantic-ui-css/components/icon.css'
|
||||
import 'fomantic-ui-css/components/input.min.css'
|
||||
import 'fomantic-ui-css/components/segment.min.css'
|
||||
import 'fomantic-ui-css/components/label.min.css'
|
||||
|
||||
import './assets/semantic-helper.css'
|
||||
|
||||
import io from 'socket.io-client'
|
||||
import EventBus from './EventBus'
|
||||
import Helpers from './Helpers'
|
||||
|
||||
const socket = io({ path: '/socket' })
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title + ' - Solid Scribe'
|
||||
next()
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.config.globalProperties.$io = socket
|
||||
app.config.globalProperties.$bus = EventBus
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(Helpers)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,443 @@
|
||||
const SquireButtonFunctions = {
|
||||
data(){
|
||||
return {
|
||||
//active button states
|
||||
activeBold: false,
|
||||
activeItalics: false,
|
||||
activeUnderline: false,
|
||||
activeTitle: false,
|
||||
activeList: false,
|
||||
activeToDo: false,
|
||||
activeColor: null,
|
||||
activeCode: false,
|
||||
activeSubTitle: false,
|
||||
//
|
||||
lastUsedColor: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
//
|
||||
// Inside squire init function
|
||||
//
|
||||
|
||||
pathChangeEvent(e){
|
||||
|
||||
//Reset all button states
|
||||
this.activeBold = false
|
||||
this.activeTitle = false
|
||||
this.activeItalics = false
|
||||
this.activeList = false
|
||||
this.activeToDo = false
|
||||
this.activeColor = null
|
||||
this.activeUnderline = false
|
||||
this.activeCode = false
|
||||
this.activeSubTitle = false
|
||||
|
||||
if(e.path.indexOf('>U>') > -1 || e.path.search(/U$/) > -1){
|
||||
this.activeUnderline = true
|
||||
}
|
||||
if(e.path.indexOf('>B>') > -1 || e.path.search(/B$/) > -1){
|
||||
this.activeBold = true
|
||||
}
|
||||
if(e.path.indexOf('>I') > -1){
|
||||
this.activeItalics = true
|
||||
}
|
||||
if(e.path.indexOf('fontSize=1.4em') > -1){
|
||||
this.activeTitle = true
|
||||
}
|
||||
if(e.path.indexOf('fontSize=0.9em') > -1){
|
||||
this.activeSubTitle = true
|
||||
}
|
||||
if(e.path.indexOf('OL>LI') > -1){
|
||||
this.activeList = true
|
||||
}
|
||||
if(e.path.indexOf('UL>LI') > -1){
|
||||
this.activeToDo = true
|
||||
}
|
||||
if(e.path.indexOf('CODE') > -1){
|
||||
this.activeCode= true
|
||||
}
|
||||
const colorIndex = e.path.indexOf('color=')
|
||||
if(colorIndex > -1){
|
||||
//Get all digigs after color index, then limit to 3
|
||||
let colors = e.path.substring(colorIndex).match(/\d+/g).slice(0,3)
|
||||
|
||||
const lastColor = `rgb(${colors.join(',')})`
|
||||
this.activeColor = lastColor
|
||||
this.lastUsedColor = lastColor
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
//Inside Squire Init
|
||||
//
|
||||
|
||||
removeFormatting(){
|
||||
this.selectLineIfNoSelect()
|
||||
this.editor.removeAllFormatting()
|
||||
},
|
||||
//If nothing is selected, select the entire line
|
||||
selectLineIfNoSelect(){
|
||||
|
||||
//Select entire line if range is not set
|
||||
let selection = this.editor.getSelection()
|
||||
|
||||
if(selection.startOffset == selection.endOffset && selection.startContainer == selection.endContainer){
|
||||
|
||||
let squireRange = this.editor.createRange(
|
||||
selection.startContainer, 0,
|
||||
selection.endContainer, selection.commonAncestorContainer.textContent.length)
|
||||
|
||||
this.editor.setSelection(squireRange)
|
||||
}
|
||||
},
|
||||
modifyFont(inSize){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
let fontInfo = this.editor.getFontInfo()
|
||||
//Toggle font size between large and normal
|
||||
if(fontInfo.size){
|
||||
this.editor.setFontSize(null)
|
||||
} else {
|
||||
this.editor.setFontSize(inSize)
|
||||
}
|
||||
},
|
||||
modifyColor(color){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
//Set color of font
|
||||
this.editor.setTextColour(color)
|
||||
|
||||
this.lastUsedColor = color
|
||||
},
|
||||
applyLastUsedColor(){
|
||||
this.modifyColor(this.lastUsedColor)
|
||||
},
|
||||
toggleList(type){
|
||||
|
||||
//Undo list if its already a lits
|
||||
if(this.editor.hasFormat(type)){
|
||||
this.editor.removeList()
|
||||
return
|
||||
}
|
||||
|
||||
if(type == 'ol'){
|
||||
this.editor.makeOrderedList()
|
||||
}
|
||||
if(type == 'ul'){
|
||||
this.editor.makeUnorderedList()
|
||||
}
|
||||
},
|
||||
toggleUnderline(){
|
||||
this.selectLineIfNoSelect()
|
||||
if( this.editor.hasFormat('u') ){
|
||||
this.editor.removeUnderline()
|
||||
} else {
|
||||
this.editor.underline()
|
||||
}
|
||||
},
|
||||
toggleBold(){
|
||||
this.selectLineIfNoSelect()
|
||||
if( this.editor.hasFormat('b') ){
|
||||
this.editor.removeBold()
|
||||
} else {
|
||||
this.editor.bold()
|
||||
}
|
||||
},
|
||||
toggleItalic(){
|
||||
this.selectLineIfNoSelect()
|
||||
if( this.editor.hasFormat('i') ){
|
||||
this.editor.removeItalic()
|
||||
} else {
|
||||
this.editor.italic()
|
||||
}
|
||||
},
|
||||
modifyCode(){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
this.editor.toggleCode()
|
||||
},
|
||||
undoCustom(){
|
||||
//The same as pressing CTRL + Z
|
||||
// this.editor.focus()
|
||||
// document.execCommand("undo", false, null)
|
||||
this.editor.undo()
|
||||
},
|
||||
uncheckAllListItems(){
|
||||
//
|
||||
// Uncheck All List Items
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||
item.classList.remove('active');
|
||||
})
|
||||
|
||||
},600)
|
||||
|
||||
},
|
||||
deleteCompletedListItems(){
|
||||
//
|
||||
// Delete Completed List Items
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
if(node.nodeName == 'UL'){
|
||||
|
||||
//Create two categories, done and not done list items
|
||||
let undoneElements = document.createDocumentFragment()
|
||||
|
||||
//Go through each item in each list we found
|
||||
node.childNodes.forEach( (checkListItem, index) => {
|
||||
|
||||
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||
if(checkListItem.nodeName == 'UL'){
|
||||
return
|
||||
}
|
||||
|
||||
//Check if list item has active class
|
||||
const checkedItem = checkListItem.classList.contains('active')
|
||||
|
||||
//Check if the next item is a list, Keep lists with intented items together
|
||||
let sublist = null
|
||||
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||
sublist = node.childNodes[index+1]
|
||||
}
|
||||
|
||||
//Push checked items and their sub lists to the done set
|
||||
if(!checkedItem){
|
||||
|
||||
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//Remove all HTML from node, push unfinished items, then finished below them
|
||||
node.innerHTML = null
|
||||
node.appendChild(undoneElements)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}, 600)
|
||||
|
||||
|
||||
},
|
||||
sortList(){
|
||||
//
|
||||
// Sort list, checked at the bottom, unchecked at the top
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile
|
||||
this.$router.go(-1)
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
if(node.nodeName == 'UL'){
|
||||
|
||||
//Create two categories, done and not done list items
|
||||
let doneElements = document.createDocumentFragment()
|
||||
let undoneElements = document.createDocumentFragment()
|
||||
|
||||
//Go through each item in each list we found
|
||||
node.childNodes.forEach( (checkListItem, index) => {
|
||||
|
||||
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||
if(checkListItem.nodeName == 'UL'){
|
||||
return
|
||||
}
|
||||
|
||||
//Check if list item has active class
|
||||
const checkedItem = checkListItem.classList.contains('active')
|
||||
|
||||
//Check if the next item is a list, Keep lists with intented items together
|
||||
let sublist = null
|
||||
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||
sublist = node.childNodes[index+1]
|
||||
}
|
||||
|
||||
//Push checked items and their sub lists to the done set
|
||||
if(checkedItem){
|
||||
|
||||
doneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
doneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//Remove all HTML from node, push unfinished items, then finished below them
|
||||
node.innerHTML = null
|
||||
node.appendChild(undoneElements)
|
||||
node.appendChild(doneElements)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
},600)
|
||||
|
||||
|
||||
},
|
||||
calculateMath(){
|
||||
//
|
||||
// Find math in note and calculate the outcome
|
||||
//
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
this.$router.go(-1)
|
||||
|
||||
// simple function that trys to evaluate javascript
|
||||
const shittyMath = (string) => {
|
||||
//Remove all chars but math chars
|
||||
const cleanString = String(string).replace(/[a-zA-Z\s]*/g,'')
|
||||
try {
|
||||
return Function('"use strict"; return (' + cleanString + ')')();
|
||||
} catch (error) {
|
||||
console.log('Math Error: ', string)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
|
||||
const line = node.innerText.trim()
|
||||
|
||||
// = sign exists and its the last character in the string
|
||||
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
|
||||
|
||||
//Pull out everything before the formula and try to evaluate it
|
||||
const formula = line.split('=').shift()
|
||||
const output = shittyMath(formula)
|
||||
|
||||
//If its a number and didn't throw an error, update the line
|
||||
if(!isNaN(output) && output != null){
|
||||
|
||||
//Since there is HTML in the line, splice in the number after the = sign
|
||||
let equalLocation = node.innerHTML.indexOf('=')
|
||||
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
|
||||
newLine += ` ${output}`
|
||||
newLine += node.innerHTML.slice(equalLocation+1).trim()
|
||||
|
||||
//Slam in that new HTML with the output
|
||||
node.innerHTML = newLine
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
},600)
|
||||
|
||||
|
||||
|
||||
},
|
||||
setText(inText){
|
||||
|
||||
|
||||
|
||||
this.editor.setHTML(inText)
|
||||
// this.noteText = this.editor._getHTML()
|
||||
// this.diffNoteText = this.editor._getHTML()
|
||||
|
||||
//Make sure all list items have draggable property
|
||||
// let container = document.getElementById('squire-id')
|
||||
// let listItems = container.getElementsByTagName('li')
|
||||
// for(let itemIndex in listItems){
|
||||
// // console.log(listItems[itemIndex])
|
||||
// // listItems[itemIndex].setAttribute('draggable','true')
|
||||
// }
|
||||
// // console.log(listItems)
|
||||
|
||||
},
|
||||
getText(){
|
||||
|
||||
return this.editor.getHTML()
|
||||
},
|
||||
insertDivide(){
|
||||
|
||||
this.editor.insertHTML(`<p><div class='divide'></div><br></p>`)
|
||||
this.editor.focus()
|
||||
this.editor.moveCursorToEnd()
|
||||
},
|
||||
insertTable(tall, wide){
|
||||
console.log(`Table: ${wide} x ${tall}`)
|
||||
|
||||
//Insert a table
|
||||
let tableSyntax = '<div>'
|
||||
tableSyntax += '<table>'
|
||||
for (let i = 0; i < tall; i++) {
|
||||
tableSyntax += '<tr>'
|
||||
for (let j = 0; j < wide; j++) {
|
||||
tableSyntax += '<td><p><br></p></td>'
|
||||
}
|
||||
tableSyntax += '</tr>'
|
||||
}
|
||||
tableSyntax += '</table></div><p><br></p>'
|
||||
|
||||
this.editor.insertHTML(tableSyntax)
|
||||
this.editor.focus()
|
||||
this.editor.moveCursorToEnd()
|
||||
|
||||
this.$router.go(-1)
|
||||
},
|
||||
indentText(){
|
||||
|
||||
// Lists use increase list level, increase quote breaks numbering
|
||||
if(this.activeList || this.activeToDo){
|
||||
|
||||
this.editor.increaseListLevel()
|
||||
return
|
||||
}
|
||||
this.editor.increaseQuoteLevel()
|
||||
},
|
||||
outdentText(){
|
||||
|
||||
// Lists use increase list level, increase quote breaks numbering
|
||||
if(this.activeList || this.activeToDo){
|
||||
|
||||
this.editor.decreaseListLevel()
|
||||
return
|
||||
}
|
||||
this.editor.decreaseQuoteLevel()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default SquireButtonFunctions
|
||||
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="ui basic segment no-fluf-segment" ref="content">
|
||||
<div class="ui stacking grid">
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h2 class="ui header">
|
||||
<i class="folder open outline icon"></i>
|
||||
<div class="content">
|
||||
Files
|
||||
<div class="sub header">Uploaded Files and Websites from notes.</div>
|
||||
<div class="sub header">
|
||||
<i class="green angle double up icon icon"></i>
|
||||
<router-link
|
||||
to="/bookmarklet">
|
||||
Push any website to solid scribe
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<!-- subnav -->
|
||||
<router-link
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments">
|
||||
<i class="open folder outline icon"></i>
|
||||
All
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['linkFiles']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/links">
|
||||
<i class="linkify icon"></i>
|
||||
Links
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['otherFiles']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/files">
|
||||
<i class="copy icon"></i>
|
||||
Other Files
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['archivedNotes']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/archived">
|
||||
<i class="archive icon"></i>
|
||||
Archived
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['trashedNotes']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/trashed">
|
||||
<i class="trash icon"></i>
|
||||
Trashed
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="$store.getters.totals && $store.getters.totals['sharedToNotes']"
|
||||
exact-active-class="green"
|
||||
class="ui basic button shrinking"
|
||||
to="/attachments/type/shared">
|
||||
<i class="send icon"></i>
|
||||
Show Shared
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="searchParams.noteId">
|
||||
<router-link class="ui green button" to="/attachments">
|
||||
<i class="chevron circle left icon"></i>
|
||||
Back to All
|
||||
</router-link>
|
||||
<div class="ui green button" v-on:click="openNote">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
|
||||
<i v-if="loading" class="green sync alternate loading icon"></i>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="searchParams['noteId'] && attachments.length == 0">
|
||||
<h3>There are no attachments for this note.</h3>
|
||||
<h3>Attachments are links or files added to the note.</h3>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<attachment-display
|
||||
v-for="item in attachments"
|
||||
:item="item"
|
||||
:key="item.id"
|
||||
:search-params="searchParams"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
loading: false,
|
||||
attachments: [],
|
||||
searchParams: {},
|
||||
loadedAttachmentsOffset: 0,
|
||||
loadingBatchTimeout: null,
|
||||
allLoaded: false,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
//
|
||||
// Perform Login check
|
||||
//
|
||||
this.$root.loginGateway()
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
//Load more attachments on scroll
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.on('update_note_attachments', () => {
|
||||
this.reset()
|
||||
this.searchAttachments()
|
||||
})
|
||||
|
||||
//Mount notes on load if note ID is set
|
||||
this.searchAttachments()
|
||||
},
|
||||
beforeUnmount(){
|
||||
|
||||
//Remove scroll event on destroy
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
|
||||
this.$io.removeListener('update_note_attachments')
|
||||
},
|
||||
watch:{
|
||||
$route (to, from){
|
||||
|
||||
//Reset everything on route change
|
||||
this.reset()
|
||||
//Params are handled by search function
|
||||
this.searchAttachments()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openNote(){
|
||||
const noteId = this.searchParams['noteId']
|
||||
this.$router.push('/notes/open/'+noteId)
|
||||
},
|
||||
onScroll(){
|
||||
|
||||
clearTimeout(this.loadingBatchTimeout)
|
||||
this.loadingBatchTimeout = setTimeout(() => {
|
||||
|
||||
//Distance to bottom of page
|
||||
const bottomOfWindow =
|
||||
Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
|
||||
+ window.innerHeight
|
||||
|
||||
//height of page
|
||||
const offsetHeight = this.$refs.content.clientHeight
|
||||
|
||||
//Determine percentage down the page
|
||||
const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
|
||||
|
||||
//If greater than 80 of the way down the page, load the next batch
|
||||
if(percentageDown >= 80){
|
||||
|
||||
this.searchAttachments()
|
||||
}
|
||||
|
||||
}, 50)
|
||||
},
|
||||
reset(){
|
||||
this.attachments = []
|
||||
this.loading = false
|
||||
this.allLoaded = false
|
||||
this.loadedAttachmentsOffset = 0
|
||||
},
|
||||
searchAttachments (){
|
||||
|
||||
if(this.loading || this.allLoaded){
|
||||
return
|
||||
}
|
||||
|
||||
delete this.searchParams.attachmentType
|
||||
delete this.searchParams.noteId
|
||||
|
||||
//Set attchment type if in URL
|
||||
if(this.$route.params.type){
|
||||
this.searchParams.attachmentType = this.$route.params.type
|
||||
}
|
||||
|
||||
// include files from shared notes or selected notes
|
||||
this.searchParams.includeShared = false
|
||||
if(this.$route.params.type == 'shared'){
|
||||
this.searchParams.includeShared = true
|
||||
}
|
||||
|
||||
//Set noteId in if in URL
|
||||
if(this.$route.params.id){
|
||||
this.searchParams.noteId = this.$route.params.id
|
||||
}
|
||||
|
||||
//Offset of attchments to load
|
||||
this.searchParams.offset = this.loadedAttachmentsOffset
|
||||
|
||||
if(this.allLoaded){
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
axios.post('/api/attachment/search', this.searchParams)
|
||||
.then( results => {
|
||||
|
||||
this.loading = false
|
||||
|
||||
if(results.data.length == 0){
|
||||
this.allLoaded = true
|
||||
return
|
||||
}
|
||||
|
||||
//Load up the results
|
||||
this.attachments.push(...results.data)
|
||||
|
||||
//Grab the next batch
|
||||
this.loadedAttachmentsOffset += results.data.length
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.attachment-display-area {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 5%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="text-container squire-box">
|
||||
|
||||
<h2 class="ui header">
|
||||
<i class="green angle double up icon icon"></i>
|
||||
<div class="content">
|
||||
Push URL to Solid Scribe - Bookmarklet
|
||||
<div class="sub header">Push any website to your file list.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<p>A bookmarklet is a small piece of code that can be run from a bookmark.</p>
|
||||
<p>Use the bookmarklet below to push URLs of website to solid scribe for later</p>
|
||||
<p>The bookmarklet works in a secure way and won't leak any data.</p>
|
||||
<p>To install the bookmarklet, all you need to do is drag it to your bookmarks bar.</p>
|
||||
|
||||
<h2>
|
||||
Drag the link below to your bookmarks.
|
||||
</h2>
|
||||
<h3>
|
||||
<a :href="`${(bookmarkletscript)}`" class="ui huge text">Push to SolidScribe</a>
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
loading: true,
|
||||
bookmarkletscript:'',
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
// Perform Login check
|
||||
this.$root.loginGateway()
|
||||
|
||||
},
|
||||
mounted: function(){
|
||||
this.getBookmarklet()
|
||||
},
|
||||
beforeUnmount(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
getBookmarklet(){
|
||||
|
||||
this.loading = true
|
||||
axios.post('/api/attachment/getbookmarklet')
|
||||
.then( results => {
|
||||
|
||||
this.bookmarkletscript = results.data
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to get bookmarklet') })
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,298 @@
|
||||
<style>
|
||||
.calendar {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
display: inline-block;
|
||||
}
|
||||
.day {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--border_color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.today {
|
||||
font-weight: bold;
|
||||
}
|
||||
.day ~ .active {
|
||||
|
||||
background-color: var(--main-accent);
|
||||
color: var(--body_bg_color);
|
||||
}
|
||||
.day ~ .has-data {
|
||||
outline: #07f4f4;
|
||||
outline-style: none;
|
||||
outline-width: medium;
|
||||
outline-style: none;
|
||||
outline-offset: -4px;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="squire-box">
|
||||
|
||||
<h3>Cycle Tracking Beta
|
||||
<span v-on:click="deleteData()">
|
||||
<i class="clickable trash icon"></i>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div class="calendar">
|
||||
<p>{{calendar.monthName}}, {{calendar.year}}</p>
|
||||
<div v-for="day in calendar.weekdays" class="day">
|
||||
{{ day }}
|
||||
</div>
|
||||
<div v-for="day in calendar.days" class="day"
|
||||
:class="{
|
||||
'today':day == calendar.today,
|
||||
'active':dateCode == `${day}.${calendar.month}.${calendar.year}`,
|
||||
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
|
||||
}"
|
||||
v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)">
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui form">
|
||||
<h3><input type="text" v-model="dateCode" v-on:keypress.enter="openDayData(dateCode)"></h3>
|
||||
<div class="field" v-for="field in fields">
|
||||
<label>{{ field.label }}</label>
|
||||
|
||||
<div v-for="(value, key) in openDay">
|
||||
|
||||
<!-- float -->
|
||||
<div v-if="field.type == 'float' && field.id == key" class="ui left icon input">
|
||||
<input type="text" :placeholder="field.label" v-on:keyup="e => saveField(field.id, e.target.value)" :value="value">
|
||||
<i class="thermometer half icon"></i>
|
||||
</div>
|
||||
|
||||
<!-- range -->
|
||||
<div v-if="field.type == 'range' && field.id == key">
|
||||
<div :class="{green:(value == 1)}" v-on:click="saveField(field.id, 1)" class="ui button">1</div>
|
||||
<div :class="{green:(value == 2)}" v-on:click="saveField(field.id, 2)" class="ui button">2</div>
|
||||
<div :class="{green:(value == 3)}" v-on:click="saveField(field.id, 3)" class="ui button">3</div>
|
||||
<div :class="{green:(value == 4)}" v-on:click="saveField(field.id, 4)" class="ui button">4</div>
|
||||
<div :class="{green:(value == 5)}" v-on:click="saveField(field.id, 5)" class="ui button">5</div>
|
||||
</div>
|
||||
|
||||
<!-- text area -->
|
||||
<div v-if="field.type == 'text' && field.id == key">
|
||||
<textarea rows="3" v-on:keyup="e => saveField(field.id, e.target.value)">
|
||||
{{ value }}
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ui green save button" v-on:click="saveDayData">
|
||||
Save
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'CycleTracking',
|
||||
components: {
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
appWorkingDate: null,
|
||||
fields:[],
|
||||
defaultFields:[
|
||||
{'type':'float','label':'Basil Temp', 'id':'BT'},
|
||||
{'type':'range','label':'Cervical Mucus', 'id':'CM'},
|
||||
{'type':'text','label':'Notes', 'id':'NO'},
|
||||
],
|
||||
cycleData: {},
|
||||
dateCode: null,
|
||||
openDay: [],
|
||||
calendar: {
|
||||
monthName: '',
|
||||
month: '',
|
||||
year: '',
|
||||
days: [],
|
||||
weekdays: ['S','M','T','W','T','F','S'],
|
||||
today: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
|
||||
// Perform Login check
|
||||
this.$root.loginGateway()
|
||||
|
||||
},
|
||||
mounted(){
|
||||
if(!this.$store.getters.getLoggedIn){
|
||||
return
|
||||
}
|
||||
|
||||
// setup date code
|
||||
// day - month - year
|
||||
const now = new Date()
|
||||
this.appWorkingDate = now
|
||||
const dateSetup = [
|
||||
now.getDate(), // 1-31 (Day)
|
||||
now.getMonth()+1, // 0-11 (Month)
|
||||
now.getFullYear(), // 1888-2022 (Year)
|
||||
]
|
||||
|
||||
this.dateCode = dateSetup.join('.')
|
||||
|
||||
this.setupCalendar(this.appWorkingDate)
|
||||
|
||||
this.fetchCycleData()
|
||||
},
|
||||
methods: {
|
||||
saveField(fieldId, value){
|
||||
this.openDay[fieldId] = value
|
||||
},
|
||||
openDayData(dateCode){
|
||||
|
||||
if(String(dateCode).split('.').length != 3){
|
||||
console.log('Invalid Date Code')
|
||||
return
|
||||
}
|
||||
|
||||
this.dateCode = dateCode || this.dateCode
|
||||
|
||||
let currentDay = this.cycleData[this.dateCode] || {}
|
||||
|
||||
//Set up each entry empty or with current value
|
||||
this.fields.forEach(field => {
|
||||
currentDay[field.id] = currentDay[field.id] || ''
|
||||
})
|
||||
|
||||
this.openDay = currentDay
|
||||
|
||||
},
|
||||
saveDayData(){
|
||||
|
||||
// remove empty keys
|
||||
let cleanDayData = {}
|
||||
Object.keys(this.openDay).forEach(key => {
|
||||
if(this.openDay[key] != ''){
|
||||
cleanDayData[key] = this.openDay[key]
|
||||
}
|
||||
})
|
||||
|
||||
this.cycleData[this.dateCode] = cleanDayData
|
||||
|
||||
// Update calendar
|
||||
this.setupCalendar(this.appWorkingDate)
|
||||
|
||||
this.saveCycleData()
|
||||
|
||||
},
|
||||
fetchCycleData(){
|
||||
|
||||
axios.post('/api/cycle-tracking/get')
|
||||
.then(({ data }) => {
|
||||
if(data.hasOwnProperty("text")){
|
||||
|
||||
let appData = null
|
||||
try {
|
||||
appData = JSON.parse(data.text)
|
||||
} catch(e) {
|
||||
console.log('Didnt parse json')
|
||||
}
|
||||
|
||||
console.log(appData)
|
||||
|
||||
this.cycleData = appData?.cycleData || {}
|
||||
this.fields = appData?.fields || []
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.setupFields()
|
||||
this.openDayData(this.dateCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', error) })
|
||||
|
||||
},
|
||||
saveCycleData(){
|
||||
|
||||
const appData = JSON.stringify({
|
||||
fields: this.fields,
|
||||
cycleData: this.cycleData,
|
||||
})
|
||||
axios.post('/api/cycle-tracking/save', { cycleData:appData })
|
||||
.then(response => {
|
||||
{ this.$bus.$emit('notification', 'Data Saved') }
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', error) })
|
||||
},
|
||||
deleteData(){
|
||||
axios.post('/api/cycle-tracking/save', { cycleData:'' })
|
||||
.then(response => {
|
||||
{ this.$bus.$emit('notification', 'Data Deleted') }
|
||||
})
|
||||
},
|
||||
setupFields(){
|
||||
|
||||
// push the first 3 default fields to users set
|
||||
if(this.fields.length == 0){
|
||||
for (let i = 0; i < 3; i++) {
|
||||
this.fields.push(this.defaultFields[i])
|
||||
}
|
||||
}
|
||||
},
|
||||
setupCalendar(date){
|
||||
// ------------
|
||||
// setup calendar display
|
||||
var y = date.getFullYear()
|
||||
var m = date.getMonth()
|
||||
|
||||
var firstDay = new Date(y, m, 1);
|
||||
var lastDay = new Date(y, m + 1, 0);
|
||||
|
||||
function getDaysInMonth(year, month) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
const currentYear = date.getFullYear();
|
||||
const currentMonth = date.getMonth() + 1;
|
||||
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
|
||||
this.calendar.year = currentYear
|
||||
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
|
||||
|
||||
const monthStartDay = firstDay.getDay()
|
||||
let days = Array(monthStartDay).fill("."); // Pad days to start on correct weekday
|
||||
for (let i = 0; i < daysInCurrentMonth; i++) {
|
||||
days.push(i+1)
|
||||
}
|
||||
this.calendar.days = days
|
||||
|
||||
// set today
|
||||
this.calendar.today = date.getDate()
|
||||
this.calendar.month = date.getMonth()+1
|
||||
|
||||
|
||||
/*
|
||||
October 2022
|
||||
S M T W T F S
|
||||
1 2 3 4 5 6
|
||||
7 8 9
|
||||
*/
|
||||
|
||||
// -------
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="text-container squire-box">
|
||||
|
||||
<h3 class="ui dividing header">
|
||||
<i class="green question circle icon"></i>
|
||||
Help
|
||||
</h3>
|
||||
|
||||
<p><u>Encrypted Data</u><br></p><blockquote><p>Note text, note title and the note search index is fully encrypted. <br></p><p><br></p><p>Encryption is the transformation of data into a form unreadable by anyone without the password. Its purpose is to ensure privacy by keeping the information hidden from anyone for whom it is not intended, even those who can see the encrypted data. If every note in the database was leaked, nothing would be readable. If the government asked for your notes, it would all be gibberish. <br></p></blockquote><p><br></p><p><u>Some Data is not encrypted</u><br></p><blockquote><p>To keep up ease of use, your Username, Files, Tags, Attachments and attachment search text is not encrypted.<br></p></blockquote><p><br></p><p><u>Searching</u><br></p><blockquote><p>Note text searching is limited to key words.<br></p><p>To allow for higher security, the complexity of the search index has been reduced. To make sure that every word of every note is fully encrypted, the search index is encrypted as well. <br></p></blockquote><p><br></p><p><u>Files</u><br></p><blockquote><p>The files section will show files that have been uploaded to notes or web pages that have been scraped from notes.<br></p><p><br></p><p>The text next to each file can be modified and found later with the search bar. <br></p><p>Files are limited to 5MB in size. <br></p><p><br></p><p>Files can be uploaded to notes. If its an image, the picture will be put into the note.<br></p><p>Images added to notes will have text scanned, so it can be searched (This isn't super accurate so don't rely to heavily on it.) The text can be updated at any time and searched later.<br></p></blockquote><p><br></p><p><u>Scratch Pad</u><br></p><blockquote><p>The Scratch Pad was designed to allow rapid input to a single note. Rather than junking up all your notes with random links, numbers or haikus, you can put them all in one place. <br></p></blockquote><p><br></p><p><u>Dark Theme</u><br></p><blockquote><p>Dark theme limits the amount of blue emitted by your screen. Less blue light at night is supposed to be helpful for falling asleep.<br></p><p>Here is some good research on the topic: <a href="https://justgetflux.com/research.html">https://justgetflux.com/research.html</a><br></p></blockquote><p><br></p><p><u>Text Editor Keyboard Shortcuts</u><br></p><blockquote><ul><li><b>Number List</b> - CTRL + SHIFT + 9<br></li><li><b>Todo List</b> - CTRL + SHIFT + 8<br></li><li><b>Underline</b> - CTRL + u<br></li><li><b>Bold</b> - CTRL + b<br></li><li><b>Quote</b> - CRTL + i<br></li><li><b>Undo</b> - CTRL + z<br></li><li><b>Redo</b> - CTRL + y<br></li><li><b>Indent</b> - TAB<br></li><li><b>Un-Indent</b> - SHIFT + TAB<br></li><li><b>Manual Save</b> - CTRL + S<br></li><li><b>Close Note</b> - ESC<br></li></ul></blockquote><p><br></p><p><u>Shared Notes</u><br></p><blockquote><p>Shared notes still respect privacy but use a different security scheme. Instead of encrypting the note with your password, a shared password is created. This note then uses <a href="https://en.wikipedia.org/wiki/Public-key_cryptography">public-key cryptography</a> to share the note with other users, while still making it unreadable to anyone else. <br></p><ul><li>You can revoke any users access to a shared note at any time.<br></li><li>Notes you share can not be shared by other people.<br></li><li>If you turn off sharing, all users lose access and the note is reverted to the default security scheme.<br></li></ul><p><br></p><p>It is possible to generate a public URL for shared notes. This exposes the Encryption Key for the note. Anyone with the key to the note can read that note. The encryption key to a shared note can easily be changed by turning off sharing, then turning it back on. <br></p><p><u><br></u></p></blockquote><p><u>Links in notes</u><br></p><blockquote><p>Links put into notes are automatically scraped. This means the data from the link will be scanned to get an image and some text from the website to help make that link more accessible in the future. <br></p><p>You can edit the text of scarped links and any time and search for it later. <br></p></blockquote><p><br></p><p><u>Deleting Notes</u><br></p><blockquote><p>Deleting a note from the trash erases all note text and the note entry. It can not be recovered.<br></p><p>None of the files related to the note are deleted. They can be manually removed at any time.<br></p></blockquote><p><br></p><p><u>Daily Backups</u><br></p><blockquote><p>All notes are backed up, every night, at midnight. If there is data loss, it can be restored from a backup. If you experience some sort of cataclysmic data loss please contact the system administrator for a copy of your data or a restoration procedure. <br></p><p>All data in the backups remains encrypted and unreadable.<br></p></blockquote><p><br></p><p><u>Two Factor Authentication (2FA)</u><br></p><blockquote><p>Two factor authentication is an additional layer of security on top of your regLine truncated
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="center aligned sixteen wide column">
|
||||
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelpPage',
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,622 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.hero {
|
||||
background-size: 50%;
|
||||
background-color: #0a2f13;
|
||||
background: linear-gradient(270deg, #21ba45, #3710a4);
|
||||
background-size: 400% 400%;
|
||||
overflow: hidden;
|
||||
|
||||
-webkit-animation: fadeorama 16s ease infinite;
|
||||
-moz-animation: fadeorama 16s ease infinite;
|
||||
animation: fadeorama 16s ease infinite;
|
||||
height: 350px;
|
||||
|
||||
text-shadow:
|
||||
1px 1px 1px rgba(69,69,69,0.1),
|
||||
-1px -1px 1px rgba(69,69,69,0.1),
|
||||
-1px 1px 1px rgba(69,69,69,0.1),
|
||||
1px -1px 1px rgba(69,69,69,0.1)
|
||||
;
|
||||
}
|
||||
.shine {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: none;
|
||||
}
|
||||
.spotlight {
|
||||
background: rgba(255,255,255,0);
|
||||
background: radial-gradient(circle at bottom, var(--main-accent) 0%, rgba(255,255,255,0) 60%);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.logo-display {
|
||||
width: 140px;
|
||||
height: auto;
|
||||
}
|
||||
.lightly-padded {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.massive-text {
|
||||
color: white;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
div#app div.lightly-padded.home-main div.ui.centered.vertically.divided.stackable.grid div.row.hero.fadeBg div.sixteen.wide.middle.aligned.center.column h2.massive-text svg.logo-display path {
|
||||
stroke: black !important;
|
||||
stroke-width: 1px !important;
|
||||
}
|
||||
.blinking {
|
||||
animation:blinkingText 1.5s linear infinite;
|
||||
}
|
||||
@keyframes blinkingText {
|
||||
0%{ opacity: 0.9; }
|
||||
50%{ opacity: 0; }
|
||||
100%{ opacity: 0.9; }
|
||||
}
|
||||
.subtext {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
.stand-out {
|
||||
color: white;
|
||||
text-shadow:
|
||||
2px 2px 1px black,
|
||||
-2px -2px 1px black,
|
||||
-2px 2px 1px black,
|
||||
2px -2px 1px black;
|
||||
}
|
||||
h2, h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes fadeorama {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@-moz-keyframes fadeorama {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
@keyframes fadeorama {
|
||||
0%{background-position:0% 50%}
|
||||
50%{background-position:100% 50%}
|
||||
100%{background-position:0% 50%}
|
||||
}
|
||||
|
||||
/*safari fix - prevents page from being below the menu */
|
||||
.green-text {
|
||||
color: #3710a4;
|
||||
}
|
||||
.dont-pad-me {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.home-main img {
|
||||
max-height: 250px;
|
||||
}
|
||||
.white-link {
|
||||
text-decoration: underline;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 740px) {
|
||||
.column > img {
|
||||
max-height: 125px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="lightly-padded home-main">
|
||||
<div class="ui centered vertically divided stackable grid">
|
||||
|
||||
<div class="row hero fadeBg" :style="{ 'height':(height+'px') }">
|
||||
|
||||
<!-- <div class="one wide large screen only column"></div> -->
|
||||
|
||||
<!-- desktop column - large screen only -->
|
||||
<div class="sixteen wide middle aligned center aligned column" style="z-index: 500;">
|
||||
|
||||
<h2 class="massive-text">
|
||||
<!-- <img class="logo-display" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo"> -->
|
||||
<logo class="logo-display" color="var(--main-accent)" stroke="true" />
|
||||
<br>
|
||||
Solid Scribe
|
||||
</h2>
|
||||
|
||||
<h3 class="subtext">
|
||||
A free, secure, online note taking application<i class="i cursor icon blinking"></i>
|
||||
</h3>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div class="eight wide middle aligned left aligned column">
|
||||
<img loading="lazy" width="90%" src="/api/static/assets/marketing/notebook.svg" alt="The Venus fly laptop about to capture another victim">
|
||||
</div> -->
|
||||
|
||||
<div v-for="i in jewelFacets" class="shine" :style="shineStyle(i)" v-bind:key="i"></div>
|
||||
|
||||
<div class="shine spotlight"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- All marketing images if you need to review -->
|
||||
<div v-if="false" class="sixteen wide column">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/add.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg">
|
||||
</div>
|
||||
|
||||
<!-- Go to notes button -->
|
||||
<div class="row" v-if="$root.loggedIn">
|
||||
<div class="sixteen wide middle algined center aligned column">
|
||||
<h3>You are already logged in</h3>
|
||||
<router-link class="ui huge green labeled icon button" to="/notes">
|
||||
<i class="external alternate icon"></i>Go to Notes
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Small login form -->
|
||||
<div class="row" v-if="!$root.loggedIn">
|
||||
<div class="sixteen wide middle algined column">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
<i class="plug icon"></i>
|
||||
Sign Up Now - Only a Username and Password required
|
||||
</h2>
|
||||
<login-form :thin="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2 class="ui dividing header">Powerful text editing and privacy</h2>
|
||||
<h3>Easily edit, share and organize thousands of notes.</h3>
|
||||
<h3>Feel safe knowing no one can read your notes but you.</h3>
|
||||
<!-- <h3>Tools to organize and collaborate on thousands of notes while maintaining security and respecting your privacy.</h3> -->
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<svg-displayer file="idea" alt="Explosion of New Ideas" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- theme selector -->
|
||||
<div class="ui white row">
|
||||
<div class="sixteen wide middle aligned column">
|
||||
<div class="ui container">
|
||||
<h2 style="color: var(--main-accent);">
|
||||
Pick your theme
|
||||
</h2>
|
||||
<h3 v-if="$root.loggedIn">Go to settings to change theme</h3>
|
||||
<div
|
||||
v-for="color in themeColors"
|
||||
v-bind:key="color"
|
||||
class="ui small basic button"
|
||||
:style="`background: linear-gradient(0deg, ${color} 4%, rgba(0,0,0,0) 5%);`"
|
||||
v-on:click="setAccentColor(color)">
|
||||
<logo style="width: 20px; height: auto;" :color="color" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- features list -->
|
||||
<div class="top aligned centered row">
|
||||
|
||||
<!-- note features -->
|
||||
<div class="six wide column">
|
||||
|
||||
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>App Features</h1>
|
||||
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey sticky note icon"></i>
|
||||
<i class="bottom left corner teal plus icon"></i>
|
||||
</i>
|
||||
Create a million notes!
|
||||
<div class="sub header">Create unlimited notes up to 5,000,000 characters long.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey tags icon"></i>
|
||||
<i class="bottom left corner purple plus icon"></i>
|
||||
</i>
|
||||
Tag Notes
|
||||
<div class="sub header">Add and edit tags on notes then search or sort by tag.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey search icon"></i>
|
||||
<i class="bottom left corner orange font icon"></i>
|
||||
</i>
|
||||
Search Note Text
|
||||
<div class="sub header">Search all notes, files, links and tags.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey search icon"></i>
|
||||
<i class="bottom left corner pink paperclip icon"></i>
|
||||
</i>
|
||||
Search Attachments
|
||||
<div class="sub header">Add text to Images and links than can be searched.</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey cloud moon icon"></i>
|
||||
<i class="bottom left corner red eye icon"></i>
|
||||
</i>
|
||||
Night Mode
|
||||
<div class="sub header">Pure black night theme with an even darker flux theme.</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- editing features -->
|
||||
<div class="six wide column">
|
||||
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>Editing Features</h1>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey list icon"></i>
|
||||
<i class="bottom left corner green check icon"></i>
|
||||
</i>
|
||||
Create To Do Lists
|
||||
<div class="sub header">Create To Do lists that are always synced, work on mobile and can be sorted.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey file icon"></i>
|
||||
<i class="bottom left corner blue pen icon"></i>
|
||||
</i>
|
||||
Formatting Tools
|
||||
<div class="sub header">Bold, Underline, Title, Add Links, Add Tables, Color Text, Color Background and more.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey file icon"></i>
|
||||
<i class="bottom left corner orange paint brush icon"></i>
|
||||
</i>
|
||||
Customized Colorful Notes
|
||||
<div class="sub header">Color the background of notes and add colored icons to make them stand out.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey images icon"></i>
|
||||
<i class="bottom left corner teal paperclip icon"></i>
|
||||
</i>
|
||||
Add Images
|
||||
<div class="sub header">Upload images to notes, add search text to the images to find them later.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey users icon"></i>
|
||||
<i class="bottom left corner olive exchange icon"></i>
|
||||
</i>
|
||||
Collaborative Note Editing
|
||||
<div class="sub header">Notes instantly update in real time everywhere its open and anywhere its shared.</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<!-- privacy features -->
|
||||
<div class="six wide column">
|
||||
<h1 class="ui center aligned dividing header"><i class="small green sliders horizontal icon"></i>Privacy Features</h1>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey lock icon"></i>
|
||||
<i class="bottom left corner yellow key icon"></i>
|
||||
</i>
|
||||
Secure Notes
|
||||
<div class="sub header">All note text is encrypted. No one can read your notes. None of your data is shared.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey search icon"></i>
|
||||
<i class="bottom left corner orange font icon"></i>
|
||||
</i>
|
||||
Private Search
|
||||
<div class="sub header">Search the contents of all your notes without compromising security.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey share alternate icon"></i>
|
||||
<i class="bottom left corner share icon"></i>
|
||||
</i>
|
||||
Encrypted Sharing
|
||||
<div class="sub header">Shared notes are still encrypted, only readable by you and the shared users.</div>
|
||||
</div>
|
||||
</h2>
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
<i class="icons">
|
||||
<i class="grey tv icon"></i>
|
||||
<i class="bottom left corner blue mobile icon"></i>
|
||||
</i>
|
||||
Two Factor Authentication
|
||||
<div class="sub header">Enable two factor authentication for added peace of mind.</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="six wide column">
|
||||
<svg-displayer file="onboarding" alt="Observe this chart" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="secure" alt="So dang secure" />
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Only you can read your notes. </h2>
|
||||
<h3>Employees can not <a target="_blank" href="https://www.forbes.com/sites/zakdoffman/2019/01/30/facebook-has-just-been-caught-spying-on-users-private-messages-and-data-again/#1e27e00a31ce"> snoop your account</a>. No one can <a target="_blank" href="https://mashable.com/article/google-reading-your-emails-response/">read your data for advertising</a>. Notes are completely unreadable without your password.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered green row">
|
||||
<div class="six wide column">
|
||||
<h2>Extremely accessible - Nothing to install</h2>
|
||||
<h3>Works on mobile or desktop browsers. <br>Behaves like an installed app on mobile phones.</h3>
|
||||
</div>
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="cloud" alt="Girl falling into the spiral of digital chaos" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="four wide right aligned column">
|
||||
<svg-displayer file="robot" alt="Murder Robot in office environment" />
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Secure Data Sharing</h2>
|
||||
<h3>Share notes with friends without compromising privacy. The data remains encrypted with a shared password for you and people you invite to view it.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Leave your Ad Blockers turned on</h2>
|
||||
<h3>SolidScribe doesn't load any trackers or ads. It was designed to run on
|
||||
<a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>, with
|
||||
<a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/" target="_blank">an Ad Blocker</a> turned on. It even works with a
|
||||
<a href="https://pi-hole.net/" target="_blank">Pi-hole</a> on the network.</h3>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<svg-displayer file="icecream" alt="Emergence of a 4th dimensional being perceived as a large ice cream" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide right aligned column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Data Backups</h2>
|
||||
<h3>Nothing you do will be forgotten.<br>You can never take back what you have done</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide right aligned column">
|
||||
<h2>Freedom to unleash yourself</h2>
|
||||
<h3>Imagine an awakening of what could be</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/grandma.svg" alt="Drinking the blood of the elderly">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- final slide -->
|
||||
<div class="middle aligned centered green row">
|
||||
<div class="twelve wide center aligned column">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<h2>What are you waiting for?<br>Sign up now.</h2>
|
||||
<br>
|
||||
<router-link class="ui huge white labeled icon button" to="/login">
|
||||
<i class="plug icon"></i>Sign Me Up!
|
||||
</router-link>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="true" class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h3>
|
||||
<a target="_blank" href="https://www.maxg.cc">Solid Scribe was created by Max Gialanella</a>
|
||||
</h3>
|
||||
<p><a target="_blank" href="https://www.maxg.cc">Check out my Resume</a></p>
|
||||
<p>OR</p>
|
||||
<p><a target="_blank" href="http://blog.maxg.cc">Check out my Programming Blog</a></p>
|
||||
<p>
|
||||
I was tired of all my data being owned by big companies, having it farmed out for marketing, and leaving the contents of my life exposed to corporations.
|
||||
</p>
|
||||
<p>
|
||||
If you want to give it a shot, feel free to make an account. There are no ads. None of this data is shared or public. I don't make any money.
|
||||
</p>
|
||||
<p>
|
||||
If you see anything broken or want to see a feature implemented; I'm open to suggestions. <i class="thumbs up icon"></i>
|
||||
</p>
|
||||
<p>Email me at <a href="mailto:[email protected]">[email protected]</a></p>
|
||||
<p>If you want to help me out with hosting this application, I would love a small Bitcoin donation.</p>
|
||||
<p>
|
||||
<a href="https://btc3.trezor.io/address/3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG" target="_blank">
|
||||
<img loading="lazy" width="160px" src="/api/static/assets/marketing/wallet.png" alt="3QYnnNKnYTcU82F8NJ1BrmzGU2zRndTyEG">
|
||||
</a>
|
||||
</p>
|
||||
<p>Awesomely Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<svg-displayer file="watching" alt="Drinking the blood of the elderly" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center aligned sixteen wide column">
|
||||
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
export default {
|
||||
name: 'WelcomePage',
|
||||
components: {
|
||||
'login-form':defineAsyncComponent(() => import('@/components/LoginFormComponent.vue')),
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
'svg-displayer':defineAsyncComponent(() => import('@/components/SvgDisplayer.vue')),
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
height: null,
|
||||
realInformation: false,
|
||||
jewelFacets: 15,
|
||||
themeColors: [
|
||||
'#21BA45', //Green
|
||||
'#b5cc18', //Lime
|
||||
'#00b5ad', //Teal
|
||||
'#2185d0', //Blue
|
||||
'#7128b9', //Violet
|
||||
'#a333c8', //Purple
|
||||
'#e03997', //Pink
|
||||
'#db2828', //Red
|
||||
'#f2711c', //Orange
|
||||
'#fbbd08', //Yellow
|
||||
'#767676', //Grey
|
||||
'#303030', //Black-almost
|
||||
],
|
||||
}
|
||||
},
|
||||
beforeCreate(){
|
||||
//Force HTTPS on prod, always. Dev doesn't have certs
|
||||
// const isDev = process.env['NODE_ENV'] == 'development'
|
||||
// if (!isDev && location.protocol != 'https:'){
|
||||
// window.location.replace('https://www.solidscribe.com')
|
||||
// }
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
methods: {
|
||||
shineStyle(i){
|
||||
|
||||
const farMax = 95 //85
|
||||
const farMin = 83
|
||||
|
||||
const farOut = (Math.floor(Math.random() * (farMax - farMin + 1)) + farMin)
|
||||
|
||||
// const rotation = 360/this.jewelFacets
|
||||
const rotMax = 360/this.jewelFacets
|
||||
const rotMin = 320/this.jewelFacets
|
||||
const rotation = (Math.floor(Math.random() * (rotMax - rotMin + 1)) + rotMin)
|
||||
|
||||
let style = `
|
||||
background: linear-gradient(
|
||||
${(i+1)*(rotation)}deg,
|
||||
rgba(255,255,255,0) ${farOut}%,
|
||||
rgba(255,255,255,0.1) ${farOut+1}%,
|
||||
rgba(255,255,255,0.0) ${farOut+10}%
|
||||
)
|
||||
;`
|
||||
|
||||
// Remove whitespace - Make it 1 line
|
||||
return style.replace(/\s+/g, '')
|
||||
},
|
||||
setAccentColor(color){
|
||||
|
||||
let root = document.documentElement
|
||||
root.style.setProperty('--main-accent', color)
|
||||
localStorage.setItem('main-accent', color)
|
||||
|
||||
if(!color || color == '#21BA45'){
|
||||
localStorage.removeItem('main-accent')
|
||||
}
|
||||
},
|
||||
showRealInformation(){
|
||||
|
||||
|
||||
this.realInformation = !this.realInformation
|
||||
if(this.realInformation){
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.real.scrollIntoView({'behavior':'smooth'})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="ui basic segment no-fluf-segment">
|
||||
<div class="ui stackable grid">
|
||||
|
||||
<div class="sixteen wide center aligned column">
|
||||
<h2>Login / Register</h2>
|
||||
</div>
|
||||
|
||||
<div class="ui sixteen wide column">
|
||||
<div class="ui text container">
|
||||
|
||||
|
||||
<div class="ui segment">
|
||||
|
||||
<h4 class="ui header">
|
||||
<i class="plug icon"></i>
|
||||
<div class="content">
|
||||
To Register
|
||||
<div class="sub header">Choose Any Username & password</div>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
|
||||
<login-form />
|
||||
|
||||
</div>
|
||||
|
||||
<p>You will remain logged in on this browser, for 20 days or until you log out.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
'login-form':defineAsyncComponent(() => import('@/components/LoginFormComponent.vue')),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
enabled: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui text container">
|
||||
|
||||
<h2 class="ui dividing header">
|
||||
Page Not Found
|
||||
</h2>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotFoundPage',
|
||||
props:[ 'message' ],
|
||||
data () {
|
||||
return {
|
||||
items: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
|
||||
<div class="ui grid" ref="content">
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
|
||||
|
||||
<div class="ui stackable grid">
|
||||
|
||||
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
|
||||
<search-input />
|
||||
</div>
|
||||
|
||||
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
||||
|
||||
<div class="ui basic button shrinking"
|
||||
v-on:click="updateFastFilters(3)"
|
||||
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
|
||||
style="position: relative;">
|
||||
<i class="green mail icon"></i>Inbox
|
||||
<span class="tiny circular floating ui green label">+{{ $store.getters.totals['youGotMailCount'] }}</span>
|
||||
</div>
|
||||
|
||||
<tag-display
|
||||
:active-tags="searchTags"
|
||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||
/>
|
||||
|
||||
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()" v-if="$store.getters.totals && $store.getters.totals['totalNotes'] > 0">
|
||||
<i v-if="titleView" class="th icon"></i>
|
||||
<i v-if="!titleView" class="bars icon"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="eight wide column" v-if="showClear">
|
||||
<!-- <fast-filters /> -->
|
||||
<span class="ui fluid green button" @click="reset">
|
||||
<i class="arrow circle left icon"></i>Show All Notes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
|
||||
<div v-if="searchResultsCount == 0" class="sub header">
|
||||
Search can only find key words. Try a single word search.
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
|
||||
<h2>Archived Notes</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
|
||||
<h2>Trash
|
||||
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
|
||||
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
|
||||
<i class="poo storm icon"></i>
|
||||
Empty Trash
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
|
||||
<h2>Shared Notes</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
|
||||
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
|
||||
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
|
||||
<i class="tag icon"></i>
|
||||
{{ tag.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- found attachments -->
|
||||
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
||||
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
|
||||
<attachment-display
|
||||
v-for="item in foundAttachments"
|
||||
:item="item"
|
||||
:key="item.id"
|
||||
:search-params="{}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Note title card display -->
|
||||
<div class="sixteen wide column">
|
||||
|
||||
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
|
||||
No Notes Yet. <br>Thats ok.<br><br> <br>
|
||||
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
|
||||
Create one when you feel ready.
|
||||
</h3>
|
||||
|
||||
<!-- Go to one wide column, do not do this on mobile interface -->
|
||||
<div :class="{'one-column':( showOneColumn() )}">
|
||||
|
||||
<!-- render each section based on notes in set -->
|
||||
<template v-for="section,index in noteSections" :key="index">
|
||||
<div v-if="section.length > 0" class="note-card-section">
|
||||
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
|
||||
|
||||
<div class="note-card-display-area">
|
||||
<note-title-display-card
|
||||
v-on:tagClick="tagId => toggleTagFilter(tagId)"
|
||||
v-for="note in section"
|
||||
:ref="'note-'+note.id"
|
||||
:onClick="openNote"
|
||||
:data="note"
|
||||
:title-view="titleView"
|
||||
:currently-open="activeNoteId1 == note.id"
|
||||
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<loading-icon v-if="loadingInProgress" message="Decrypting Notes" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<note-input-panel
|
||||
v-if="activeNoteId1 != null"
|
||||
:key="activeNoteId1"
|
||||
:noteid="activeNoteId1"
|
||||
:url-data="$route.params"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'SearchBar',
|
||||
components: {
|
||||
|
||||
'note-input-panel': defineAsyncComponent(() => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue')),
|
||||
|
||||
'note-title-display-card': defineAsyncComponent(() => import('@/components/NoteTitleDisplayCard.vue')),
|
||||
// 'fast-filters': defineAsyncComponent(() => import('@/components/FastFilters.vue')),
|
||||
'search-input': defineAsyncComponent(() => import('@/components/SearchInput.vue')),
|
||||
'attachment-display': defineAsyncComponent(() => import('@/components/AttachmentDisplayCard.vue')),
|
||||
'counter':defineAsyncComponent(() => import('@/components/AnimatedCounterComponent.vue')),
|
||||
'tag-display':defineAsyncComponent(() => import('@/components/TagDisplayComponent.vue')),
|
||||
'loading-icon':defineAsyncComponent(() => import('@/components/LoadingIconComponent.vue')),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
initComponent: true,
|
||||
tagSuggestions:[],
|
||||
searchTerm: '',
|
||||
searchResultsCount: 0,
|
||||
searchTags: [],
|
||||
notes: [],
|
||||
highlights: [],
|
||||
searchDebounce: null,
|
||||
fastFilters: {},
|
||||
titleView: false,
|
||||
|
||||
//Load up notes in batches
|
||||
firstLoadBatchSize: 10, //First set of rapidly loaded notes
|
||||
batchSize: 25, //Size of batch loaded when user scrolls through current batch
|
||||
batchOffset: 0, //Tracks the current batch that has been loaded
|
||||
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
|
||||
loadingInProgress: false,
|
||||
scrollLoadEnabled: true,
|
||||
|
||||
//Clear button is not visible
|
||||
showClear: false,
|
||||
initialPostData: null,
|
||||
|
||||
//Currently open notes in app
|
||||
activeNoteId1: null,
|
||||
activeNoteId2: null,
|
||||
|
||||
//Position determines how note is Positioned
|
||||
activeNote1Position: 0,
|
||||
activeNote2Position: 0,
|
||||
|
||||
lastVisibilityState: null,
|
||||
|
||||
foundAttachments: [],
|
||||
|
||||
sectionData: {
|
||||
'pinned': ['thumbtack', 'Pinned'],
|
||||
'archived': ['archive', 'Archived'],
|
||||
'shared': ['envelope outline', 'Inbox'],
|
||||
'sent': ['paper plane outline', 'Sent Notes'],
|
||||
'notes': ['file','Notes'],
|
||||
'highlights': ['paragraph', 'Found In Text'],
|
||||
'trashed': ['poop', 'Trashed Notes'],
|
||||
'tagged': ['tag', 'Tagged'],
|
||||
},
|
||||
noteSections: {
|
||||
pinned: [],
|
||||
archived: [],
|
||||
shared:[],
|
||||
sent:[],
|
||||
notes: [],
|
||||
highlights: [],
|
||||
trashed: [],
|
||||
tagged:[],
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
this.$root.loginGateway()
|
||||
|
||||
this.$io.on('new_note_created', noteId => {
|
||||
|
||||
//Do not update note if its open
|
||||
if(this.activeNoteId1 != noteId){
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
this.updateSingleNote(noteId, false)
|
||||
}
|
||||
})
|
||||
|
||||
this.$io.on('note_attribute_modified', noteId => {
|
||||
//Do not update note if its open
|
||||
if(this.activeNoteId1 != noteId){
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
this.updateSingleNote(noteId, false)
|
||||
}
|
||||
})
|
||||
|
||||
//Update title cards when new note text is saved
|
||||
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||
|
||||
//Do not update note if its open
|
||||
if(this.activeNoteId1 != noteId){
|
||||
this.updateSingleNote(noteId, true)
|
||||
}
|
||||
})
|
||||
|
||||
this.$bus.$on('update_single_note', (noteId) => {
|
||||
//Do not update note if its open
|
||||
if(this.activeNoteId1 != noteId){
|
||||
this.updateSingleNote(noteId)
|
||||
}
|
||||
})
|
||||
|
||||
//Update totals for app
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
|
||||
//Close note event
|
||||
this.$bus.$on('close_active_note', ({noteId, modified}) => {
|
||||
|
||||
if(modified){
|
||||
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
|
||||
}
|
||||
|
||||
//A note has been closed
|
||||
if(this.$route.fullPath != '/notes'){
|
||||
this.$router.push('/notes')
|
||||
}
|
||||
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
//Focus and animate if modified
|
||||
this.updateSingleNote(noteId, modified)
|
||||
})
|
||||
|
||||
this.$bus.$on('note_deleted', (noteId) => {
|
||||
//Remove deleted note from set, its deleted
|
||||
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key].forEach( (note, index) => {
|
||||
if(note.id == noteId){
|
||||
this.noteSections[key].splice(index,1)
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.$bus.$on('update_fast_filters', filterIndex => {
|
||||
|
||||
this.updateFastFilters(filterIndex)
|
||||
})
|
||||
|
||||
//Event to update search from other areas
|
||||
this.$bus.$on('update_search_term', sentInSearchTerm => {
|
||||
this.searchTerm = sentInSearchTerm
|
||||
this.search(true, this.batchSize)
|
||||
.then( () => {
|
||||
|
||||
this.searchAttachments()
|
||||
|
||||
const postData = {
|
||||
'tagText':this.searchTerm.trim()
|
||||
}
|
||||
|
||||
this.tagSuggestions = []
|
||||
axios.post('/api/tag/suggest', postData)
|
||||
.then( response => {
|
||||
|
||||
this.tagSuggestions = response.data
|
||||
})
|
||||
|
||||
// return
|
||||
})
|
||||
})
|
||||
|
||||
//Reload page content - don't trigger if load is in progress
|
||||
this.$bus.$on('note_reload', () => {
|
||||
if(!this.loadingInProgress){
|
||||
this.reset()
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
|
||||
//Close notes when back button is pressed
|
||||
// window.addEventListener('hashchange', this.hashChangeAction)
|
||||
|
||||
//update note on visibility change
|
||||
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
|
||||
|
||||
},
|
||||
beforeUnmount(){
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
||||
|
||||
this.$bus.$off('note_reload')
|
||||
this.$bus.$off('close_active_note')
|
||||
// this.$bus.$off('update_single_note')
|
||||
this.$bus.$off('note_deleted')
|
||||
this.$bus.$off('update_fast_filters')
|
||||
this.$bus.$off('update_search_term')
|
||||
|
||||
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working
|
||||
// this.$off() // Remove all event listeners
|
||||
// this.$bus.$off()
|
||||
},
|
||||
mounted() {
|
||||
|
||||
//Open note on load if ID is set
|
||||
if(this.$route.params.id > 1){
|
||||
this.activeNoteId1 = this.$route.params.id
|
||||
}
|
||||
|
||||
//Loads initial batch and tags
|
||||
this.reset()
|
||||
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id': function(id){
|
||||
//Open note on ID, null id will close note
|
||||
this.activeNoteId1 = id
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleTitleView(){
|
||||
this.titleView = !this.titleView
|
||||
},
|
||||
showOneColumn(){
|
||||
|
||||
return this.$store.getters.getIsUserOnMobile
|
||||
|
||||
//If note 1 or 2 is open, show one column. Or if the user is on mobile
|
||||
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
|
||||
!this.$store.getters.getIsUserOnMobile
|
||||
},
|
||||
openNote(id, event = null){
|
||||
|
||||
//Don't open note if a link is clicked in display card
|
||||
if(event && event.target && event.target.nodeName){
|
||||
const nodeClick = event.target.nodeName
|
||||
if(nodeClick == 'A'){ return }
|
||||
}
|
||||
|
||||
//Open note if a link was not clicked
|
||||
this.$router.push('/notes/open/'+id)
|
||||
return
|
||||
},
|
||||
toggleTagFilter(tagId){
|
||||
|
||||
this.searchTags = [tagId]
|
||||
|
||||
//Reset note set and load up notes and tags
|
||||
if(this.searchTags.length > 0){
|
||||
this.search(true, this.batchSize)
|
||||
return
|
||||
}
|
||||
|
||||
//If no tags are selected, reset entire page
|
||||
this.reset()
|
||||
|
||||
},
|
||||
onScroll(e){
|
||||
|
||||
clearTimeout(this.loadingBatchTimeout)
|
||||
this.loadingBatchTimeout = setTimeout(() => {
|
||||
|
||||
//Detect distance scrolled down the page
|
||||
const scrolledDown = window.pageYOffset + window.innerHeight
|
||||
//Get height of div to properly detect scroll distance down
|
||||
const height = document.getElementById('app').scrollHeight
|
||||
|
||||
//Load if less than 500px from the bottom
|
||||
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled && !this.loadingInProgress){
|
||||
|
||||
this.search(false, this.batchSize, true)
|
||||
}
|
||||
|
||||
}, 30)
|
||||
|
||||
|
||||
return
|
||||
},
|
||||
visibiltyChangeAction(event){
|
||||
|
||||
//Fuck this shit, just use web sockets
|
||||
return
|
||||
|
||||
//@TODO - phase this out, update it via socket.io
|
||||
//If user leaves page then returns to page, reload the first batch
|
||||
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
|
||||
//Load initial batch, then tags, then other batch
|
||||
this.search(false, this.firstLoadBatchSize)
|
||||
.then( () => {
|
||||
// return
|
||||
})
|
||||
}
|
||||
|
||||
this.lastVisibilityState = document.visibilityState
|
||||
|
||||
},
|
||||
// @TODO Don't even trigger this if the note wasn't changed
|
||||
updateSingleNote(noteId, focuseAndAnimate = true){
|
||||
|
||||
noteId = parseInt(noteId)
|
||||
|
||||
//Find local note, if it exists; continue
|
||||
let note = null
|
||||
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
|
||||
note = this.$refs['note-'+noteId][0].note
|
||||
//Show that note is working on updating
|
||||
this.$refs['note-'+noteId][0].showWorking = true
|
||||
}
|
||||
|
||||
|
||||
//Lookup one note using passed in ID
|
||||
const postData = {
|
||||
searchQuery: this.searchTerm,
|
||||
searchTags: this.searchTags,
|
||||
fastFilters:{
|
||||
noteIdSet:[noteId]
|
||||
}
|
||||
}
|
||||
|
||||
//Note data must be fetched, then sorted into existing note data
|
||||
axios.post('/api/note/search', postData)
|
||||
.then(results => {
|
||||
|
||||
//Pull note data out of note set
|
||||
let newNote = results.data.notes[0]
|
||||
|
||||
if(newNote === undefined){
|
||||
return
|
||||
}
|
||||
|
||||
if(note && newNote){
|
||||
|
||||
//go through each prop and update it with new values
|
||||
Object.keys(newNote).forEach(prop => {
|
||||
note[prop] = newNote[prop]
|
||||
})
|
||||
|
||||
//Push new note to front if its modified or we want it to
|
||||
if( focuseAndAnimate || note.updated != newNote.updated ){
|
||||
|
||||
// Find note, in section, move to front
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key].forEach( (searchNote, index) => {
|
||||
if(searchNote.id == noteId){
|
||||
//Remove note from location and push to front
|
||||
this.noteSections[key].splice(index, 1)
|
||||
this.noteSections[key].unshift(note)
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.$nextTick( () => {
|
||||
//Trigger close animation on note
|
||||
this.$refs['note-'+noteId][0].justClosed()
|
||||
this.$refs['note-'+noteId][0].showWorking = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//New notes don't exist in list, push them to the front
|
||||
if(note == null){
|
||||
this.noteSections.notes.unshift(newNote)
|
||||
//Trigger close animation on note
|
||||
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
|
||||
this.$refs['note-'+noteId][0].justClosed()
|
||||
this.$refs['note-'+noteId][0].showWorking = false
|
||||
}
|
||||
}
|
||||
|
||||
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
|
||||
this.$refs['note-'+noteId][0].showWorking = false
|
||||
}
|
||||
|
||||
//Trigger section rebuild
|
||||
this.rebuildNoteCategorise()
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
this.$bus.$emit('notification', 'Failed to Update Note')
|
||||
})
|
||||
},
|
||||
searchAttachments(){
|
||||
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
|
||||
.then(results => {
|
||||
this.foundAttachments = results.data
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
|
||||
},
|
||||
search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Don't double load note batches
|
||||
if(this.loadingInProgress){
|
||||
console.log('Loading already in progress')
|
||||
return resolve(false)
|
||||
}
|
||||
|
||||
//Reset a lot of stuff if we are not merging batches
|
||||
if(!mergeExisting){
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key] = []
|
||||
})
|
||||
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
|
||||
}
|
||||
this.searchResultsCount = 0
|
||||
|
||||
//Remove all filter limits from previous queries
|
||||
delete this.fastFilters.limitSize
|
||||
delete this.fastFilters.limitOffset
|
||||
|
||||
let postData = {
|
||||
searchQuery: this.searchTerm,
|
||||
searchTags: this.searchTags,
|
||||
fastFilters: this.fastFilters,
|
||||
}
|
||||
|
||||
//Save initial post data on first load
|
||||
if(this.initialPostData == null){
|
||||
this.initialPostData = JSON.stringify(postData)
|
||||
}
|
||||
//If post data is not the same as initial, show clear button
|
||||
if(JSON.stringify(postData) != this.initialPostData){
|
||||
this.showClear = true
|
||||
}
|
||||
|
||||
if(notesInNextLoad && notesInNextLoad > 0){
|
||||
//Create limit based off of the number of notes already loaded
|
||||
postData.fastFilters.limitSize = notesInNextLoad
|
||||
postData.fastFilters.limitOffset = this.batchOffset
|
||||
}
|
||||
|
||||
//Perform search - or die
|
||||
this.loadingInProgress = true
|
||||
axios.post('/api/note/search', postData)
|
||||
.then(response => {
|
||||
|
||||
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
|
||||
|
||||
//Save the number of notes just loaded
|
||||
this.batchOffset += response.data.notes.length
|
||||
|
||||
//Enable or disable scroll loading
|
||||
this.scrollLoadEnabled = response.data.notes.length > 0
|
||||
|
||||
if(response.data.total > 0){
|
||||
this.searchResultsCount = response.data.total
|
||||
}
|
||||
|
||||
this.loadingInProgress = false
|
||||
this.generateNoteCategories(response.data.notes, mergeExisting)
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
|
||||
})
|
||||
},
|
||||
rebuildNoteCategorise(){
|
||||
let currentNotes = []
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key].forEach( note => {
|
||||
currentNotes.push(note)
|
||||
})
|
||||
})
|
||||
this.generateNoteCategories(currentNotes, false)
|
||||
},
|
||||
generateNoteCategories(notes, mergeExisting){
|
||||
// Place each note in a category based on certain attributes and fast filters
|
||||
|
||||
//Reset all sections if we are not merging existing
|
||||
if(!mergeExisting){
|
||||
Object.keys(this.noteSections).forEach( key => {
|
||||
this.noteSections[key] = []
|
||||
})
|
||||
}
|
||||
|
||||
//Sort notes into defined sections
|
||||
notes.forEach(note => {
|
||||
|
||||
if(this.searchTerm.length > 0){
|
||||
if(note.pinned == 1){
|
||||
this.noteSections.pinned.push(note)
|
||||
return
|
||||
}
|
||||
|
||||
//Push to default note section
|
||||
this.noteSections.notes.push(note)
|
||||
return
|
||||
}
|
||||
|
||||
//Display all tags in tag section
|
||||
if(this.searchTags.length >= 1){
|
||||
this.noteSections.tagged.push(note)
|
||||
return
|
||||
}
|
||||
|
||||
//Only show trashed notes when trashed
|
||||
if(this.fastFilters.onlyShowTrashed == 1){
|
||||
|
||||
if(note.trashed == 1){
|
||||
this.noteSections.trashed.push(note)
|
||||
}
|
||||
return
|
||||
}
|
||||
if(note.trashed == 1){
|
||||
return
|
||||
}
|
||||
|
||||
//Show archived notes
|
||||
if(this.fastFilters.onlyArchived == 1){
|
||||
|
||||
if(note.pinned == 1 && note.archived == 1){
|
||||
this.noteSections.pinned.push(note)
|
||||
return
|
||||
}
|
||||
if(note.archived == 1){
|
||||
this.noteSections.archived.push(note)
|
||||
}
|
||||
return
|
||||
}
|
||||
if(note.archived == 1){ return }
|
||||
|
||||
//Only show sent notes section if shared is selected
|
||||
if(this.fastFilters.onlyShowSharedNotes == 1){
|
||||
|
||||
if(note.shared == 2){
|
||||
this.noteSections.sent.push(note)
|
||||
}
|
||||
if(note.shareUsername != null){
|
||||
this.noteSections.shared.push(note)
|
||||
}
|
||||
return
|
||||
}
|
||||
//Show shared notes on main list but not notes shared with you
|
||||
if(note.shareUsername != null){ return }
|
||||
|
||||
// Pinned notes are always first, they can appear in the archive
|
||||
if(note.pinned == 1){
|
||||
this.noteSections.pinned.push(note)
|
||||
return
|
||||
}
|
||||
|
||||
//Push to default note section
|
||||
this.noteSections.notes.push(note)
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
},
|
||||
reset(){
|
||||
this.showClear = false
|
||||
this.scrollLoadEnabled = true
|
||||
this.searchTerm = ''
|
||||
this.searchTags = []
|
||||
this.tagSuggestions = []
|
||||
this.fastFilters = {}
|
||||
this.foundAttachments = [] //Remove all attachments
|
||||
|
||||
this.updateFastFilters(5) //This loads notes
|
||||
|
||||
},
|
||||
updateFastFilters(index){
|
||||
|
||||
//clear out tags
|
||||
this.searchTags = []
|
||||
this.tagSuggestions = []
|
||||
this.loadingInProgress = false
|
||||
this.searchTerm = ''
|
||||
this.$bus.$emit('reset_fast_filters') //Clear out search
|
||||
|
||||
const options = [
|
||||
'withLinks', // 'Only Show Notes with Links'
|
||||
'withTags', // 'Only Show Notes with Tags'
|
||||
'onlyArchived', //'Only Show Archived Notes'
|
||||
'onlyShowSharedNotes', //Only show shared notes
|
||||
'onlyShowTrashed',
|
||||
'notesHome',
|
||||
]
|
||||
|
||||
let filter = {}
|
||||
filter[options[index]] = 1
|
||||
|
||||
this.fastFilters = filter
|
||||
//Fetch First batch of notes with new filter
|
||||
this.search(true, this.firstLoadBatchSize, false)
|
||||
.then( r => this.search(false, this.batchSize, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.detail {
|
||||
float: right;
|
||||
}
|
||||
.note-card-display-area {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.display-area-title {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.note-card-section {
|
||||
/*padding-bottom: 15px;*/
|
||||
}
|
||||
.note-card-section + .note-card-section {
|
||||
padding: 15px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="ui basic segment no-fluf-segment">
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="ui sixteen wide column">
|
||||
<h2 class="ui header">
|
||||
<i class="sticky note outline icon"></i>
|
||||
<div class="content">
|
||||
The Scratch Pad
|
||||
<div class="sub header">One place to put random junk</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide middle aligned column" v-if="quickNoteId > 0">
|
||||
|
||||
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui compact basic button">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
|
||||
<div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
|
||||
<i class="sync alternate reload icon"></i>
|
||||
New Scratch Pad
|
||||
</div>
|
||||
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
|
||||
<i class="close icon"></i>
|
||||
Cancel
|
||||
</div>
|
||||
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="newQuickNote()">
|
||||
<i class="green thumbs up icon"></i>
|
||||
Confirm
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui sixteen wide column">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<textarea
|
||||
class="quick-note-input"
|
||||
rows="1"
|
||||
ref="fastInput"
|
||||
v-model="newText"
|
||||
v-on:keydown="checkKeyup"
|
||||
placeholder="Push to the top of the quick note."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div v-on:click="appendQuickNote" class="ui green button">Save (Enter)</div>
|
||||
<div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)">
|
||||
<i class="folder open outline icon"></i>
|
||||
Files
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="one wide column"></div>
|
||||
<div class="fourteen wide column">
|
||||
<div class="note-card-text" v-html="savedQuickNoteText"></div>
|
||||
</div>
|
||||
<div class="one wide column"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style type="text/css" scoped>
|
||||
.quick-note-input {
|
||||
box-sizing: border-box !important;
|
||||
resize: none !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
data: function(){
|
||||
return {
|
||||
newText: '',
|
||||
savedQuickNoteText: '',
|
||||
quickNoteId: null,
|
||||
showNewNoteConfirm: false,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
//
|
||||
// Perform Login check
|
||||
//
|
||||
this.$root.loginGateway()
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
this.$io.on('new_note_created', noteId => {
|
||||
this.getQuickNote()
|
||||
})
|
||||
|
||||
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
|
||||
this.getQuickNote()
|
||||
})
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
if(this.$refs.fastInput){
|
||||
//Load up note text
|
||||
this.getQuickNote()
|
||||
|
||||
//Set focus to input pane
|
||||
this.$nextTick(() => {
|
||||
this.$refs.fastInput.focus()
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
return this.$store.getters.getLoggedIn
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newQuickNote(){
|
||||
|
||||
this.showNewNoteConfirm = false
|
||||
|
||||
axios.post('/api/quick-note/new')
|
||||
.then( ({data}) => {
|
||||
this.savedQuickNoteText = ''
|
||||
this.quickNoteId = ''
|
||||
})
|
||||
|
||||
},
|
||||
openNoteEdit(){
|
||||
this.$router.push({'path':'/notes/open/'+this.quickNoteId})
|
||||
},
|
||||
checkKeyup(event){
|
||||
|
||||
let element = event.target
|
||||
let padding = 0
|
||||
|
||||
element.style.height = 'auto';
|
||||
element.style.height = (element.scrollHeight + padding) +'px';
|
||||
|
||||
//Enter Key submits by default
|
||||
if(event.keyCode == 13){
|
||||
this.appendQuickNote()
|
||||
return
|
||||
}
|
||||
},
|
||||
appendQuickNote(){
|
||||
|
||||
//Don't submit empty note
|
||||
if(this.newText.trim() == ''){ return }
|
||||
|
||||
axios.post('/api/quick-note/update', { 'pushText':this.newText.trim() } )
|
||||
.then( results => {
|
||||
|
||||
this.newText = '' //Clear text area
|
||||
this.$refs.fastInput.style.height = 'auto' //Back to normal size
|
||||
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Update Quick Note') })
|
||||
},
|
||||
getQuickNote(){
|
||||
axios.post('/api/quick-note/get')
|
||||
.then( ({data}) => {
|
||||
this.savedQuickNoteText = data.text
|
||||
this.quickNoteId = data.id
|
||||
})
|
||||
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Quick Note') })
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="squire-box">
|
||||
<div>
|
||||
|
||||
<h3 class="ui dividing header">
|
||||
<i class="inline green cog icon"></i>
|
||||
Settings for {{ $store.getters.getUsername }}
|
||||
</h3>
|
||||
|
||||
<h4>New Scratch Pad</h4>
|
||||
<div class="ui segment">
|
||||
<p>Create a new scratch pad. Old scratch pad will turn into a normal note.</p>
|
||||
<div class="ui compact basic button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
|
||||
<i class="sync alternate reload icon"></i>
|
||||
New Scratch Pad
|
||||
</div>
|
||||
<div v-if="showNewNoteConfirm" class="ui compact basic button shrinking" v-on:click="showNewNoteConfirm = false">
|
||||
<i class="close icon"></i>
|
||||
Cancel
|
||||
</div>
|
||||
<div v-if="showNewNoteConfirm" class="ui compact basic button shrinking" v-on:click="newQuickNote()">
|
||||
<i class="green thumbs up icon"></i>
|
||||
Confirm
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accent Color -->
|
||||
<h4 class="ui header">
|
||||
Accent Color
|
||||
</h4>
|
||||
<div class="ui segment">
|
||||
<div class="ui doubling grid">
|
||||
<div class="sixteen wide column">
|
||||
<p>Theme changes are only saved to this browser.</p>
|
||||
<div
|
||||
v-for="color in themeColors"
|
||||
class="ui compact basic button"
|
||||
:style="`background: linear-gradient(0deg, ${color} 4%, rgba(0,0,0,0) 5%);`"
|
||||
v-on:click="setAccentColor(color)">
|
||||
<logo style="width: 33px; height: auto;" :color="color" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enable Two Factor -->
|
||||
<h4>Two Factor Authentication</h4>
|
||||
<div class="ui segment">
|
||||
<div class="ui stackable grid">
|
||||
<div class="six wide column">
|
||||
<div class="ui tiny dividing header">1. Enter Password and get QR</div>
|
||||
<div class="ui fluid action input">
|
||||
<input type="password" placeholder="Current Password" v-model="password">
|
||||
|
||||
<div v-if="password.length == 0" class="ui disabled button">
|
||||
Get QR code
|
||||
</div>
|
||||
<div v-if="password.length > 0" class="ui green button" v-on:click="getQrCode()">
|
||||
Get QR code
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<div class="ui tiny dividing header">2. Scan QR Code</div>
|
||||
<p v-if="qrCode == ''">(QR Code will appear here)</p>
|
||||
<img v-if="qrCode != ''" :src="qrCode" class="ui image" alt="QR Code">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<div class="ui tiny dividing header">3. Verify with code</div>
|
||||
<div class="ui fluid action input" v-if="qrCode != ''">
|
||||
<input type="text" placeholder="Verification Code" v-model="verificationToken" v-on:keyup.enter="verifyQrCode()">
|
||||
<div class="ui green button">
|
||||
Verify!
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui fluid action input" v-if="qrCode == ''">
|
||||
<input type="text" placeholder="Verification Code" >
|
||||
<div class="ui disabled button">
|
||||
Verify!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- change password -->
|
||||
<h4>Change Password</h4>
|
||||
<div class="ui segment">
|
||||
<div class="ui stackable grid">
|
||||
<div class="five wide column">
|
||||
<p>Current Password</p>
|
||||
<div class="ui fluid input">
|
||||
<input v-model="change1" type="password" placeholder="Current Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="five wide column">
|
||||
<p>New Password</p>
|
||||
<div class="ui fluid input">
|
||||
<input v-model="change2" type="password" placeholder="New Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<p>Rereat New Password</p>
|
||||
<div class="ui fluid action input">
|
||||
<input v-model="change3" type="password" placeholder="Repeat Password">
|
||||
<div v-on:click="passwordChange()" class="ui button" :class="{'green':(change1.length > 0 && change2 == change3)}">
|
||||
Change it!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- log out -->
|
||||
<h4>Revoke Other Active Sessions</h4>
|
||||
<div class="ui segment">
|
||||
<div class="ui stackable grid">
|
||||
<div class="sixteen wide column">
|
||||
<p>Revoke access on any logged in device, except for the one you are currently using.<br><br></p>
|
||||
<div class="ui button" v-on:click="revokeAllSessions()">
|
||||
<i class="sign out icon"></i>
|
||||
Log Out all other devices
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Export All Data (In Development)</h4>
|
||||
<div class="ui segment">
|
||||
<p>Download all files and notes in raw text or html</p>
|
||||
<div class="ui button">Export all Data</div>
|
||||
</div>
|
||||
|
||||
<h4>Delete Account (In Development)</h4>
|
||||
<div class="ui segment">
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<p>Delete all data. This can not be undone.</p>
|
||||
<div class="ui fluid input">
|
||||
<input type="password" placeholder="Current Password" v-model="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="four wide bottom aligned column">
|
||||
<div class="ui fluid button">Verify</div>
|
||||
</div>
|
||||
<div class="four wide bottom aligned column">
|
||||
<div class="ui disabled fluid button">Delete Account</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="center aligned sixteen wide column">
|
||||
<router-link to="/terms">Solid Scribe Terms of Use</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'SettingsPage',
|
||||
components: {
|
||||
'logo':defineAsyncComponent(() => import('@/components/LogoComponent.vue')),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
password: '',
|
||||
qrCode: '',
|
||||
verificationToken: '',
|
||||
showNewNoteConfirm: false,
|
||||
|
||||
themeColors: [
|
||||
'#21BA45', //Green
|
||||
'#b5cc18', //Lime
|
||||
'#00b5ad', //Teal
|
||||
'#2185d0', //Blue
|
||||
'#7128b9', //Violet
|
||||
'#a333c8', // "Purple"
|
||||
'#e03997', //Pink
|
||||
'#db2828', //Red
|
||||
'#f2711c', //Orange
|
||||
'#fbbd08', //Yellow
|
||||
'#767676', //Grey
|
||||
'#303030', //Black-almost
|
||||
],
|
||||
|
||||
change1: '',
|
||||
change2: '',
|
||||
change3: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newQuickNote(){
|
||||
|
||||
this.showNewNoteConfirm = true
|
||||
|
||||
axios.post('/api/quick-note/new')
|
||||
.then( ({data}) => {
|
||||
this.showNewNoteConfirm = false
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
this.$bus.$emit('notification', 'New Scratch Pad Created')
|
||||
})
|
||||
|
||||
},
|
||||
setAccentColor(color){
|
||||
|
||||
let root = document.documentElement
|
||||
root.style.setProperty('--main-accent', color)
|
||||
localStorage.setItem('main-accent', color)
|
||||
|
||||
if(!color || color == '#21BA45'){
|
||||
localStorage.removeItem('main-accent')
|
||||
}
|
||||
},
|
||||
getQrCode(){
|
||||
|
||||
axios.post('/api/user/twofactorsetup', { password:this.password })
|
||||
.then(({data}) => {
|
||||
this.qrCode = data
|
||||
})
|
||||
},
|
||||
verifyQrCode(){
|
||||
|
||||
axios.post('/api/user/verifytwofactorsetuptoken', { password:this.password, token: this.verificationToken })
|
||||
.then(({data}) => {
|
||||
if(data == true){
|
||||
//Two FA is set up
|
||||
} else {
|
||||
//It failed
|
||||
}
|
||||
})
|
||||
},
|
||||
passwordChange(){
|
||||
|
||||
if(this.change1 == '' || this.change2 == '' || this.change3 == ''){
|
||||
this.$bus.$emit('notification', 'All Password Fields Required')
|
||||
return
|
||||
}
|
||||
|
||||
if(this.change1 == this.change2){
|
||||
this.$bus.$emit('notification', 'Old password matches new password')
|
||||
return
|
||||
}
|
||||
|
||||
if(this.change2 != this.change3){
|
||||
this.$bus.$emit('notification', 'New Passwords do not match')
|
||||
return
|
||||
}
|
||||
|
||||
const postData = {
|
||||
'currentPass':this.change1,
|
||||
'newPass':this.change3
|
||||
}
|
||||
|
||||
axios.post('/api/user/changepassword', postData)
|
||||
.then(({data}) => {
|
||||
if(data){
|
||||
this.$bus.$emit('notification', 'Success: Password Changed')
|
||||
this.change1 = ''
|
||||
this.change2 = ''
|
||||
this.change3 = ''
|
||||
} else {
|
||||
this.$bus.$emit('notification', 'Failed to change password')
|
||||
this.change1 = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
revokeAllSessions(){
|
||||
axios.post('/api/user/revokesessions')
|
||||
.then(({data}) => {
|
||||
this.$bus.$emit('notification', 'All other active sessions revoked.')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="sixteen wide column"></div>
|
||||
|
||||
<div class="sixteen wide column" v-if="text.length > 0 || title.length > 0">
|
||||
<div class="ui text container">
|
||||
|
||||
<div class="ui segment" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
|
||||
|
||||
<h1 v-if="title">{{title}}</h1>
|
||||
|
||||
<div v-if="text" v-html="text" class="squire-box"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="!$store.getters.getLoggedIn">
|
||||
<div class="ui text container">
|
||||
|
||||
<div class="ui segment">
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="three wide middle aligned center aligned column">
|
||||
<img class="small-logo" loading="lazy" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo">
|
||||
</div>
|
||||
<div class="thirteen wide column">
|
||||
<!-- header -->
|
||||
<h2 class="ui header">
|
||||
<div class="content">
|
||||
Solid Scribe is an easy, free, secure Note App
|
||||
<div class="sub header">
|
||||
Encrypted notes, only readable by you. Unless you share them.
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<!-- buttons -->
|
||||
<div class="ui grid">
|
||||
<div class="eight wide center aligned column">
|
||||
<router-link class="ui compact green button" to="/login">
|
||||
<i class="plug icon"></i>Sign Up
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="eight wide center aligned column">
|
||||
<router-link class="ui compact green button" to="/">
|
||||
<i class="comment outline icon"></i>
|
||||
Learn More
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<h4>{{ failText }}</h4>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'SharePage',
|
||||
data(){
|
||||
return {
|
||||
title: '',
|
||||
text: '',
|
||||
failText: '',
|
||||
styleObject:{},
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
//You can put something here for live updates
|
||||
// this.$io.on
|
||||
|
||||
this.openNote()
|
||||
|
||||
},
|
||||
methods:{
|
||||
fail(){
|
||||
this.failText = 'Failed to open Shared Note'
|
||||
this.$bus.$emit('notification', 'Failed to Open Shared Note')
|
||||
},
|
||||
openNote(){
|
||||
|
||||
const noteId = this.$route.params.id
|
||||
const sharedKey = this.$route.params.token
|
||||
|
||||
axios.post('/api/public/opensharednote', {noteId, sharedKey})
|
||||
.then( ({data}) => {
|
||||
|
||||
if(data.success){
|
||||
this.title = data.title
|
||||
this.text = data.text
|
||||
this.styleObject = data.styleObject
|
||||
} else {
|
||||
this.fail()
|
||||
}
|
||||
})
|
||||
.catch(error => { this.fail() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.small-logo {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="text-container squire-box">
|
||||
|
||||
<h3 class="ui dividing header">
|
||||
<i class="green info circle icon"></i>
|
||||
Solid Scribe Terms of Use
|
||||
</h3>
|
||||
|
||||
<p>Welcome to Solid Scribe’s web-based application at solidscribe.com.<br></p><p><br></p><p>Your use of the Site and/or participation in any of the Services offered through the Site, is subject to these Terms of Use.<br></p><p>By registering an account or using the Site you agree to be bound by these Terms of Use.<br></p><p><br></p><p>The Company reserves the right to change these Terms of Use from time to time by publishing them on the Site.<br></p><p>By using the Site, you agree to the most current version of these Terms of Use as posted on the Site.<br></p><p><br></p><p>All content on the Site is provided "as is." Solid Scribe is not liable for any informational errors, incompleteness, delays or any actions taken in reliance on information contained on the Site. The Company cannot and does not guarantee that any content on the Site will be entirely accurate or up-to-date at all times as the information on the Site originates from various sources, which may or may not be within the control of the Company.<br></p><p><br></p><p>The Company is not responsible for any omission, deletion, failure to store, improper delivery or untimely delivery of any information or material. The Company is not responsible for any reliance upon or damage arising from the use of the Site, including technical damage resulting from downloading or accessing any information or material provided.<br></p><p><br></p><p>You understand and agree that the Company is not responsible for any content posted by you or third parties. You further understand that the Company does not monitor the content posted by other users.<br></p><p><br></p><p>The Company reserves the right to remove content that the Company deems inappropriate for any reason what so ever without notice. The Company also reserves the right, in the Company’s sole discretion, to remove a user's privilege to post content on the Site.<br></p><p><br></p><p>The Company does not in any manner endorse the content of its users and cannot and does not vouch for its reliability or otherwise accept liability for it.<br></p><p><br></p><p>Minors (persons under 18 years of age), persons or organizations that cannot enter into binding contracts under applicable law, and those who may be prevented by contractual obligations from entering into binding contracts with Solid Scribe are not allowed to use this Site.<br></p><p><br></p><p>The Company has the right, but not the obligation, to take any of the following actions in the Company’s sole discretion at any time and for any reason without providing you any prior notice:<br></p><p><br></p><p>1. Restrict, suspend, modify or terminate access to the Site or all or any part of the Company’s Services;<br></p><p>2. Change, suspend or discontinue all or any part of the Company’s Services;<br></p><p>3. Refuse, move, modify or remove any material submitted to the Site by any person, for any reason;<br></p><p>4. Refuse, move, modify or remove any content available on the Site;<br></p><p>5. Deactivate or delete your account and all related information and files in your account;<br></p><p>6. Establish, discontinue or change general practices and limits concerning use of the Site and the Company’s Services.<br></p><p><br></p><p>You agree that the Company is not liable to you or any third party for taking any of these actions. Any registration or other eligibility requirements are in the sole discretion of the Company and may be modified for any reason at any time.<br></p><p><br></p><p>YOUR INFORMATION AND CONTENT<br></p><p><br></p><p>Any information you provide to Solid Scribe or other users during your registration or use of the Site (in any public message area, through any mail feature, or otherwise generated out of your use of the Site or the Services), is subject to the following:<br></p><p><br></p><p>(a) You are solely responsible for the content of your information, postings or Notes. The Company reserves the right to take any action with respect to such information that it deems necessary or appropriate in its sole discretion for any reason, including if the Company believes information is inappropriate for the Site or the Services, may create liability for the Company.<br></p><p><br></p><p>(b) You agree that your information will not, to the best of your knowledge: (i) violate any law (including without limitation those governing export control, consumer protection, unfair competition, anti-discrimination or false advertising); or (ii) be libelous, defamatory, obscene, abusive, violate a third party's right to privacy, otherwise violate any applicable local, state, national or international law, amount to spamming or be otherwise inappropriate. You may not post content that degrades others on the basis of gender, race, class, ethnicity, national origin, religion, sexual preference, disability or other classification. Epithets and other language intended to intimidate, unlawfully threaten, unlawfully harass or incite violence are alLine truncated
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TermsPage',
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const HomePage = () => import('@/pages/HomePage.vue')
|
||||
const LoginPage = () => import('@/pages/LoginPage.vue')
|
||||
const HelpPage = () => import('@/pages/HelpPage.vue')
|
||||
const TermsPage = () => import('@/pages/TermsPage.vue')
|
||||
const SettingsPage = () => import('@/pages/SettingsPage.vue')
|
||||
const SharePage = () => import('@/pages/SharePage.vue')
|
||||
const NotesPage = () => import('@/pages/NotesPage.vue')
|
||||
const QuickPage = () => import('@/pages/QuickPage.vue')
|
||||
const AttachmentsPage = () => import('@/pages/AttachmentsPage.vue')
|
||||
const OverviewPage = () => import('@/pages/OverviewPage.vue')
|
||||
const BookmarkletPage = () => import('@/pages/BookmarkletPage.vue')
|
||||
const NotFoundPage = () => import('@/pages/NotFoundPage.vue')
|
||||
const MetrictrackingPage = () => import('@/pages/MetrictrackingPage.vue')
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'HomePage', meta: { title: 'Home' }, component: HomePage },
|
||||
{ path: '/login', name: 'LoginPage', meta: { title: 'Login' }, component: LoginPage },
|
||||
{ path: '/notes', name: 'Note Page', meta: { title: 'Notes' }, component: NotesPage },
|
||||
{ path: '/notes/open/:id', name: 'Open Note', meta: { title: 'Open Note' }, component: NotesPage },
|
||||
{ path: '/search/tags/:tag', name: 'Search Notes', meta: { title: 'Search Notes' }, component: NotesPage },
|
||||
{ path: '/notes/open/:id/menu/:openMenu', name: 'Open Note Menu', meta: { title: 'Open Note Menu' }, component: NotesPage },
|
||||
{ path: '/help', name: 'Help', meta: { title: 'Help' }, component: HelpPage },
|
||||
{ path: '/terms', name: 'Terms', meta: { title: 'Terms' }, component: TermsPage },
|
||||
{ path: '/bookmarklet', name: 'Bookmarklet', meta: { title: 'Bookmarklet' }, component: BookmarkletPage },
|
||||
{ path: '/settings', name: 'Settings', meta: { title: 'Settings' }, component: SettingsPage },
|
||||
{ path: '/public/note/:id/:token', name: 'Share', meta: { title: 'Shared' }, component: SharePage },
|
||||
{ path: '/quick', name: 'Quick', meta: { title: 'Scratch Pad' }, component: QuickPage },
|
||||
{ path: '/attachments', name: 'Attachments', meta: { title: 'Attachments' }, component: AttachmentsPage },
|
||||
{ path: '/attachments/note/:id', name: 'Attachments for Note', meta: { title: 'Attachments for Note' }, component: AttachmentsPage },
|
||||
{ path: '/attachments/type/:type', name: 'Attachments by Type', meta: { title: 'Attachments by Type' }, component: AttachmentsPage },
|
||||
{ path: '/overview', name: 'Overview of Notes', meta: { title: 'Overview of Notes' }, component: OverviewPage },
|
||||
{ path: '/metrictrack', name: 'Metric Tracking', meta: { title: 'Metric Tracking' }, component: MetrictrackingPage },
|
||||
{ path: '/:pathMatch(.*)*', name: 'Page Not Found', meta: { title: '404 Page Not Found' }, component: NotFoundPage },
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,196 @@
|
||||
import { createStore } from 'vuex'
|
||||
import axios from 'axios'
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
username: null,
|
||||
nightMode: false,
|
||||
isUserOnMobile: false,
|
||||
fetchTotalsTimeout: null,
|
||||
userTotals: null, // {} // setting this to object breaks reactivity
|
||||
activeSessions: 0,
|
||||
},
|
||||
mutations: {
|
||||
setUsername(state, username){
|
||||
|
||||
localStorage.removeItem('username') //We only want one login token per computer
|
||||
localStorage.setItem('username', username)
|
||||
state.username = username
|
||||
},
|
||||
destroyLoginToken(state){
|
||||
|
||||
//Remove login token from local storage and from headers
|
||||
localStorage.removeItem('loginToken')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('currentVersion')
|
||||
localStorage.removeItem('snippetCache')
|
||||
delete axios.defaults.headers.common['authorizationtoken']
|
||||
state.username = null
|
||||
state.userTotals = null
|
||||
},
|
||||
toggleNightMode(state, pastTheme){
|
||||
|
||||
const themes = {
|
||||
'white':{
|
||||
'body_bg_color': '#f1f1f1',//'#f5f6f7',
|
||||
'small_element_bg_color': '#fff',
|
||||
'text_color': '#3d3d3d',
|
||||
'dark_border_color': '#d9d9d9',//'#DFE1E6',
|
||||
'border_color': '#DFE1E6',
|
||||
'menu-accent': '#cecece',
|
||||
'menu-text': '#5e6268',
|
||||
},
|
||||
'black':{
|
||||
'body_bg_color': 'rgb(12 4 30)',
|
||||
//'#0f0f0f',//'#000',
|
||||
'small_element_bg_color': '#000',
|
||||
'text_color': '#FFF',
|
||||
'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with
|
||||
'border_color': '#505050',
|
||||
'menu-accent': '#626262',
|
||||
'menu-text': '#d9d9d9',
|
||||
},
|
||||
}
|
||||
|
||||
//Catch values not in set
|
||||
|
||||
const totalThemes = Object.keys(themes).length
|
||||
state.nightMode++
|
||||
if(state.nightMode > totalThemes-1){
|
||||
state.nightMode = 0
|
||||
}
|
||||
if(pastTheme != null){
|
||||
state.nightMode = pastTheme
|
||||
}
|
||||
|
||||
//Final catch for numbers
|
||||
if(Number.isInteger(parseInt(state.nightMode)) == false){
|
||||
state.nightMode = 0
|
||||
}
|
||||
|
||||
const currentTheme = Object.keys(themes)[state.nightMode]
|
||||
|
||||
//Toggle state and save to local storage
|
||||
localStorage.setItem('nightMode', state.nightMode)
|
||||
|
||||
//Go through each color and set CSS variable
|
||||
let root = document.documentElement
|
||||
Object.keys( themes[currentTheme] ).forEach( attribute => {
|
||||
root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
|
||||
})
|
||||
|
||||
},
|
||||
detectIsUserOnMobile(state){
|
||||
|
||||
//Script from http://detectmobilebrowsers.com
|
||||
//Create closure and pass in browser data and state
|
||||
(function(a, state){
|
||||
if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))
|
||||
{
|
||||
state.isUserOnMobile = true
|
||||
}
|
||||
})(navigator.userAgent||navigator.vendor||window.opera, state);
|
||||
},
|
||||
setSocketIoSocket(state, socket){
|
||||
|
||||
//Put socket id in axios headers
|
||||
axios.defaults.headers.common['socketId'] = socket
|
||||
state.socket = socket
|
||||
},
|
||||
setUserTotals(state, totalsObject){
|
||||
|
||||
if(!state.userTotals){
|
||||
state.userTotals = {}
|
||||
}
|
||||
|
||||
// retain old values loaded on initial, extended options load
|
||||
let oldMissingValues = {}
|
||||
Object.keys(state.userTotals).forEach(key => {
|
||||
if(!totalsObject[key] && totalsObject[key] !== 0){
|
||||
oldMissingValues[key] = state.userTotals[key]
|
||||
}
|
||||
})
|
||||
|
||||
// combine old settings with updated settings
|
||||
let oldAndNew = Object.assign(oldMissingValues, totalsObject)
|
||||
|
||||
state.userTotals = oldAndNew
|
||||
|
||||
//Set computer version from server
|
||||
const currentVersion = localStorage.getItem('currentVersion')
|
||||
if(currentVersion == null){
|
||||
localStorage.setItem('currentVersion', totalsObject.currentVersion)
|
||||
return
|
||||
}
|
||||
|
||||
//If version is already set and it doesn't match the server, reload app
|
||||
if(currentVersion != totalsObject.currentVersion){
|
||||
localStorage.setItem('currentVersion', totalsObject.currentVersion)
|
||||
location.reload(true)
|
||||
}
|
||||
|
||||
|
||||
// console.log('-------------')
|
||||
// Object.keys(totalsObject).forEach( key => {
|
||||
// console.log(key + ' -- ' + totalsObject[key])
|
||||
// })
|
||||
},
|
||||
setActiveSessions(state, countData){
|
||||
//Count of the number of active socket.io sessions for this user
|
||||
state.activeSessions = countData
|
||||
},
|
||||
hideMetricTrackingReminder(state){
|
||||
if(state.userTotals){
|
||||
state.userTotals['showTrackMetricsButton'] = false
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getUsername: state => {
|
||||
return state.username
|
||||
},
|
||||
getLoggedIn: state => {
|
||||
let weIn = (state.username && state.username.length > 0)
|
||||
return weIn
|
||||
},
|
||||
getIsNightMode: state => {
|
||||
return state.nightMode
|
||||
},
|
||||
getIsUserOnMobile: state => {
|
||||
return state.isUserOnMobile
|
||||
},
|
||||
getIsNoteSettingsOpen: state => {
|
||||
return state.isNoteSettingsOpen
|
||||
},
|
||||
getSocket: state => {
|
||||
return state.socket
|
||||
},
|
||||
totals: state => {
|
||||
return state.userTotals
|
||||
},
|
||||
getActiveSessions: state => {
|
||||
return state.activeSessions
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
fetchAndUpdateUserTotals ({ commit, state }) {
|
||||
clearTimeout(state.fetchTotalsTimeout)
|
||||
state.fetchTotalsTimeout = setTimeout(() => {
|
||||
// load extended options on initial load
|
||||
let postData = {
|
||||
extendedOptions: !state.userTotals
|
||||
}
|
||||
axios.post('/api/user/totals', postData)
|
||||
.then( ({data}) => {
|
||||
commit('setUserTotals', data)
|
||||
})
|
||||
.catch( error => {
|
||||
if(error.response && error.response.status == 400){
|
||||
commit('destroyLoginToken')
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
transformAssetUrls: { includeAbsolute: false },
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000',
|
||||
'/socket': { target: 'http://localhost:3001', ws: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user