Compare commits
13 Commits
dev
...
8833a213a7
Author | SHA1 | Date | |
---|---|---|---|
|
8833a213a7 | ||
|
f833845452 | ||
|
05152cd5a4 | ||
|
cf3289aac6 | ||
|
acf72ca67e | ||
|
7f93925f74 | ||
|
d2c1dedffb | ||
|
003c7e32b1 | ||
|
de646cf1de | ||
|
2828cc9462 | ||
|
f99d6ed430 | ||
|
4216c1825e | ||
|
8d07a8e11a |
18
backupDatabase.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
BACKUPDIR="/home/mab/databaseBackupPi"
|
||||
|
||||
mkdir -p $BACKUPDIR
|
||||
cd $BACKUPDIR
|
||||
|
||||
NOW=$(date +"%Y-%m-%d_%H-%M")
|
||||
ssh mab@avidhabit.com -p 13328 "mysqldump --all-databases --user root -pRootPass1234!" > "backup-$NOW.sql"
|
||||
|
||||
cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
|
||||
|
||||
echo "Database Backup Complete on $NOW"
|
||||
|
||||
#Restore DB
|
||||
# copy file over, run restore
|
||||
# scp -P 13328 backup-2019-12-04_03-00.sql mab@avidhabit.com:/home/mab
|
||||
# mysql -u root -p < backup-2019-12-04_03-00.sql
|
@@ -5,19 +5,16 @@
|
||||
# Push built release files to production server
|
||||
#
|
||||
|
||||
echo -e "\e[32m\nStarting Build, hold onto your parts... \n\e[0m"
|
||||
echo -e "\e[32m\nStarting Build. \n\e[0m"
|
||||
|
||||
# Build out new release
|
||||
cd client
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# Remove old releases
|
||||
rm release.tar.gz
|
||||
|
||||
# only compress client/dist and server with node_modules
|
||||
echo -e "\e[32m\nCompressing client and server code... \n\e[0m"
|
||||
tar -czf release.tar.gz server node_modules client/dist package.json
|
||||
tar -czf release.tar.gz server node_modules client/dist staticFiles/assets
|
||||
|
||||
#send compressed release to remote machine
|
||||
echo -e "\e[32m\nMoving compressed release to production... \n\e[0m"
|
||||
@@ -28,7 +25,7 @@ rm release.tar.gz
|
||||
|
||||
#uncompress release on server
|
||||
echo -e "\e[32m\nExtracting release on production... \n\e[0m"
|
||||
ssh mab@avidhabit.com -p 13328 "cd /home/mab/pi/; rm -r server node_modules client; tar -xzf *.tar.gz; rm *.tar.gz; pm2 reload all"
|
||||
ssh mab@avidhabit.com -p 13328 "cd /home/mab/pi/; rm -r server node_modules client; tar -xzf *.tar.gz --overwrite; rm *.tar.gz; pm2 reload all"
|
||||
|
||||
#Congratulate how awesome you are
|
||||
echo -e "\e[32m\nRelease Complete! Nice Work! \n\e[0m"
|
@@ -58,11 +58,11 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
test: /\.(eot|ttf|otf|woff|woff2)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
name: utils.assetsPath('fonts/[name].[ext]')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@@ -17,6 +17,9 @@ const devWebpackConfig = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
|
||||
},
|
||||
watchOptions: {
|
||||
ignored: ['uploads', 'node_modules']
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: config.dev.devtool,
|
||||
|
||||
|
@@ -13,7 +13,7 @@ module.exports = {
|
||||
proxyTable: {},
|
||||
|
||||
// Various Dev Server settings
|
||||
host: 'localhost', // can be overwritten by process.env.HOST
|
||||
host: '0.0.0.0',//'localhost', // can be overwritten by process.env.HOST
|
||||
port: 8444, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
|
||||
autoOpenBrowser: false,
|
||||
errorOverlay: true,
|
||||
|
@@ -3,10 +3,17 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>client</title>
|
||||
|
||||
<link rel="icon" href="/api/static/assets/favicon.ico" type="image/ico"/>
|
||||
<link rel="shortcut icon" href="/api/static/assets/favicon.ico" type="image/x-icon"/>
|
||||
|
||||
<meta name="theme-color" content="#000" />
|
||||
<link rel="manifest" href="/api/static/assets/manifest.webmanifest">
|
||||
|
||||
<title>Notes</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<!-- built files will be auto injected, somewhere around here -->
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -11,20 +11,12 @@
|
||||
"build": "node build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-build-classic": "^12.3.1",
|
||||
"@ckeditor/ckeditor5-build-decoupled-document": "^12.3.1",
|
||||
"@ckeditor/ckeditor5-dev-utils": "^12.0.2",
|
||||
"@ckeditor/ckeditor5-dev-webpack-plugin": "^8.0.2",
|
||||
"@ckeditor/ckeditor5-indent": "^10.0.1",
|
||||
"@ckeditor/ckeditor5-paragraph": "^11.0.4",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^14.1.1",
|
||||
"@ckeditor/ckeditor5-vue": "^1.0.0-beta.2",
|
||||
"axios": "^0.18.0",
|
||||
"ckeditor5-indent-text": "^1.0.8",
|
||||
"es6-promise": "^4.2.6",
|
||||
"pell": "^1.0.6",
|
||||
"postcss-loader": "^2.1.6",
|
||||
"raw-loader": "^0.5.1",
|
||||
"semantic-ui": "^2.4.2",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"vue": "^2.5.2",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.1.0"
|
||||
@@ -46,6 +38,7 @@
|
||||
"file-loader": "^1.1.4",
|
||||
"friendly-errors-webpack-plugin": "^1.6.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"ip": "^1.1.5",
|
||||
"node-notifier": "^5.1.2",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"ora": "^1.2.0",
|
||||
|
@@ -1,37 +1,25 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet">
|
||||
<global-site-menu />
|
||||
|
||||
<!-- Hide this menu on the notes page -->
|
||||
<div class="ui basic segment" v-if="
|
||||
this.$router.currentRoute.name != 'NotesPage'
|
||||
">
|
||||
<div class="ui container">
|
||||
<div class="ui tabular menu">
|
||||
<router-view />
|
||||
|
||||
<router-link class="item" exact-active-class="active" to="/">Home</router-link>
|
||||
<router-link v-if="loggedIn" exact-active-class="active" class="item" to="/notes">Notes</router-link>
|
||||
<router-link class="item" exact-active-class="active" to="/help">Help</router-link>
|
||||
<router-link v-if="!loggedIn" exact-active-class="active" class="item" to="/login">Login</router-link>
|
||||
<div v-if="loggedIn" v-on:click="destroyLoginToken" class="item">Logout</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-view/>
|
||||
<global-notification />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
// import io from 'socket.io-client'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
|
||||
components: {
|
||||
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
|
||||
'global-notification':require('@/components/GlobalNotificationComponent.vue').default
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
// loggedIn:
|
||||
@@ -39,26 +27,41 @@ export default {
|
||||
},
|
||||
beforeCreate: function(){
|
||||
|
||||
//Puts token into state on page load
|
||||
let token = localStorage.getItem('loginToken')
|
||||
let username = localStorage.getItem('username')
|
||||
|
||||
// const socket = io({ path:'/socket' });
|
||||
const socket = this.$io
|
||||
socket.on('connect', () => {
|
||||
|
||||
this.$store.commit('setSocketIoSocket', socket.id)
|
||||
|
||||
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 color theme based on local storage
|
||||
if(localStorage.getItem('nightMode') == 'true'){
|
||||
this.$store.commit('toggleNightMode')
|
||||
}
|
||||
|
||||
//Puts token into state on page load
|
||||
let token = localStorage.getItem('loginToken')
|
||||
let username = localStorage.getItem('username')
|
||||
|
||||
//Put user data into global store on load
|
||||
if(token){
|
||||
this.$store.commit('setLoginToken', {token, username})
|
||||
} else {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push({'path':'/'})
|
||||
}
|
||||
|
||||
//Detect if user is on a mobile browser and set a flag in store
|
||||
this.$store.commit('detectIsUserOnMobile')
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
//Update totals for entire app on event
|
||||
this.$io.on('update_counts', () => {
|
||||
console.log('Got event, update totals')
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
})
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
@@ -69,6 +72,13 @@ export default {
|
||||
methods: {
|
||||
destroyLoginToken() {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
},
|
||||
loginGateway() {
|
||||
if(!this.loggedIn){
|
||||
console.log('This user is not logged in')
|
||||
this.$router.push({'path':'/login'})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
client/src/assets/roboto-latin-bold.woff2
Normal file
BIN
client/src/assets/roboto-latin.woff2
Normal file
@@ -1,110 +1,357 @@
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Roboto Bold'), local('Roboto-Bold'), url(/static/fonts/roboto-latin-bold.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--background_color: #fff;
|
||||
--text_color: #3d3d3d;
|
||||
--outline_color: rgba(34,36,38,.15);
|
||||
--background_color: #fff;
|
||||
--text_color: #3d3d3d;
|
||||
--outline_color: rgba(34,36,38,.15);
|
||||
--border_color: rgba(34,36,38,.20);
|
||||
|
||||
/*Global purple menu styles */
|
||||
--menu-border: #534c68;
|
||||
--menu-background: #221f2b;
|
||||
}
|
||||
|
||||
html {
|
||||
/*scrollbar-width: none;*/
|
||||
}
|
||||
|
||||
div.ui.basic.segment.no-fluf-segment {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
||||
body{
|
||||
body {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
background-color: var(--background_color);
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.ui.form input:not([type]),
|
||||
.ui.form input:not([type]):focus {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
.ui.form input:not([type]):focus,
|
||||
.ui.form textarea:not([type]),
|
||||
.ui.form textarea:not([type]):focus {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.ui.basic.label {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
.ui.basic.label, .ui.header, .ui.header div.sub.header {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
div.ui.basic.green.label {
|
||||
background-color: var(--background_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
}
|
||||
.ui.basic.button, .ui.basic.buttons .button {
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color) !important;
|
||||
box-shadow: none;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ui.basic.button:focus, .ui.basic.button:hover {
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
box-shadow: none;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ui.tabular.menu .item {
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
}
|
||||
.ui.tabular.menu .item.active {
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--border_color) !important;
|
||||
background-color: var(--background_color) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--border_color) !important;
|
||||
}
|
||||
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
||||
|
||||
|
||||
.color-picker {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
|
||||
position: absolute;
|
||||
width: 175px;
|
||||
height: 100px;
|
||||
top: 26px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
left: -63px;
|
||||
z-index: 100;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color) !important;
|
||||
/* Styles for public display pages */
|
||||
.fun {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
color: var(--text_color);
|
||||
}
|
||||
.color-picker .button{
|
||||
border: 1px solid !important;
|
||||
border-color: var(--border_color) !important;
|
||||
color: var(--border_color) !important;
|
||||
.fun h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
.fun h2 {
|
||||
font-size: 1.9em;
|
||||
}
|
||||
.fun h3 {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
.fun p {
|
||||
/*font-size: 1.5em;*/
|
||||
}
|
||||
.fun blockquote {
|
||||
border-left: 5px solid cornflowerblue;
|
||||
padding-left: 25px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
/* Styles for public display pages */
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*//
|
||||
// Purple Global Menu
|
||||
//*/
|
||||
.note-menu {
|
||||
width: 100%;
|
||||
/*display: block;*/
|
||||
display: inline-table;
|
||||
background: var(--menu-background);
|
||||
color: white;
|
||||
/*overflow: hidden;*/
|
||||
border: 1px solid var(--menu-border);
|
||||
/*height: 50px;*/
|
||||
}
|
||||
.note-menu > .nm-button {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
font-size: 1.2em;
|
||||
vertical-align: middle;
|
||||
/*height: 40px;*/
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
}
|
||||
.nm-button i.icon {
|
||||
margin: 0;
|
||||
}
|
||||
.nm-button span {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.nm-button.right {
|
||||
float: right;
|
||||
border-left: 1px solid var(--menu-border);
|
||||
}
|
||||
.nm-button:hover {
|
||||
background-color: #534c68;
|
||||
color: white;
|
||||
}
|
||||
.nm-button + .nm-button {
|
||||
border-left: 1px solid #534c68;
|
||||
}
|
||||
/*.shrink-icons-on-mobile.note-menu span {
|
||||
display: none;
|
||||
}*/
|
||||
|
||||
/* Shrink button text for mobile */
|
||||
@media only screen and (max-width: 740px) {
|
||||
.note-menu .nm-button span {
|
||||
font-size: 0.7em;
|
||||
line-height: 0.4em;
|
||||
margin-left: 0;
|
||||
}
|
||||
.nm-button i.icon {
|
||||
width: 100%;
|
||||
}
|
||||
/*prevents buttons from being jammed into corners of round phones*/
|
||||
.shrink-icons-on-mobile.note-menu {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.shrink-icons-on-mobile .nm-button {
|
||||
padding: 2px 3px;
|
||||
}
|
||||
.shrink-icons-on-mobile .nm-button i.icon {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
/*//
|
||||
// Purple Global Menu
|
||||
//*/
|
||||
|
||||
.note-status-indicator {
|
||||
float: right;
|
||||
width: 100px;
|
||||
padding: 9px 0;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
color: var(--text_color);
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
padding: 16px 0;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
color: var(--text_color);
|
||||
bottom: -13px;
|
||||
right: -7px;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* squire text styles */
|
||||
.squire-box {
|
||||
border: none;
|
||||
height: calc(100% - 69px);
|
||||
box-sizing: border-box;
|
||||
padding: 10px 15px 10px;
|
||||
background: transparent;
|
||||
overflow-x: scroll;
|
||||
/*color: var(--text_color);*/
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
word-wrap: break-word;
|
||||
/*border-bottom: 1px solid #ccc;*/
|
||||
scrollbar-width: none;
|
||||
}
|
||||
/*Makes the first line real big */
|
||||
.squire-box p:first-child {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
.squire-box:focus {
|
||||
outline: none;
|
||||
}
|
||||
.squire-box span.size {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.squire-box a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-card-text i,
|
||||
.squire-box i {
|
||||
padding: 0.5em 0.99em;
|
||||
border: 1px solid #CCC;
|
||||
margin: 1px;
|
||||
border-radius: 9px;
|
||||
display: inline-block;
|
||||
}
|
||||
.squire-box p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.squire-box blockquote {
|
||||
margin: 0;
|
||||
padding: 0.8em;
|
||||
border-left: 2px solid blue;
|
||||
}
|
||||
.note-card-text img {
|
||||
max-width:100%;
|
||||
height: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
.squire-box img {
|
||||
max-width:100%;
|
||||
height: auto;
|
||||
}
|
||||
.note-card-text li > p,
|
||||
.squire-box p,
|
||||
.squire-box li > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.note-card-text ul > li,
|
||||
.squire-box ul > li {
|
||||
position: relative;
|
||||
list-style-type: none;
|
||||
}
|
||||
.note-card-text ul > li:before,
|
||||
.squire-box ul > li:before {
|
||||
|
||||
content: "\f111";
|
||||
font-family: 'Icons';
|
||||
backface-visibility: hidden;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
text-align: center;
|
||||
line-height: 1.4em;
|
||||
font-size: 0.75em;
|
||||
|
||||
height: 17px;
|
||||
width: 17px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
left: -30px;
|
||||
/*border: 2px solid #444;*/
|
||||
/*border-radius: 4px;*/
|
||||
bottom: 0;
|
||||
top: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
ul > li.active:before {
|
||||
|
||||
font-family: 'Icons';
|
||||
content: "\f058";
|
||||
color: #21BA45;
|
||||
opacity: 1;
|
||||
}
|
||||
/* adjust checkboxes for mobile. Make them a little bigger, easier to click */
|
||||
@media only screen and (max-width: 740px) {
|
||||
|
||||
.note-card-text ul > li,
|
||||
.squire-box ul > li {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.note-card-text ul > li:before,
|
||||
.squire-box ul > li:before {
|
||||
|
||||
content: "\f111";
|
||||
font-family: outline-icons;
|
||||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
left: -40px;
|
||||
bottom: 0;
|
||||
top: 0px;
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 0.9em;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
ul > li.active:before {
|
||||
|
||||
font-family: 'Icons';
|
||||
content: "\f058";
|
||||
color: #21BA45;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
.float-right {
|
||||
float: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.textarea-height {
|
||||
height: calc(100% - 90px);
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
.ck-content {
|
||||
font-family: 'Open Sans' !important;
|
||||
font-size: 1.3rem !important;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
height: calc(100% - 40px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ck .ck-editor__nested-editable:focus {
|
||||
background-color: var(--background_color) !important;
|
||||
.mobile-textarea-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ui.white.button {
|
||||
background: #FFF;
|
||||
}
|
||||
.input-floating-button {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
transform: translateY(-50%);
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
.fade-in-fwd {
|
||||
animation: fade-in-fwd 0.8s both;
|
||||
@@ -116,12 +363,12 @@ div.ui.basic.green.label {
|
||||
* ----------------------------------------
|
||||
*/
|
||||
@keyframes fade-in-fwd {
|
||||
0% {
|
||||
transform: translateZ(-80px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateZ(0);
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
transform: translateZ(-80px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateZ(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
40312
client/src/assets/semantic-min.css
vendored
4995
client/src/assets/squire.js
Normal file
113
client/src/components/AnimatedCounterComponent.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<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>
|
219
client/src/components/AttachmentDisplayCard.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.attachment-display-card {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_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: 100px;
|
||||
}
|
||||
.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" :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"></textarea>
|
||||
|
||||
<!-- link -->
|
||||
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="ui small compact basic button" v-on:click="openNote">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
<div 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,
|
||||
|
||||
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()
|
||||
})
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
},
|
||||
saveIt(){
|
||||
|
||||
//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)
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
163
client/src/components/ColorPicker.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
|
||||
|
||||
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||
<div class="ui basic segment">
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<div class="ui fluid button" v-on:click="clearStyles">
|
||||
<i class="refresh icon"></i>
|
||||
Clear All Styles
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<br>
|
||||
<p>Note Color</p>
|
||||
<div v-for="color in getReducedColors()"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chosenColor(color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<p>Note Icon
|
||||
<span v-if="allStyles.noteIcon" >
|
||||
<i :class="`large ${allStyles.noteIcon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</span>
|
||||
</p>
|
||||
<div v-for="icon in icons" class="icon-button" v-on:click="chosenIcon(icon)" >
|
||||
<i :class="`large ${icon} icon`" :style="{ 'color':allStyles.iconColor }"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<p>Icon Color</p>
|
||||
<div v-for="color in getReducedColors()"
|
||||
class="color-button"
|
||||
:style="{ backgroundColor:color }"
|
||||
v-on:click="chooseIconColor(color)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</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: [
|
||||
"#ffebee","#ffcdd2","#ef9a9a","#e57373","#ef5350","#f44336","#e53935","#d32f2f","#c62828","#b71c1c","#fce4ec","#f8bbd0","#f48fb1","#f06292","#ec407a","#e91e63","#d81b60","#c2185b","#ad1457","#880e4f","#f3e5f5","#e1bee7","#ce93d8","#ba68c8","#ab47bc","#9c27b0","#8e24aa","#7b1fa2","#6a1b9a","#4a148c","#ede7f6","#d1c4e9","#b39ddb","#9575cd","#7e57c2","#673ab7","#5e35b1","#512da8","#4527a0","#311b92","#e8eaf6","#c5cae9","#9fa8da","#7986cb","#5c6bc0","#3f51b5","#3949ab","#303f9f","#283593","#1a237e","#e3f2fd","#bbdefb","#90caf9","#64b5f6","#42a5f5","#2196f3","#1e88e5","#1976d2","#1565c0","#0d47a1","#e1f5fe","#b3e5fc","#81d4fa","#4fc3f7","#29b6f6","#03a9f4","#039be5","#0288d1","#0277bd","#01579b","#e0f7fa","#b2ebf2","#80deea","#4dd0e1","#26c6da","#00bcd4","#00acc1","#0097a7","#00838f","#006064","#e0f2f1","#b2dfdb","#80cbc4","#4db6ac","#26a69a","#009688","#00897b","#00796b","#00695c","#004d40","#e8f5e9","#c8e6c9","#a5d6a7","#81c784","#66bb6a","#4caf50","#43a047","#388e3c","#2e7d32","#1b5e20","#f1f8e9","#dcedc8","#c5e1a5","#aed581","#9ccc65","#8bc34a","#7cb342","#689f38","#558b2f","#33691e","#f9fbe7","#f0f4c3","#e6ee9c","#dce775","#d4e157","#cddc39","#c0ca33","#afb42b","#9e9d24","#827717","#fffde7","#fff9c4","#fff59d","#fff176","#ffee58","#ffeb3b","#fdd835","#fbc02d","#f9a825","#f57f17","#fff8e1","#ffecb3","#ffe082","#ffd54f","#ffca28","#ffc107","#ffb300","#ffa000","#ff8f00","#ff6f00","#fff3e0","#ffe0b2","#ffcc80","#ffb74d","#ffa726","#ff9800","#fb8c00","#f57c00","#ef6c00","#e65100","#fbe9e7","#ffccbc","#ffab91","#ff8a65","#ff7043","#ff5722","#f4511e","#e64a19","#d84315","#bf360c","#efebe9","#d7ccc8","#bcaaa4","#a1887f","#8d6e63","#795548","#6d4c41","#5d4037","#4e342e","#3e2723","#fafafa","#f5f5f5","#eeeeee","#e0e0e0","#bdbdbd","#9e9e9e","#757575","#616161","#424242","#212121","#eceff1","#cfd8dc","#b0bec5","#90a4ae","#78909c","#607d8b","#546e7a","#455a64","#37474f","#263238","#ffffff","#000000"],
|
||||
icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench']
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
styleObject: function(updatedStyles){
|
||||
this.allStyles = updatedStyles
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
this.allStyles = this.styleObject
|
||||
},
|
||||
methods: {
|
||||
getReducedColors(){
|
||||
|
||||
let reduced = []
|
||||
|
||||
this.colors.forEach((color,i) => {
|
||||
|
||||
if(i%20 <= 10){
|
||||
return
|
||||
}
|
||||
|
||||
let mod = (i % 10)+1 //1 - 10
|
||||
let lines = [3, 5, 8, 9, 10]
|
||||
if(lines.includes(mod)){
|
||||
reduced.push(color)
|
||||
}
|
||||
})
|
||||
|
||||
reduced.push("#000")
|
||||
|
||||
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
|
||||
|
||||
//Automatically select note text color
|
||||
|
||||
// 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;
|
||||
|
||||
//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 {
|
||||
height: 40px;
|
||||
width: 14.2%;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.color-button {
|
||||
height: 50px;
|
||||
width: 20%;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
}
|
||||
</style>
|
43
client/src/components/CrunchMenu.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>Crunch Menu</p>
|
||||
<div v-for="(item, index) in items">
|
||||
<slot :name="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'CrunchMenu',
|
||||
data () {
|
||||
return {
|
||||
items: []
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
console.log(this)
|
||||
// console.log(this.$slots.default)
|
||||
this.$slots.default.forEach( vnode => {
|
||||
if(vnode.tag && vnode.tag.length > 0){
|
||||
this.items.push(vnode)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(this.items)
|
||||
},
|
||||
methods: {
|
||||
onClickTag(index){
|
||||
console.log('yup')
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
</style>
|
@@ -20,9 +20,6 @@
|
||||
'Order by Last Edited' :'lastEdited',
|
||||
'Order by Last Opened' :'lastOpened',
|
||||
'Order by Last Created' :'lastCreated',
|
||||
'Only Show Notes with Links' :'withLinks',
|
||||
'Only Show Notes with Tags' :'withTags',
|
||||
'Only Show Archived Notes' :'onlyArchived',
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -32,9 +29,6 @@
|
||||
})
|
||||
},
|
||||
methods:{
|
||||
confirmDelete(){
|
||||
this.click++
|
||||
},
|
||||
displayString(){
|
||||
return this.orderString.replace('Order by','').replace('Only Show','')
|
||||
},
|
||||
@@ -54,7 +48,7 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.filter-header {
|
||||
width: 270px;
|
||||
width: 200px;
|
||||
padding: 0 0 0 10px;
|
||||
border: 1px solid rgba(0,0,0,0);
|
||||
border-bottom: none;
|
||||
@@ -62,24 +56,27 @@
|
||||
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(--background_color);
|
||||
border-color: var(--border_color);
|
||||
|
||||
|
||||
border: 1px solid;
|
||||
border-top: none;
|
||||
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 270px;
|
||||
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;
|
||||
|
91
client/src/components/FileUploadButton.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<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"/>
|
||||
</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 axios from 'axios'
|
||||
export default {
|
||||
name: 'FileUploadButton',
|
||||
props: [ 'noteId' ],
|
||||
components: {
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
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>
|
82
client/src/components/GlobalNotificationComponent.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.popup-body {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
min-height: 50px;
|
||||
min-width: 200px;
|
||||
max-width: calc(100% - 20px);
|
||||
z-index: 1002;
|
||||
border-top: 2px solid #21ba45;
|
||||
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
}
|
||||
.popup-row {
|
||||
padding: 1em 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.popup-row > span {
|
||||
width: calc(100% - 50px);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px 0;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.popup-row + .popup-row {
|
||||
border-top: 1px solid #000;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="popup-body" v-on:click="dismiss" v-if="notifications.length > 0">
|
||||
<div class="popup-row" v-for="item in notifications">
|
||||
<i class="disabled angle left icon"></i>
|
||||
<span>{{ item }}</span>
|
||||
<i class="disabled angle right icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'GlobalNotificationComponent',
|
||||
data () {
|
||||
return {
|
||||
notifications: [],
|
||||
totalTimeout: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.$bus.$on('notification', info => {
|
||||
this.displayNotification(info)
|
||||
})
|
||||
},
|
||||
mounted(){
|
||||
|
||||
|
||||
},
|
||||
methods: {
|
||||
displayNotification(newNotification){
|
||||
this.notifications.push(newNotification)
|
||||
clearTimeout(this.totalTimeout)
|
||||
this.totalTimeout = setTimeout(() => {
|
||||
this.dismiss()
|
||||
}, 4000)
|
||||
},
|
||||
dismiss(){
|
||||
this.notifications = []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
323
client/src/components/GlobalSiteMenu.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<style scoped>
|
||||
.slotholder {
|
||||
height: 100vh;
|
||||
width: 140px;
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
.global-menu {
|
||||
width: 140px;
|
||||
background: #221f2b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 111;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: #fff;
|
||||
padding: 0.8em 10px 0.8em 10px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 1.15em;
|
||||
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;
|
||||
}
|
||||
.menu-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu-button:hover {
|
||||
background-color: #534c68;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.router-link-active i {
|
||||
/*color: #16ab39;*/
|
||||
}
|
||||
.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: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
.top-menu-bar {
|
||||
/*color: var(--text_color);*/
|
||||
/*width: 100%;*/
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: var(--background_color);
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
padding: 5px 1rem 5px;
|
||||
}
|
||||
.place-holder {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
.top-menu-bar img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
</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">
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="seven wide column">
|
||||
<div class="ui large basic compact icon button" v-on:click="collapseMenu">
|
||||
<i class="green bars icon"></i>
|
||||
</div>
|
||||
|
||||
<router-link class="ui large basic compact icon button" to="/notes" v-on:click.native="emitReloadEvent()">
|
||||
<i class="green home icon"></i>
|
||||
</router-link>
|
||||
|
||||
<div v-on:click="toggleNightMode" class="ui large basic compact icon button">
|
||||
<i class="green moon outline icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide center aligned column">
|
||||
<img v-if="!loggedIn" src="/api/static/assets/favicon.ico" alt="logo" />
|
||||
<search-input v-if="loggedIn && mobile"></search-input>
|
||||
</div>
|
||||
<div class="three wide right aligned column">
|
||||
|
||||
<!-- mobile create note button -->
|
||||
<div v-if="loggedIn">
|
||||
<div v-if="!disableNewNote" @click="createNote" class="ui large basic compact icon button">
|
||||
<i class="green plus icon"></i>
|
||||
</div>
|
||||
<div v-if="disableNewNote" class="ui large basic compact icon button">
|
||||
<i class="grey plus icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</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">
|
||||
<div class="menu-item menu-button" v-on:click="collapseMenu">
|
||||
<i class="angle left icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div v-if="!disableNewNote" @click="createNote" class="menu-item menu-item menu-button">
|
||||
<i class="green plus icon"></i>New Note
|
||||
</div>
|
||||
<div v-if="disableNewNote" class="menu-item menu-item menu-button">
|
||||
<i class="purple plus icon"></i>Creating
|
||||
</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 class="float-right" number-id="totalNotes" />
|
||||
</router-link>
|
||||
<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="folder open outline icon"></i>Files
|
||||
<counter class="float-right" number-id="totalFiles" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
|
||||
<i class="paper plane outline icon"></i>Quick
|
||||
</router-link>
|
||||
</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">
|
||||
<i class="moon outline icon"></i>Light Theme</span>
|
||||
<span v-else>
|
||||
<i class="moon outline icon"></i>Dark Theme</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn" data-tooltip="Click to log out" data-inverted="" data-position="right center">
|
||||
<div v-if="loggedIn" v-on:click="destroyLoginToken" class="menu-item menu-button">
|
||||
<i class="user outline icon"></i>{{ucWords($store.getters.getUsername)}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <router-link class="ui basic compact button" exact-active-class="active" to="/help">
|
||||
<i class="question mark icon"></i>Help
|
||||
</router-link> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'search-input': require('@/components/SearchInput.vue').default,
|
||||
'counter':require('@/components/AnimatedCounterComponent.vue').default,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
username: '',
|
||||
collapsed: false,
|
||||
mobile: false,
|
||||
disableNewNote: false,
|
||||
menuOpen: true,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
mounted: function(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
this.collapsed = this.$store.getters.getIsUserOnMobile
|
||||
|
||||
// {{ totals['totalNotes'] }}
|
||||
|
||||
if(this.mobile){
|
||||
this.menuOpen = false
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
//Map logged in from state
|
||||
return this.$store.getters.getLoggedIn
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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){
|
||||
const title = ''
|
||||
this.disableNewNote = true
|
||||
|
||||
axios.post('/api/note/create', {title})
|
||||
.then(response => {
|
||||
|
||||
if(response.data && response.data.id){
|
||||
this.$router.push('/notes/open/'+response.data.id)
|
||||
this.$bus.$emit('open_note', response.data.id)
|
||||
this.disableNewNote = false
|
||||
}
|
||||
})
|
||||
},
|
||||
destroyLoginToken() {
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push('/')
|
||||
},
|
||||
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(index){
|
||||
|
||||
//A little hacky, brings user to notes page then filters on click
|
||||
if(this.$route.name != 'NotesPage'){
|
||||
this.$router.push('/notes')
|
||||
setTimeout( () => {
|
||||
this.updateFastFilters(index)
|
||||
}, 500 )
|
||||
}
|
||||
|
||||
const options = [
|
||||
'withLinks', // 'Only Show Notes with Links'
|
||||
'withTags', // 'Only Show Notes with Tags'
|
||||
'onlyArchived', //'Only Show Archived Notes'
|
||||
'onlyShowSharedNotes', //Only show shared notes
|
||||
]
|
||||
|
||||
let filter = {}
|
||||
filter[options[index]] = 1
|
||||
|
||||
this.$bus.$emit('update_fast_filters', filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<span>
|
||||
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete">
|
||||
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right">
|
||||
<i class="grey trash alternate icon"></i>
|
||||
</span>
|
||||
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="left center">
|
||||
<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>
|
||||
@@ -26,9 +26,11 @@
|
||||
this.click++
|
||||
},
|
||||
actuallyDelete(){
|
||||
|
||||
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
|
||||
if(response.data == true){
|
||||
this.$bus.$emit('note_deleted')
|
||||
this.$bus.$emit('note_deleted', this.noteId)
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
44
client/src/components/NoteMenuButtonComponent.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="nm-button" :class="moreClass" :data-tooltip="tip" data-inverted>
|
||||
<!-- 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'],
|
||||
data () {
|
||||
return {
|
||||
files: [],
|
||||
mobile: false,
|
||||
showTooltip: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
},
|
||||
mounted(){
|
||||
// console.log('Im a button')
|
||||
},
|
||||
methods: {
|
||||
onFileClick(file){
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,32 +1,36 @@
|
||||
<template>
|
||||
<div v-on:mouseover="fullTagEdit = true">
|
||||
|
||||
<!-- simple string view -->
|
||||
<div v-if="!fullTagEdit" class="ui basic segment">
|
||||
<div class="simple-tag-display">
|
||||
|
||||
<!-- Show Loading -->
|
||||
<span v-if="!loaded">Loading Tags...</span>
|
||||
|
||||
<!-- Default count -->
|
||||
<span v-if="loaded">
|
||||
<i class="tags icon"></i> <b>{{tags.length}} Tags</b>
|
||||
</span>
|
||||
|
||||
<!-- No tags default text -->
|
||||
<span v-if="tags.length == 0 && loaded" class="ui small compact green button">
|
||||
<i class="plus icon"></i> Add a tag
|
||||
</span>
|
||||
|
||||
<!-- display tags in comma delimited list -->
|
||||
<span v-if="tags.length > 0">
|
||||
<span v-for="(tag, i) in tags"><span v-if="i > 0">, </span>{{ucWords(tag.text)}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<!-- hover over view -->
|
||||
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()">
|
||||
<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">
|
||||
@@ -39,19 +43,12 @@
|
||||
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="onClickTag(index)">
|
||||
<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>
|
||||
|
||||
<!-- existing tags -->
|
||||
<div class="delete-tag-display" v-if="tags.length > 0">
|
||||
<div class="ui icon large label" v-for="tag in tags" :class="{ 'green':(newTagInput == tag.text) }">
|
||||
{{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,9 +66,11 @@
|
||||
newTagInput: '',
|
||||
typeDebounce: null,
|
||||
|
||||
allTags: [],
|
||||
noteTagIds: [],
|
||||
suggestions: [],
|
||||
selection: 0,
|
||||
fullTagEdit: false,
|
||||
fullTagEdit: true,
|
||||
loaded: false,
|
||||
}
|
||||
},
|
||||
@@ -84,14 +83,38 @@
|
||||
methods: {
|
||||
getTags(){
|
||||
//Get Note Tags -> /api/tags/get
|
||||
let vm = this
|
||||
axios.post('/api/tag/get', {'noteId': this.noteId})
|
||||
.then(response => {
|
||||
vm.loaded = true
|
||||
//Set up local data
|
||||
vm.tags = response.data
|
||||
.then( ({data}) => {
|
||||
|
||||
this.loaded = true
|
||||
|
||||
this.allTags = data.allTags
|
||||
this.noteTagIds = data.noteTagIds
|
||||
})
|
||||
},
|
||||
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
|
||||
|
||||
@@ -150,9 +173,38 @@
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
onClickTag(index){
|
||||
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(){
|
||||
|
||||
@@ -162,7 +214,7 @@
|
||||
}
|
||||
|
||||
let postData = {
|
||||
'tagText':this.newTagInput,
|
||||
'tagText':this.newTagInput.trim(),
|
||||
'noteId':this.noteId
|
||||
}
|
||||
let vm = this
|
||||
@@ -176,6 +228,7 @@
|
||||
})
|
||||
},
|
||||
onFocus(){
|
||||
return
|
||||
//Show suggested tags
|
||||
let vm = this
|
||||
let postData = {
|
||||
@@ -206,14 +259,16 @@
|
||||
},
|
||||
removeTag(tagId){
|
||||
|
||||
console.log(tagId)
|
||||
|
||||
let postData = {
|
||||
'tagId':tagId,
|
||||
'noteId':this.noteId
|
||||
}
|
||||
let vm = this
|
||||
|
||||
axios.post('/api/tag/removefromnote', postData)
|
||||
.then(response => {
|
||||
vm.getTags()
|
||||
this.getTags()
|
||||
})
|
||||
},
|
||||
clearSuggestions(){
|
||||
@@ -233,28 +288,24 @@
|
||||
|
||||
/* note tag edit area */
|
||||
.full-tag-area {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
padding: 15px;
|
||||
border: 1px solid;
|
||||
/*padding: 15px;*/
|
||||
/*border: 1px solid;*/
|
||||
border-color: var(--border_color);
|
||||
}
|
||||
.full-tag-area .delete-tag-display {
|
||||
margin-top: 15px;
|
||||
/*margin-top: 15px;*/
|
||||
}
|
||||
.full-tag-area .ui.label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.simple-tag-display {
|
||||
width: 100%;
|
||||
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 */
|
||||
@@ -269,8 +320,8 @@
|
||||
height: 40px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
color: black;
|
||||
background-color: var(--background_color);
|
||||
color: var(--text_color);
|
||||
}
|
||||
.suggestion-item.active {
|
||||
background: green;
|
||||
|
@@ -1,71 +1,82 @@
|
||||
<template>
|
||||
<div class="note-title-display-card fade-in-fwd"
|
||||
:style="{'background-color':color, 'color':fontColor}"
|
||||
<div class="note-title-display-card"
|
||||
:style="{'background-color':color, 'color':fontColor, 'border-color':color }"
|
||||
:class="{'currently-open':currentlyOpen}"
|
||||
>
|
||||
<!-- fade-in-fwd -->
|
||||
<div v-if="noteIcon" class="badge">
|
||||
<i :class="`large ${noteIcon} icon`" :style="{ 'color':iconColor }"></i>
|
||||
</div>
|
||||
|
||||
<div class="ui grid max-height">
|
||||
|
||||
<!-- Show title and snippet below it -->
|
||||
<div class="top aligned row" @click.stop="onClick(note.id)">
|
||||
<div class="top aligned row" @click.self="onClick(note.id)">
|
||||
|
||||
<div class="sixteen wide column overflow-hidden note-card-text" @click="e => onClick(note.id, e)">
|
||||
|
||||
<div class="subtext" v-if="note.shareUsername">Shared by {{ note.shareUsername }}</div>
|
||||
<div class="subtext" v-if="note.shared == 2">You Shared</div>
|
||||
|
||||
|
||||
<!-- Title display -->
|
||||
<div v-if="note.title.length > 0"
|
||||
data-test-id="title"
|
||||
:class="{ 'big-text':(note.titleLength <= 100), 'small-text-title':(note.titleLength >= 100) }"
|
||||
v-html="note.title"></div>
|
||||
|
||||
<!-- Sub text display -->
|
||||
<div v-if="note.subtext.length > 0 && !isShowingSearchResults()"
|
||||
data-test-id="subtext"
|
||||
:class="{ 'big-text':(note.subtextLength <= 100 && note.titleLength <= 100), 'small-text':(note.subtextLength >= 100) }"
|
||||
v-html="note.subtext"></div>
|
||||
|
||||
<div class="sixteen wide column overflow-hidden" v-if="isShowingSearchResults()">
|
||||
<!-- Display highlights from solr results -->
|
||||
<div v-if="note.note_highlights.length > 0" class="term-usage">
|
||||
<h4><i class="paragraph icon"></i> Found in Text</h4>
|
||||
<div class="usage-row" v-for="highlight in note.note_highlights" v-html="cleanHighlight(highlight)"></div>
|
||||
<div v-if="note.note_highlights.length > 0 && textResults" class="term-usage">
|
||||
<div
|
||||
class="usage-row"
|
||||
v-for="highlight in note.note_highlights"
|
||||
:class="{ 'big-text':(highlight <= 100), 'small-text-title':(highlight >= 100) }"
|
||||
v-html="cleanHighlight(highlight)"></div>
|
||||
</div>
|
||||
<div v-if="note.attachment_highlights.length > 0" class="term-usage">
|
||||
<h4><i class="linkify icon"></i> Found in URL</h4>
|
||||
<div class="usage-row" v-for="highlight in note.attachment_highlights" v-html="cleanHighlight(highlight)"></div>
|
||||
</div>
|
||||
<div v-if="note.tag_highlights.length > 0" class="term-usage">
|
||||
<h4><i class="tags icon"></i> Found in Tags</h4>
|
||||
<div class="usage-row" v-for="highlight in note.tag_highlights">
|
||||
<span
|
||||
v-for="tag in splitTags(highlight)"
|
||||
class="ui label"
|
||||
>
|
||||
<span v-html="tag"></span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Toolbar on the bottom -->
|
||||
<div class="bottom aligned row" @click.self="onClick(note.id)">
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui grid reduced-padding">
|
||||
|
||||
<div class="thirteen wide column clickable icon-bar" @click="onClick(note.id)">
|
||||
<!-- {{$helpers.timeAgo(note.updated)}} -->
|
||||
<span v-if="note.tags">
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span>
|
||||
</span>
|
||||
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
|
||||
<i class="green pin icon"></i>
|
||||
</span>
|
||||
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
|
||||
<i class="green archive icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="three wide right aligned column">
|
||||
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="note.thumbs">
|
||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||
<img v-for="thumb in note.thumbs.split(',').reverse()" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column overflow-hidden">
|
||||
<h3 class="clickable">{{note.title}}</h3>
|
||||
</div>
|
||||
<div class="sixteen wide column overflow-hidden">
|
||||
<p class="clickable">{{note.subtext}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom aligned row" @click.self.stop="onClick(note.id)">
|
||||
<div class="six wide column clickable" @click.stop="onClick(note.id)">
|
||||
{{$helpers.timeAgo(note.updated)}}
|
||||
</div>
|
||||
|
||||
<div class="ten wide right aligned column split-spans">
|
||||
<span v-if="note.pinned == 1" data-tooltip="Pinned">
|
||||
<i class="green pin icon"></i>
|
||||
</span>
|
||||
<span v-if="note.archived == 1" data-tooltip="Archived">
|
||||
<i class="green archive icon"></i>
|
||||
</span>
|
||||
<span v-if="note.attachment_count > 0">
|
||||
<i class="linkify icon"></i> {{note.attachment_count}}
|
||||
</span>
|
||||
<span v-if="note.tag_count == 1" data-tooltip="Note has 1 tag">
|
||||
<i class="tags icon"></i> {{note.tag_count}}
|
||||
</span>
|
||||
<span v-if="note.tag_count > 1" :data-tooltip="`Note has ${note.tag_count} tags`">
|
||||
<i class="tags icon"></i> {{note.tag_count}}
|
||||
</span>
|
||||
<delete-button :note-id="note.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,7 +84,7 @@
|
||||
|
||||
export default {
|
||||
name: 'NoteTitleDisplayCard',
|
||||
props: [ 'onClick', 'data', 'currentlyOpen' ],
|
||||
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults' ],
|
||||
components: {
|
||||
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
||||
},
|
||||
@@ -93,37 +104,86 @@
|
||||
},
|
||||
splitTags(text){
|
||||
return text.split(',')
|
||||
}
|
||||
},
|
||||
openEditAttachment(){
|
||||
this.$router.push('/attachments/note/'+this.note.id)
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
note: null,
|
||||
color: null, //'#FFF',
|
||||
fontColor: null, //'#000'
|
||||
color: null,
|
||||
fontColor: null,
|
||||
noteIcon: null,
|
||||
iconColor: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
this.note = this.data
|
||||
|
||||
if(this.note.color != null && this.note.color != '#FFF'){
|
||||
this.color = this.note.color
|
||||
this.fontColor = '#FFF'
|
||||
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">
|
||||
|
||||
/*Strict font sizes for card display*/
|
||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||
/*font-size: 1.0em !important;*/
|
||||
font-size: 15px !important;
|
||||
}
|
||||
.small-text > p, , .small-text > h1, .small-text > h2 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.big-text, .big-text > p, .big-text > h1, .big-text > h2 {
|
||||
/*font-size: 1.3em !important;*/
|
||||
font-size: 16px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
.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-bottom: 10px;
|
||||
/*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;
|
||||
@@ -133,22 +193,77 @@
|
||||
|
||||
.note-title-display-card {
|
||||
position: relative;
|
||||
box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);
|
||||
margin: 0 15px 15px 0;
|
||||
padding: 1em;
|
||||
/*box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);*/
|
||||
box-shadow: 0 0px 5px 1px rgba(34,36,38,0);
|
||||
margin: 5px;
|
||||
padding: 0.7em 1em;
|
||||
border-radius: .28571429rem;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
width: calc(33.333% - 15px);
|
||||
transition: box-shadow 0.3s;
|
||||
/*width: calc(33.333% - 10px);*/
|
||||
width: calc(25% - 10px);
|
||||
/*transition: box-shadow 0.3s;*/
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 1.8rem;
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
.note-title-display-card:hover {
|
||||
box-shadow: 0 1px 2px -0 rgba(34,36,38,.50);
|
||||
/*box-shadow: 0 3px 6px -0 rgba(34,36,38,.50);*/
|
||||
box-shadow: 0 0px 5px 1px rgba(34,36,38,0.3);
|
||||
}
|
||||
.icon-bar {
|
||||
opacity: 0.8;
|
||||
/*margin-top: -2.2rem;*/
|
||||
}
|
||||
.hover-hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
.little-tag {
|
||||
font-size: 0.7em;
|
||||
padding: 5px 5px;
|
||||
border: 1px solid var(--border_color);
|
||||
margin: 5px 3px 0 0;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
line-height: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.one-column .note-title-display-card {
|
||||
margin-right: 65%;
|
||||
width: 33%;
|
||||
/*margin-right: 65%;*/
|
||||
/*width: 33%;*/
|
||||
width: 100%;
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
@@ -162,6 +277,7 @@
|
||||
.currently-open:after {
|
||||
content: 'Open';
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@@ -176,10 +292,17 @@
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
/* Tweak mobile display to show only one column */
|
||||
@media only screen and (max-width: 740px) {
|
||||
.note-title-display-card {
|
||||
width: 100%;
|
||||
margin: 15px 0 0 0;
|
||||
width: calc(100% + 10px);
|
||||
margin: 0px -5px 10px -5px;
|
||||
}
|
||||
}
|
||||
</style>
|
43
client/src/components/SearchInput.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="ui form">
|
||||
<div class="ui left icon fluid input">
|
||||
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes and Files" />
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
data: function(){
|
||||
return {
|
||||
searchTerm: '',
|
||||
searchTimeout: null,
|
||||
searchDebounceDuration: 300,
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
//search clear
|
||||
this.$bus.$on('reset_fast_filters', () => {
|
||||
this.searchTerm = ''
|
||||
})
|
||||
|
||||
},
|
||||
methods: {
|
||||
searchKeyUp(){
|
||||
clearTimeout(this.searchTimeout)
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.search()
|
||||
}, this.searchDebounceDuration)
|
||||
},
|
||||
search(){
|
||||
this.$bus.$emit('update_search_term', this.searchTerm)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
112
client/src/components/ShareNoteComponent.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="ui grid" v-if="this.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="this.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 {
|
||||
sharedWithUsers: [],
|
||||
shareUserInput: '',
|
||||
debounce: null,
|
||||
enableSubmitShare: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
|
||||
if(this.shareUsername == null){
|
||||
this.loadShareList()
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
loadShareList(){
|
||||
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
|
||||
.then( ({data}) => {
|
||||
this.sharedWithUsers = data
|
||||
})
|
||||
},
|
||||
onRevokeAccess(noteId){
|
||||
axios.post('/api/note/shareremoveuser', {'noteId':noteId})
|
||||
.then( ({data}) => {
|
||||
console.log(data)
|
||||
if(data == true){
|
||||
this.loadShareList()
|
||||
}
|
||||
})
|
||||
},
|
||||
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')
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
148
client/src/components/SideSlideMenuComponent.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<style type="text/css" scoped>
|
||||
.slide-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 55%;
|
||||
bottom: 0;
|
||||
z-index: 400;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_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(--background_color);*/
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.slide-shadow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 50%;
|
||||
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: 399;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modal-fade-enter,
|
||||
.modal-fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity .5s ease;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div>
|
||||
|
||||
<div class="slide-container" :style="{ 'background-color':bgColor, 'color':textColor}">
|
||||
|
||||
<!-- content of the editor -->
|
||||
<div class="slide-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="slide-shadow" v-on:click="close"></div>
|
||||
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SideSlideMenu',
|
||||
props: [ 'name', 'styleObject' ],
|
||||
components: {
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
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()
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy(){
|
||||
},
|
||||
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: {
|
||||
onClickTag(index){
|
||||
console.log('yup')
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
119
client/src/components/SimpleAttachmentNoteComponent.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.img-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.img-row {
|
||||
height: 30vh;
|
||||
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(){
|
||||
axios.post('/api/attachment/search', {'attachmentType':2})
|
||||
.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)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
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){
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,5 +1,6 @@
|
||||
// The Vue build version to load with the `import` command
|
||||
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
import Vuex from 'vuex'
|
||||
@@ -9,10 +10,43 @@ import store from './stores/mainStore';
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
|
||||
require('./assets/semantic-min.css')
|
||||
require('./assets/semantic-helper.css')
|
||||
// Fonts
|
||||
require('./assets/roboto-latin.woff2')
|
||||
require('./assets/roboto-latin-bold.woff2')
|
||||
|
||||
require('./assets/themes/default/assets/fonts/icons.eot')
|
||||
require('./assets/themes/default/assets/fonts/icons.otf')
|
||||
require('./assets/themes/default/assets/fonts/icons.svg')
|
||||
require('./assets/themes/default/assets/fonts/icons.ttf')
|
||||
require('./assets/themes/default/assets/fonts/icons.woff')
|
||||
require('./assets/themes/default/assets/fonts/icons.woff2')
|
||||
|
||||
require('./assets/themes/default/assets/fonts/outline-icons.eot')
|
||||
require('./assets/themes/default/assets/fonts/outline-icons.svg')
|
||||
require('./assets/themes/default/assets/fonts/outline-icons.ttf')
|
||||
require('./assets/themes/default/assets/fonts/outline-icons.woff')
|
||||
require('./assets/themes/default/assets/fonts/outline-icons.woff2')
|
||||
|
||||
require('./assets/squire.js')
|
||||
|
||||
//Import socket io, init using nginx configured socket path
|
||||
import io from 'socket.io-client';
|
||||
const socket = io({ path:'/socket' });
|
||||
|
||||
//integrate connected socket into vue instance
|
||||
Object.defineProperties(Vue.prototype, {
|
||||
$io: {
|
||||
get: () => socket
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// This callback runs before every route change, including on page load.
|
||||
// Sets the title of the page using vue router
|
||||
router.beforeEach((to, from, next) => {
|
||||
|
||||
document.title = to.meta.title;
|
||||
next();
|
||||
});
|
||||
@@ -21,16 +55,10 @@ router.beforeEach((to, from, next) => {
|
||||
import EventBus from './EventBus'
|
||||
import Helpers from './Helpers'
|
||||
|
||||
import CKEditor from '@ckeditor/ckeditor5-vue';
|
||||
Vue.use( CKEditor )
|
||||
|
||||
require('./assets/semantic-min.css')
|
||||
require('./assets/semantic-helper.css')
|
||||
|
||||
Vue.use(Vuex)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
/* eslint-disable no-new */
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
|
108
client/src/pages/AttachmentsPage.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<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="folder open outline icon"></i>
|
||||
<div class="content">
|
||||
Files
|
||||
<div class="sub header">Uploaded Files and Websites from notes.</div>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column" v-if="searchParams.noteId">
|
||||
<div class="ui green button" v-on:click="clearNote">
|
||||
<i class="chevron circle left icon"></i>
|
||||
Show All Attachments
|
||||
</div>
|
||||
<div class="ui green button" v-on:click="openNote">
|
||||
<i class="file outline icon"></i>
|
||||
Open Note
|
||||
</div>
|
||||
</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 axios from 'axios'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
attachments: [],
|
||||
searchParams: {}
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
//
|
||||
// Perform Login check
|
||||
//
|
||||
this.$parent.loginGateway()
|
||||
},
|
||||
mounted: function(){
|
||||
|
||||
//Mount notes on load if note ID is set
|
||||
this.openNoteAttachments()
|
||||
this.searchAttachments()
|
||||
},
|
||||
watch:{
|
||||
$route (to, from){
|
||||
//Open or close notes on route change
|
||||
this.openNoteAttachments()
|
||||
this.searchAttachments()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openNoteAttachments(){
|
||||
if(this.$route.params && this.$route.params.id){
|
||||
const inputNoteId = this.$route.params.id
|
||||
this.searchParams['noteId'] = inputNoteId
|
||||
}
|
||||
},
|
||||
openNote(){
|
||||
const noteId = this.searchParams['noteId']
|
||||
this.$router.push('/notes/open/'+noteId)
|
||||
},
|
||||
clearNote(){
|
||||
this.$router.push('/attachments/')
|
||||
delete this.searchParams.noteId
|
||||
},
|
||||
searchAttachments (){
|
||||
|
||||
axios.post('/api/attachment/search', this.searchParams)
|
||||
.then( results => {
|
||||
this.attachments = results.data
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.attachment-display-area {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 5%;
|
||||
}
|
||||
</style>
|
@@ -1,13 +1,325 @@
|
||||
<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;
|
||||
}
|
||||
.lightly-padded {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.massive-text {
|
||||
color: white;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.blinking {
|
||||
animation:blinkingText 1.5s linear infinite;
|
||||
}
|
||||
@keyframes blinkingText{
|
||||
0%{ opacity: 0.9; }
|
||||
50%{ opacity: 0; }
|
||||
100%{ opacity: 0.9; }
|
||||
}
|
||||
.subtext {
|
||||
border-bottom: 1px solid white;
|
||||
border-right: 1px solid white;
|
||||
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 */
|
||||
.dont-pad-me {
|
||||
margin-right: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui container">
|
||||
<h1>Welcome</h1>
|
||||
<div class="lightly-padded">
|
||||
<div class="ui centered vertically divided stackable grid">
|
||||
|
||||
<div class="row hero fadeBg" :style="{ 'height':(height+'px') }">
|
||||
|
||||
<!-- 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" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/gardening.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/growth.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/icecream.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/investing.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/onboarding.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/robot.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/solution.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/watching.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/cloud.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/grandma.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/hamburger.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/idea.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/notebook.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/plan.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/secure.svg" alt="">
|
||||
<img loading="lazy" width="10%" src="/api/static/assets/marketing/void.svg" alt="">
|
||||
</div>
|
||||
|
||||
<div class="one wide large screen only column"></div>
|
||||
|
||||
<!-- desktop column - large screen only -->
|
||||
<div class="seven wide middle aligned left aligned column">
|
||||
<h2 class="massive-text">Take Notes, <br>Like Never Before</h2>
|
||||
<h3 class="subtext">
|
||||
Using an online note application <i class="i cursor icon blinking"></i>
|
||||
</h3>
|
||||
<p>Assuming you have never used a note application previously in your life.</p>
|
||||
<br>
|
||||
<i class="huge inverted chevron circle down icon"></i>
|
||||
</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>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Everyone has knowledge that need to be expressed</h2>
|
||||
<h3>Utilize action potential to create notes by encoding raw brainwaves converted to written language</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/idea.svg" alt="Explosion of New Ideas">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/gardening.svg" alt="Pruning the mind garden">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Dream it, then do it</h2>
|
||||
<h3>Easily record your unlimited imagination. Ideas, stories, notes, plays, poems anything, that can reasonably be put into text</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered green row">
|
||||
<div class="six wide column">
|
||||
<h2>Unbridled Input</h2>
|
||||
<h3>Revolutionary technology allows the use of any keyboard with up to 395 keys</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/add.svg" alt="A shpere of newness">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/solution.svg" alt="Hypercube of Solutions">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Solutions with the Internet</h2>
|
||||
<h3>With the power to save any combination of letters, you can easily inscribe thoughts</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Search your data</h2>
|
||||
<h3>Type in a word and find that same word but somewhere else</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/cloud.svg" alt="Girl falling into the spiral of digital chaos">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/plan.svg" alt="Scheme for planetary destruction">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Embrace the Void</h2>
|
||||
<h3>Remove unnecessary clutter for your brain and save it to the cloud, allowing you to easily embrace the gaping abyss</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Space for Growth</h2>
|
||||
<h3>Groom a clear path for new expressions and innovations. Elevate your being and lower your cholesterol</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/growth.svg" alt="Endless progress at the cost of sanity and health">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/onboarding.svg" alt="Shrunken man near giant tablet">
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<h2>Become your Data</h2>
|
||||
<h3>We exist as electrical impulses, no different from data on a computer</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- set -->
|
||||
<div class="middle aligned centered row">
|
||||
<div class="six wide column">
|
||||
<h2>Ice Cream</h2>
|
||||
<h3>Get excited without all the screaming</h3>
|
||||
</div>
|
||||
<div class="six wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/icecream.svg" 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 column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/secure.svg" alt="marketing mumbo jumbo">
|
||||
</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 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>
|
||||
OR
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<span class="ui button" v-on:click="showRealInformation">View real information about this site</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="realInformation" class="middle aligned centered row" ref="real">
|
||||
<div class="six wide column">
|
||||
<h2 class="ui center aligned">
|
||||
What is this really?
|
||||
</h2>
|
||||
<h3>Its just a little web app for taking notes. This page is mocking the "over the top" marketing sites use to sell their products.</h3>
|
||||
<p>
|
||||
This App exists because 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>Hero Slide Photo Credit - <a target="_blank" href="https://unsplash.com/@tkaslik14">https://unsplash.com/@tkaslik14</a></p>
|
||||
<p>Generic Marketing Images - <a target="_blank" href="https://undraw.co/">https://unDraw.co/</a></p>
|
||||
</div>
|
||||
<div class="four wide column">
|
||||
<img loading="lazy" width="100%" src="/api/static/assets/marketing/watching.svg" alt="Drinking the blood of the elderly">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WelcomePage'
|
||||
name: 'WelcomePage',
|
||||
data(){
|
||||
return {
|
||||
height: null,
|
||||
realInformation: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
|
||||
|
||||
//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.avidhabit.com')
|
||||
}
|
||||
|
||||
//Don't change hero banner on mobile
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
let windowHeight = window.innerHeight
|
||||
this.height = windowHeight - (windowHeight * 0.10)
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
showRealInformation(){
|
||||
|
||||
|
||||
this.realInformation = !this.realInformation
|
||||
if(this.realInformation){
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.real.scrollIntoView({'behavior':'smooth'})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,41 +1,41 @@
|
||||
<template>
|
||||
<div class="ui container">
|
||||
<h3>Login</h3>
|
||||
<p>Begin the login process by typing your username or email.</p>
|
||||
<p>To create an account, type in the username you want to use followed by the password.</p>
|
||||
<p>You will remain logged in on this browser, until you log out.</p>
|
||||
<div class="ui basic segment no-fluf-segment">
|
||||
<div class="ui grid">
|
||||
<div class="ui sixteen wide column">
|
||||
|
||||
<div class="ui segment" v-on:keyup.enter="submit">
|
||||
<div class="ui large form">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
|
||||
<p><b>Create an account:</b> type in the username you want to use followed by the password.</p>
|
||||
|
||||
<div class="ui segment" v-on:keyup.enter="submit">
|
||||
<div class="ui large form">
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="username" type="text" name="email" placeholder="Username or E-mail address" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Login</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" v-if="username.length > 0">
|
||||
<div class="ui input">
|
||||
<input v-model="password" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="submit" class="ui massive compact fluid green submit button">Login</div>
|
||||
</div>
|
||||
|
||||
<p>You will remain logged in on this browser, until you log out.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
//ajax calls
|
||||
import axios from 'axios';
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data () {
|
||||
return {
|
||||
message: 'Login stuff',
|
||||
enabled: false,
|
||||
username: '',
|
||||
password: ''
|
||||
@@ -61,26 +61,24 @@
|
||||
|
||||
axios.post('/api/user/login', data)
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
if(response.data.success){
|
||||
|
||||
const token = response.data.token
|
||||
const username = response.data.username
|
||||
|
||||
vm.$store.commit('setLoginToken', {token, username})
|
||||
|
||||
//Redirect user to notes section after login
|
||||
this.$router.push('/notes')
|
||||
vm.$router.push('/notes')
|
||||
} else {
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
vm.$store.commit('destroyLoginToken')
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('There was an error with log in request')
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getRudeMessage'
|
||||
])
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,120 +1,157 @@
|
||||
<template>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui basic segment no-fluf-segment">
|
||||
|
||||
<div class="ui equal width grid">
|
||||
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
|
||||
|
||||
<!-- <div class="ui row">{{ $store.getters.getIsUserOnMobile ? 'Mobile Device':'Normal Browser' }}</div> -->
|
||||
<div class="sixteen wide column">
|
||||
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
|
||||
|
||||
<!-- mobile search menu -->
|
||||
<div class="ui mobile only row">
|
||||
<!-- Small screen new note button -->
|
||||
<div class="ui four wide column">
|
||||
<div @click="createNote" class="ui fluid green icon button">
|
||||
<i class="plus icon"></i>
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="six wide column" v-if="!$store.getters.getIsUserOnMobile">
|
||||
<search-input></search-input>
|
||||
</div>
|
||||
|
||||
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
|
||||
|
||||
<div class="ui basic button"
|
||||
v-on:click="updateFastFilters(3)"
|
||||
v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)"
|
||||
style="position: relative;">
|
||||
<i class="green mail icon"></i>Shared Notes
|
||||
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
|
||||
{{ $store.getters.totals['unreadNotes'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ui basic button" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
|
||||
<i class="green archive icon"></i>Archived
|
||||
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
|
||||
</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>Back to All Notes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ui twelve wide column">
|
||||
<div class="ui form">
|
||||
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- search menu -->
|
||||
<div class="ui large screen only row">
|
||||
<h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2>
|
||||
<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
|
||||
<h2 v-if="fastFilters['onlyArchived'] == 1">Archived Notes</h2>
|
||||
<h2 v-if="fastFilters['onlyShowSharedNotes'] == 1">Shared Notes</h2>
|
||||
|
||||
<div class="ui two wide column">
|
||||
<div @click="createNote" class="ui fluid green button">
|
||||
<i class="plus icon"></i>
|
||||
New Note
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui five wide column">
|
||||
<div class="ui form">
|
||||
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui nine wide column">
|
||||
|
||||
<router-link class="ui basic button" to="/help">Help</router-link>
|
||||
|
||||
<div v-on:click="toggleNightMode" class="ui basic icon button">
|
||||
<i class="eye icon"></i> Dark Theme:
|
||||
<span v-if="$store.getters.getIsNightMode">On</span>
|
||||
<span v-else>Off</span>
|
||||
</div>
|
||||
|
||||
<div v-on:click="toggleArchivedVisible" class="ui basic icon button">
|
||||
<i class="archive icon"></i> Archived:
|
||||
<span v-if="showArchived == 1">Visible</span>
|
||||
<span v-else>Hidden</span>
|
||||
</div>
|
||||
|
||||
<div class="ui right floated basic button"
|
||||
data-tooltip="Log Out" data-position="left center"
|
||||
v-on:click="destroyLoginToken"><i class="user icon"></i> {{username}}</div>
|
||||
</div>
|
||||
<div v-if="commonTags.length > 0" class="sixteen wide column">
|
||||
<h4><i class="green tags icon"></i>Tags</h4>
|
||||
<span v-for="tag in commonTags" @click="toggleTagFilter(tag.id)">
|
||||
<span class="ui clickable basic label" :class="{ 'green':(searchTags.includes(tag.id)) }">
|
||||
{{ucWords(tag.text)}} <span class="detail">{{tag.usages}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ui row">
|
||||
<!-- Note title card display -->
|
||||
<div class="sixteen wide column">
|
||||
<h3 v-if="searchTerm.length > 0 && notes.length == 0">No notes found. Check your spelling, try completing the word or using a different phrase.</h3>
|
||||
|
||||
<!-- tags display -->
|
||||
<div class="ui two wide large screen only column" v-if="activeNoteId1 == null && activeNoteId2 == null">
|
||||
<div class="ui small basic fluid button" @click="reset">
|
||||
<i class="undo icon"></i>Reset Filters
|
||||
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0">
|
||||
No Notes Yet. Create one when you feel ready.
|
||||
</h3>
|
||||
|
||||
<!-- <div v-if="working">
|
||||
<div class="ui active inline loader"></div> Working...
|
||||
</div> -->
|
||||
|
||||
<!-- Go to one wide column, do not do this on mobile interface -->
|
||||
<div v-if="notes !== null && notes.length > 0"
|
||||
:class="{'one-column':(
|
||||
(activeNoteId1 != null || activeNoteId2 != null) &&
|
||||
!$store.getters.getIsUserOnMobile
|
||||
)}">
|
||||
|
||||
<!-- pinned notes -->
|
||||
<div v-if="containsPinnednotes > 0" class="note-card-section">
|
||||
<!-- ({{containsPinnednotes}}) -->
|
||||
<h4><i class="green pin icon"></i>Pinned</h4>
|
||||
<div class="note-card-display-area">
|
||||
<note-title-display-card
|
||||
v-for="note in notes"
|
||||
v-if="note.pinned"
|
||||
:onClick="openNote"
|
||||
:data="note"
|
||||
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
|
||||
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui section list">
|
||||
<div class="item" v-for="tag in commonTags" @click="toggleTagFilter(tag.id)">
|
||||
<div class="ui clickable basic fluid large label" :class="{ 'green':(searchTags.includes(tag.id)) }">
|
||||
{{ucWords(tag.text)}} <div class="detail">{{tag.usages}}</div>
|
||||
</div>
|
||||
|
||||
<!-- normal notes -->
|
||||
<div v-if="containsNormalNotes > 0" class="note-card-section">
|
||||
<!-- ({{containsNormalNotes}}) -->
|
||||
<h4><i class="green file icon"></i>Notes</h4>
|
||||
<div class="note-card-display-area">
|
||||
<note-title-display-card
|
||||
v-for="note in notes"
|
||||
v-if="note.note_highlights && !note.pinned"
|
||||
:onClick="openNote"
|
||||
:data="note"
|
||||
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
|
||||
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- found in text -->
|
||||
<div v-if="containsTextResults" class="note-card-section">
|
||||
<h4><i class="green paragraph icon"></i> Found in Text ({{containsTextResults}})</h4>
|
||||
<div class="note-card-display-area">
|
||||
<note-title-display-card
|
||||
v-for="note in notes"
|
||||
v-if="note.note_highlights && note.note_highlights.length"
|
||||
:textResults="true"
|
||||
:onClick="openNote"
|
||||
:data="note"
|
||||
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
|
||||
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note title cards -->
|
||||
<div class="ui fourteen wide computer sixteen wide mobile column">
|
||||
<h2>
|
||||
({{notes.length}}) <fast-filters />
|
||||
</h2>
|
||||
|
||||
<h3 v-if="searchTerm.length > 0 && notes.length == 0">No notes found. Check your spelling, try completing the word or using a different phrase.</h3>
|
||||
|
||||
<h3 v-if="searchTerm.length == 0 && notes.length == 0">Create your first note. Click the "New Note" button.</h3>
|
||||
|
||||
<div v-if="working"><div class="ui active inline loader"></div> Working...</div>
|
||||
|
||||
<div v-if="notes !== null && !working"
|
||||
class="note-card-display-area"
|
||||
:class="{'one-column':(activeNoteId1 != null || activeNoteId2 != null )}
|
||||
">
|
||||
<note-title-display-card
|
||||
v-for="note in notes"
|
||||
:onClick="openNote"
|
||||
:data="note"
|
||||
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
|
||||
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- found attachments -->
|
||||
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
|
||||
<h4><i class="folder open outline icon"></i> Found in Files ({{ foundAttachments.length }})</h4>
|
||||
<attachment-display
|
||||
v-for="item in foundAttachments"
|
||||
:item="item"
|
||||
:key="item.id"
|
||||
:search-params="{}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" />
|
||||
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" />
|
||||
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" ref="note1" />
|
||||
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" ref="note2" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'SearchBar',
|
||||
@@ -122,60 +159,162 @@
|
||||
'input-notes': require('@/components/NoteInputPanel.vue').default,
|
||||
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
|
||||
'fast-filters': require('@/components/FastFilters.vue').default,
|
||||
'search-input': require('@/components/SearchInput.vue').default,
|
||||
'attachment-display': require('@/components/AttachmentDisplayCard').default,
|
||||
'counter':require('@/components/AnimatedCounterComponent.vue').default
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
username:'',
|
||||
initComponent: true,
|
||||
commonTags: [],
|
||||
searchTerm: '',
|
||||
searchTags: [],
|
||||
notes: [],
|
||||
highlights: [],
|
||||
searchDebounce: null,
|
||||
fastFilters: {},
|
||||
showArchived: 0,
|
||||
working: false,
|
||||
//Load up notes in batches
|
||||
firstLoadBatchSize: 30, //First set of rapidly loaded notes
|
||||
batchSize: 100, //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,
|
||||
fetchTags: false,
|
||||
|
||||
//Clear button is not visible
|
||||
showClear: false,
|
||||
initialPostData: null,
|
||||
currentPostData: null,
|
||||
|
||||
containsNormalNotes: 0,
|
||||
containsPinnednotes: 0,
|
||||
containsTextResults: 0,
|
||||
// containsTagResults: 0,
|
||||
// containsAttachmentResults: 0,
|
||||
|
||||
//Currently open notes in app
|
||||
activeNoteId1: null,
|
||||
activeNoteId2: null,
|
||||
|
||||
//Position determines how note is Positioned
|
||||
activeNote1Position: 0,
|
||||
activeNote2Position: 0,
|
||||
|
||||
lastVisibilityState: null,
|
||||
|
||||
foundAttachments: [],
|
||||
|
||||
noteSections: {
|
||||
'pinned': {},
|
||||
'archived': {},
|
||||
'recieved': {},
|
||||
'sent':{},
|
||||
'notes':{},
|
||||
'textMatch':{}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
let username = this.$store.getters.getUsername
|
||||
this.username = this.ucWords(username)
|
||||
this.$parent.loginGateway()
|
||||
|
||||
this.$bus.$on('close_active_note', position => {
|
||||
//Update totals for app
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
|
||||
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
|
||||
this.closeNote(position)
|
||||
this.$store.dispatch('fetchAndUpdateUserTotals')
|
||||
if(modified){
|
||||
this.updateSingleNote(noteId)
|
||||
}
|
||||
|
||||
})
|
||||
this.$bus.$on('note_deleted', () => {
|
||||
this.search()
|
||||
this.$bus.$on('note_deleted', (noteId) => {
|
||||
//Remove deleted note from set, its deleted
|
||||
this.notes.forEach( (note, index) => {
|
||||
if(note.id == noteId){
|
||||
if(note.pinned == 1){
|
||||
this.containsPinnednotes--
|
||||
} else {
|
||||
this.containsNormalNotes--
|
||||
}
|
||||
this.notes.splice(index, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
this.$bus.$on('update_fast_filters', newFilter => {
|
||||
this.fastFilters = newFilter
|
||||
this.search()
|
||||
//Fast filters always return all the results and tags
|
||||
this.search(true, this.batchSize, false).then( () => {
|
||||
return this.fetchUserTags()
|
||||
})
|
||||
})
|
||||
|
||||
//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()
|
||||
|
||||
return this.fetchUserTags()
|
||||
})
|
||||
})
|
||||
|
||||
//New note button pushes open note event
|
||||
this.$bus.$on('open_note', noteId => {
|
||||
this.openNote(noteId)
|
||||
})
|
||||
|
||||
//Reload page content
|
||||
this.$bus.$on('note_reload', () => {
|
||||
this.reset()
|
||||
})
|
||||
|
||||
//Mount notes on load if note ID is set
|
||||
if(this.$route.params && this.$route.params.id){
|
||||
const id = this.$route.params.id
|
||||
console.log('About to load note ', id)
|
||||
this.openNote(id)
|
||||
}
|
||||
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);
|
||||
|
||||
},
|
||||
beforeDestroy(){
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
window.removeEventListener('hashchange', this.hashChangeAction)
|
||||
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
|
||||
|
||||
//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() {
|
||||
|
||||
this.search()
|
||||
|
||||
//Loads initial batch and tags
|
||||
this.reset()
|
||||
},
|
||||
methods: {
|
||||
openNote(id){
|
||||
showOneColumn(){
|
||||
//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 }
|
||||
}
|
||||
|
||||
//Do not open same note twice
|
||||
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
|
||||
@@ -196,7 +335,6 @@
|
||||
this.activeNote2Position = 2 //Left side of page
|
||||
return
|
||||
}
|
||||
|
||||
//2 notes open
|
||||
if(this.activeNoteId2 != null && this.activeNoteId1 == null){
|
||||
this.activeNoteId1 = id
|
||||
@@ -219,12 +357,17 @@
|
||||
this.activeNoteId2 = null
|
||||
}
|
||||
|
||||
this.$router.push('/notes')
|
||||
//IF two notes get opened, update ID of open note
|
||||
if(this.activeNoteId1 || this.activeNoteId2){
|
||||
this.$router.push('/notes/open/'+Math.max(this.activeNoteId1,this.activeNoteId2))
|
||||
} else {
|
||||
//No notes are open, just show notes page
|
||||
this.$router.push('/notes')
|
||||
}
|
||||
|
||||
this.activeNote1Position = 0
|
||||
this.activeNote2Position = 0
|
||||
|
||||
this.search(false)
|
||||
},
|
||||
toggleTagFilter(tagId){
|
||||
|
||||
@@ -234,55 +377,261 @@
|
||||
this.searchTags.push(tagId)
|
||||
}
|
||||
|
||||
this.search()
|
||||
},
|
||||
search(showLoading = true){
|
||||
|
||||
//Add archived to fast filters
|
||||
this.fastFilters['archived'] = 0
|
||||
if(this.showArchived == 1){
|
||||
this.fastFilters['archived'] = 1
|
||||
//Reset note set and load up notes and tags
|
||||
if(this.searchTags.length > 0){
|
||||
this.search(true, this.batchSize)
|
||||
return
|
||||
}
|
||||
|
||||
let postData = {
|
||||
//If no tags are selected, reset entire page
|
||||
this.reset()
|
||||
|
||||
},
|
||||
onScroll(e){
|
||||
|
||||
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 )
|
||||
|
||||
// console.log( percentageDown + '%' )
|
||||
|
||||
//If greater than 80 of the way down the page, load the next batch
|
||||
if(percentageDown >= 80){
|
||||
|
||||
this.search(false, this.batchSize, true)
|
||||
}
|
||||
|
||||
}, 50)
|
||||
|
||||
|
||||
return
|
||||
},
|
||||
//Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123
|
||||
hashChangeAction(event){
|
||||
|
||||
//Clean up path of hash change
|
||||
let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash
|
||||
let newPath = event.newURL.replace(path,'')
|
||||
let oldPath = event.oldURL.replace(path,'')
|
||||
|
||||
//If we go from open note ID to no note ID, close the note
|
||||
if(newPath == '' && oldPath.indexOf('/open/') != -1){
|
||||
//Pull note ID out of URL
|
||||
const noteIdToClose = oldPath.split('/').pop()
|
||||
|
||||
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
|
||||
this.$refs.note1.close()
|
||||
}
|
||||
|
||||
if(this.$refs.note2 && this.$refs.note2.currentNoteId == noteIdToClose){
|
||||
this.$refs.note2.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
visibiltyChangeAction(event){
|
||||
|
||||
//@TODO - set a timeout on this like 2 minutes or just dont do shit and update it via socket.io
|
||||
//If user leaves page then returns to page, reload the first batch
|
||||
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
|
||||
// console.log('Welcome back. Reloading a batch')
|
||||
//Load initial batch, then tags, then other batch
|
||||
this.search(false, this.firstLoadBatchSize)
|
||||
.then( () => {
|
||||
return this.fetchUserTags()
|
||||
})
|
||||
}
|
||||
|
||||
this.lastVisibilityState = document.visibilityState
|
||||
|
||||
},
|
||||
// @TODO Don't even trigger this if the note wasn't changed
|
||||
updateSingleNote(noteId){
|
||||
|
||||
//Lookup one note using passed in ID
|
||||
const postData = {
|
||||
searchQuery: this.searchTerm,
|
||||
searchTags: this.searchTags,
|
||||
fastFilters: this.fastFilters,
|
||||
fastFilters:{
|
||||
noteIdSet:[noteId]
|
||||
}
|
||||
}
|
||||
|
||||
if(showLoading){
|
||||
this.working = true
|
||||
}
|
||||
axios.post('/api/note/search', postData)
|
||||
.then(results => {
|
||||
|
||||
//Pull note data out of note set
|
||||
let newNote = results.data.notes[0]
|
||||
let foundNote = false
|
||||
|
||||
//Perform search
|
||||
let vm = this
|
||||
axios.post('/api/note/search', postData).
|
||||
then(response => {
|
||||
vm.commonTags = response.data.tags
|
||||
vm.notes = response.data.notes
|
||||
vm.highlights = response.data.highlights
|
||||
this.working = false
|
||||
if(newNote === undefined){
|
||||
console.log('Note not visible on this page')
|
||||
return
|
||||
}
|
||||
|
||||
//Go through each note and find the one just updated
|
||||
this.notes.forEach( (note,index) => {
|
||||
|
||||
if(note.id == noteId){
|
||||
foundNote = true
|
||||
|
||||
//Don't move notes that were not changed
|
||||
if(note.updated == newNote.updated){
|
||||
return
|
||||
}
|
||||
|
||||
//Compare note tags, if they changed, reload tags
|
||||
if(newNote.tag_count != note.tag_count){
|
||||
console.log('Tags changed, update those bitches')
|
||||
this.fetchUserTags()
|
||||
}
|
||||
|
||||
//go through each prop and update it with new values
|
||||
Object.keys(newNote).forEach(prop => {
|
||||
note[prop] = newNote[prop]
|
||||
})
|
||||
|
||||
this.notes.splice(index, 1)
|
||||
this.notes.unshift(note)
|
||||
}
|
||||
})
|
||||
|
||||
//This note was not found, update it in list
|
||||
if(foundNote == false){
|
||||
if(newNote.pinned == 1){
|
||||
this.containsPinnednotes++
|
||||
} else {
|
||||
this.containsNormalNotes++
|
||||
}
|
||||
this.notes.unshift(newNote)
|
||||
}
|
||||
})
|
||||
},
|
||||
searchAttachments(){
|
||||
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
|
||||
.then(results => {
|
||||
console.log('Attachment Results')
|
||||
console.log(results.data)
|
||||
this.foundAttachments = results.data
|
||||
})
|
||||
},
|
||||
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Don't double load note batches
|
||||
if(this.loadingInProgress){
|
||||
return resolve()
|
||||
}
|
||||
|
||||
//Reset a lot of stuff if we are not merging batches
|
||||
if(!mergeExisting){
|
||||
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
|
||||
// this.commonTags = [] //Don't reset tags, if search returns tags, they will be set
|
||||
}
|
||||
|
||||
//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,
|
||||
}
|
||||
|
||||
if(showLoading){
|
||||
this.working = true
|
||||
}
|
||||
|
||||
//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 => {
|
||||
|
||||
//Save the number of notes just loaded
|
||||
this.batchOffset += response.data.notes.length
|
||||
|
||||
//Mush the two new sets of data together (set will be empty is reset is on)
|
||||
if(response.data.tags.length > 0){
|
||||
this.commonTags = response.data.tags
|
||||
}
|
||||
|
||||
//Either reload all notes with return data or merge return data
|
||||
if(!mergeExisting){
|
||||
this.notes = response.data.notes
|
||||
} else {
|
||||
this.notes = this.notes.concat(response.data.notes)
|
||||
}
|
||||
|
||||
//Go through each note and see which section to display
|
||||
let textResultsCount = 0
|
||||
let pinnedResultsCount = 0
|
||||
let normalNotesCount = 0
|
||||
response.data.notes.forEach(note => {
|
||||
|
||||
if(note.note_highlights.length > 0){
|
||||
textResultsCount++
|
||||
return
|
||||
}
|
||||
|
||||
if(note.pinned == 1){
|
||||
pinnedResultsCount++
|
||||
return
|
||||
}
|
||||
|
||||
normalNotesCount++
|
||||
})
|
||||
|
||||
if(!mergeExisting){
|
||||
this.containsNormalNotes = normalNotesCount
|
||||
this.containsPinnednotes = pinnedResultsCount
|
||||
this.containsTextResults = textResultsCount
|
||||
} else {
|
||||
this.containsNormalNotes += normalNotesCount
|
||||
this.containsPinnednotes += pinnedResultsCount
|
||||
this.containsTextResults += textResultsCount
|
||||
}
|
||||
|
||||
this.working = false
|
||||
this.loadingInProgress = false
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
},
|
||||
searchKeyUp(){
|
||||
let vm = this
|
||||
clearTimeout(vm.searchDebounce)
|
||||
vm.searchDebounce = setTimeout(() => {
|
||||
vm.search()
|
||||
}, 300)
|
||||
},
|
||||
createNote(event){
|
||||
const title = ''
|
||||
let vm = this
|
||||
|
||||
axios.post('/api/note/create', {title})
|
||||
.then(response => {
|
||||
|
||||
if(response.data && response.data.id){
|
||||
vm.openNote(response.data.id)
|
||||
}
|
||||
})
|
||||
this.search(true, this.batchSize)
|
||||
.then( () => {
|
||||
return this.fetchUserTags()
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
ucWords(str){
|
||||
return (str + '')
|
||||
@@ -291,31 +640,74 @@
|
||||
})
|
||||
},
|
||||
reset(){
|
||||
this.showClear = false
|
||||
this.searchTerm = ''
|
||||
this.searchTags = []
|
||||
this.fastFilters = {}
|
||||
this.foundAttachments = [] //Remove all attachments
|
||||
this.$bus.$emit('reset_fast_filters')
|
||||
this.search()
|
||||
|
||||
//Load initial batch, then tags, then other batch
|
||||
this.search(true, this.firstLoadBatchSize)
|
||||
.then( () => {
|
||||
return this.fetchUserTags()
|
||||
})
|
||||
.then( () => {
|
||||
//Load a larger batch once first batch has loaded
|
||||
return this.search(false, this.batchSize, true)
|
||||
})
|
||||
.then( i => {
|
||||
//Thats how you promise chain
|
||||
})
|
||||
},
|
||||
destroyLoginToken() {
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push('/')
|
||||
fetchUserTags(){
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let postData = {
|
||||
searchQuery: this.searchTerm,
|
||||
searchTags: this.searchTags,
|
||||
fastFilters: this.fastFilters,
|
||||
}
|
||||
|
||||
axios.post('/api/tag/usertags', postData)
|
||||
.then( ({data}) => {
|
||||
this.commonTags = data
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleNightMode(){
|
||||
this.$store.commit('toggleNightMode')
|
||||
},
|
||||
toggleArchivedVisible(){
|
||||
if(this.showArchived == 0){
|
||||
this.showArchived = 1
|
||||
} else {
|
||||
this.showArchived = 0
|
||||
updateFastFilters(index){
|
||||
|
||||
//clear out tags
|
||||
this.searchTags = []
|
||||
|
||||
//A little hacky, brings user to notes page then filters on click
|
||||
if(this.$route.name != 'NotesPage'){
|
||||
this.$router.push('/notes')
|
||||
setTimeout( () => {
|
||||
this.updateFastFilters(index)
|
||||
}, 500 )
|
||||
}
|
||||
this.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
|
||||
]
|
||||
|
||||
let filter = {}
|
||||
filter[options[index]] = 1
|
||||
|
||||
this.$bus.$emit('update_fast_filters', filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.mush-it-up {
|
||||
width: calc(50% - 130px);
|
||||
}
|
||||
.detail {
|
||||
float: right;
|
||||
}
|
||||
@@ -323,4 +715,14 @@
|
||||
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>
|
127
client/src/pages/QuickPage.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<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="paper plane outline icon"></i>
|
||||
<div class="content">
|
||||
Quick
|
||||
<div class="sub header">Add new information with great speed</div>
|
||||
</div>
|
||||
</h2>
|
||||
</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 (CRTL + 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>
|
||||
Quick Files + Links
|
||||
</div>
|
||||
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
|
||||
<i class="file outline icon"></i>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fun" v-html="savedQuickNoteText"></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
|
||||
}
|
||||
},
|
||||
beforeCreate: function(){
|
||||
//
|
||||
// Perform Login check
|
||||
//
|
||||
this.$parent.loginGateway()
|
||||
},
|
||||
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: {
|
||||
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';
|
||||
|
||||
//If command+enter or control+enter is pressed, submit
|
||||
if((event.metaKey || event.ctrlKey) && [13].includes(event.keyCode)){
|
||||
this.appendQuickNote()
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
this.savedQuickNoteText = results.data.text
|
||||
this.quickNoteId = results.data.id
|
||||
})
|
||||
},
|
||||
getQuickNote (){
|
||||
axios.post('/api/quick-note/get')
|
||||
.then( results => {
|
||||
this.savedQuickNoteText = results.data.text
|
||||
this.quickNoteId = results.data.id
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
49
client/src/pages/SharePage.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui container">
|
||||
<div class="fun" :style="{'color':color}" v-if="noteText" v-html="noteText"></div>
|
||||
</div>
|
||||
<div class="ui basic segment"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'SharePage',
|
||||
data(){
|
||||
return {
|
||||
noteText: null,
|
||||
color: '#000'
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
//Mount notes on load if note ID is set
|
||||
if(this.$route.params && this.$route.params.id){
|
||||
const id = this.$route.params.id
|
||||
this.openNote(id)
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
openNote(noteId){
|
||||
axios.post('/api/public/note', {'noteId': noteId})
|
||||
.then( response => {
|
||||
|
||||
let colors = JSON.parse(response.data.color)
|
||||
|
||||
if(colors && colors.noteBackground){
|
||||
document.body.style.background = colors.noteBackground
|
||||
}
|
||||
if(colors && colors.noteText){
|
||||
this.color = colors.noteText
|
||||
}
|
||||
|
||||
this.noteText = response.data.text
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
@@ -1,10 +1,23 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
import HomePage from '@/pages/HomePage'
|
||||
import LoginPage from '@/pages/LoginPage'
|
||||
//Breaking components into function sections allows webpack to load them dynamically
|
||||
//import HomePage from '@/pages/HomePage'
|
||||
const HomePage = () => import('@/pages/HomePage')
|
||||
|
||||
// import LoginPage from '@/pages/LoginPage'
|
||||
const LoginPage = () => import('@/pages/LoginPage')
|
||||
|
||||
// import HelpPage from '@/pages/HelpPage'
|
||||
const HelpPage = () => import('@/pages/HelpPage')
|
||||
|
||||
// import SharePage from '@/pages/SharePage'
|
||||
const SharePage = () => import('@/pages/SharePage')
|
||||
|
||||
//These guys can all be loaded as a chunk
|
||||
import NotesPage from '@/pages/NotesPage'
|
||||
import HelpPage from '@/pages/HelpPage'
|
||||
import QuickPage from '@/pages/QuickPage'
|
||||
import AttachmentsPage from '@/pages/AttachmentsPage'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
@@ -40,5 +53,29 @@ export default new Router({
|
||||
meta: {title:'Help'},
|
||||
component: HelpPage
|
||||
},
|
||||
{
|
||||
path: '/share/:id',
|
||||
name: 'Share',
|
||||
meta: {title:'Shared'},
|
||||
component: SharePage
|
||||
},
|
||||
{
|
||||
path: '/quick',
|
||||
name: 'Quick',
|
||||
meta: {title:'Quick'},
|
||||
component: QuickPage
|
||||
},
|
||||
{
|
||||
path: '/attachments',
|
||||
name: 'Attachments',
|
||||
meta: {title:'Attachments'},
|
||||
component: AttachmentsPage
|
||||
},
|
||||
{
|
||||
path: '/attachments/note/:id',
|
||||
name: 'Attachments',
|
||||
meta: {title:'Attachments'},
|
||||
component: AttachmentsPage
|
||||
},
|
||||
]
|
||||
})
|
||||
|
@@ -6,17 +6,15 @@ Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
count: 0,
|
||||
message: 'Get out me yard ya wankers',
|
||||
token: null,
|
||||
username: null,
|
||||
nightMode: false,
|
||||
isUserOnMobile: false,
|
||||
isNoteSettingsOpen: false, //Little note settings pane
|
||||
socket: null,
|
||||
userTotals: null,
|
||||
},
|
||||
mutations: {
|
||||
increment (state) {
|
||||
state.count++
|
||||
},
|
||||
setLoginToken(state, userData){
|
||||
|
||||
const username = userData.username
|
||||
@@ -29,7 +27,7 @@ export default new Vuex.Store({
|
||||
localStorage.setItem('username', username)
|
||||
|
||||
//Set default token to axios, every request will have header
|
||||
axios.defaults.headers.common['Authorization'] = token
|
||||
axios.defaults.headers.common['authorizationtoken'] = token
|
||||
|
||||
state.token = token
|
||||
state.username = username
|
||||
@@ -39,7 +37,7 @@ export default new Vuex.Store({
|
||||
//Remove login token from local storage and from headers
|
||||
localStorage.removeItem('loginToken')
|
||||
localStorage.removeItem('username')
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
delete axios.defaults.headers.common['authorizationtoken']
|
||||
state.token = null
|
||||
state.username = null
|
||||
},
|
||||
@@ -54,6 +52,7 @@ export default new Vuex.Store({
|
||||
'background_color': '#fff',
|
||||
'text_color': '#3d3d3d',
|
||||
'outline_color': 'rgba(34,36,38,.15)',
|
||||
'border_color': 'rgba(34,36,38,.20)',
|
||||
}
|
||||
//Night mode colors
|
||||
if(state.nightMode){
|
||||
@@ -61,6 +60,7 @@ export default new Vuex.Store({
|
||||
'background_color': '#000',
|
||||
'text_color': '#a98457',
|
||||
'outline_color': '#a98457',
|
||||
'border_color': '#a98457',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +80,27 @@ export default new Vuex.Store({
|
||||
state.isUserOnMobile = true
|
||||
}
|
||||
})(navigator.userAgent||navigator.vendor||window.opera, state);
|
||||
},
|
||||
toggleNoteSettingsPane(state){
|
||||
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
|
||||
},
|
||||
setSocketIoSocket(state, socket){
|
||||
|
||||
//Put socket id in axios headers
|
||||
axios.defaults.headers.common['socketId'] = socket
|
||||
state.socket = socket
|
||||
},
|
||||
setUserTotals(state, totalsObject){
|
||||
//Save all the totals for the user
|
||||
state.userTotals = totalsObject
|
||||
|
||||
// Object.keys(totalsObject).forEach( key => {
|
||||
// console.log(key + ' -- ' + totalsObject[key])
|
||||
// })
|
||||
}
|
||||
|
||||
},
|
||||
getters: {
|
||||
getRudeMessage: state => {
|
||||
return state.message
|
||||
},
|
||||
getUsername: state => {
|
||||
return state.username
|
||||
},
|
||||
@@ -102,6 +116,23 @@ export default new Vuex.Store({
|
||||
},
|
||||
getIsUserOnMobile: state => {
|
||||
return state.isUserOnMobile
|
||||
},
|
||||
getIsNoteSettingsOpen: state => {
|
||||
return state.isNoteSettingsOpen
|
||||
},
|
||||
getSocket: state => {
|
||||
return state.socket
|
||||
},
|
||||
totals: state => {
|
||||
return state.userTotals
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchAndUpdateUserTotals ({ commit }) {
|
||||
axios.post('/api/user/totals')
|
||||
.then( ({data}) => {
|
||||
commit('setUserTotals', data)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
@@ -1,32 +1,31 @@
|
||||
##
|
||||
#
|
||||
# This is just a mock config file, describing what is needed to run the app
|
||||
# The app currently only needs two paths / and /api
|
||||
#
|
||||
##
|
||||
|
||||
#
|
||||
# This is needed to define any ports the app may use from node
|
||||
#
|
||||
upstream expressapp {
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name logiclabs.icu;
|
||||
root /home/mab/pi/client/dist;
|
||||
|
||||
access_log /var/log/nginx/httpslocalhost.access.log;
|
||||
error_log /var/log/nginx/httpslocalhost.error.log;
|
||||
|
||||
#
|
||||
# Needed to server up static, compiled JS files and index.html
|
||||
#
|
||||
location / {
|
||||
autoindex on;
|
||||
#try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /app {
|
||||
proxy_pass http://127.0.0.1:8444;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
location /api {
|
||||
#
|
||||
# define the api route to connect to the backend and serve up static files
|
||||
#
|
||||
location /api {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
@@ -34,15 +33,6 @@ server {
|
||||
|
||||
proxy_pass http://expressapp;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
location /solr {
|
||||
proxy_pass http://127.0.0.1:8983;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -10,3 +10,5 @@ common.js
|
||||
*/unminified/bundle.*
|
||||
bundle.*
|
||||
client/dist*
|
||||
server/public/*
|
||||
client/dist*
|
@@ -12,10 +12,14 @@
|
||||
"body-parser": "^1.18.3",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"express": "^4.16.4",
|
||||
"gm": "^1.23.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"multer": "^1.4.2",
|
||||
"mysql2": "^1.6.5",
|
||||
"node-tesseract-ocr": "^1.0.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"socket.io": "^2.3.0",
|
||||
"solr-node": "^1.2.1"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
|
31
server/ecosystem.config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
apps : [{
|
||||
name: 'NoteServer',
|
||||
script: 'index.js',
|
||||
|
||||
// Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
|
||||
// args: 'one two',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: true,
|
||||
ignore_watch : ["node_modules", "staticFiles"],
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
}],
|
||||
|
||||
deploy : {
|
||||
production : {
|
||||
user : 'node',
|
||||
host : '212.83.163.1',
|
||||
ref : 'origin/master',
|
||||
repo : 'git@github.com:repo.git',
|
||||
path : '/var/www/production',
|
||||
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production'
|
||||
}
|
||||
}
|
||||
};
|
2236
server/helpers/DiffMatchPatch.js
Normal file
161
server/helpers/ProcessText.js
Normal file
@@ -0,0 +1,161 @@
|
||||
|
||||
let ProcessText = module.exports = {}
|
||||
|
||||
ProcessText.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()
|
||||
}
|
||||
|
||||
ProcessText.getUrlsFromString = (string) => {
|
||||
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
|
||||
return string.match(urlPattern)
|
||||
}
|
||||
|
||||
/*
|
||||
Pulls out title and subtext of note
|
||||
+ Title is always first line
|
||||
+ Empty lines are skipped
|
||||
+ URLs are turned into links
|
||||
+ All URLs are givent the target="_blank" property
|
||||
+ Lists are given extra display characters
|
||||
+ If note starts as a list, skip the title
|
||||
*/
|
||||
|
||||
ProcessText.deduceNoteTitle = (inString) => {
|
||||
|
||||
let title = '' //Title of note
|
||||
let sub = '' //sub text below note
|
||||
|
||||
if(!inString || inString == null || inString.length == 0){
|
||||
return {title, sub}
|
||||
}
|
||||
|
||||
//Remove inline styles that may be added by editor
|
||||
inString = inString.replace(/style=".*?"/g,'')
|
||||
|
||||
//Match full line and closing tag or just closing tag
|
||||
let lines = inString.match(/[<[a-zA-Z0-9]+>(.*?)<\/[a-zA-Z0-9]+>|<\/[a-zA-Z0-9>]+?>/g)
|
||||
if(lines == null){ lines = [inString] }
|
||||
//.match(/[^\r\n]+/g) //Match return or newline
|
||||
|
||||
// console.log(lines)
|
||||
|
||||
let finalLines = []
|
||||
|
||||
const startTags = ['<ol','<li','<ul']
|
||||
const endTags = ['</o','</l','</u']
|
||||
|
||||
let totalLines = Math.min(lines.length, 6)
|
||||
let charLimit = 250
|
||||
let listStart = false
|
||||
let noTitleJustList = false
|
||||
|
||||
for(let i=0; i < totalLines; i++){
|
||||
|
||||
//Just in case 'i' gets bigger than array
|
||||
if(lines[i] === undefined){
|
||||
continue
|
||||
}
|
||||
|
||||
//Various empty chars are possible
|
||||
const cleanLine = ProcessText.removeHtml(lines[i])
|
||||
.replace('<br>','')
|
||||
.trim()
|
||||
const lineStart = lines[i].trim().substring(0, 3)
|
||||
charLimit -= cleanLine.length
|
||||
|
||||
//Close out list if char limit is hit
|
||||
if(charLimit <= 0 && listStart){
|
||||
finalLines.push(lines[i])
|
||||
break
|
||||
}
|
||||
|
||||
//Images appear as empty, push em!
|
||||
if(cleanLine.length == 0 && lines[i].indexOf('<img') != -1){
|
||||
finalLines.push(lines[i])
|
||||
continue
|
||||
}
|
||||
|
||||
//Check if note starts with a list, don't include title, just show list
|
||||
if(finalLines.length == 0 && startTags.includes(lineStart)){
|
||||
noTitleJustList = true
|
||||
}
|
||||
|
||||
//Empty line, may be a list open or close
|
||||
if(cleanLine.length == 0 && (startTags.includes(lineStart) || endTags.includes(lineStart) )){
|
||||
if(listStart == false){
|
||||
charLimit = 400 //Double size for list notes
|
||||
}
|
||||
finalLines.push(lines[i])
|
||||
totalLines++
|
||||
listStart = true
|
||||
continue
|
||||
}
|
||||
|
||||
//If line is part of a list, up counter, we want the whole list
|
||||
if(startTags.includes(lineStart)){
|
||||
totalLines++
|
||||
}
|
||||
|
||||
//Skip empty lines
|
||||
if(!cleanLine || cleanLine.length == 0){
|
||||
totalLines++
|
||||
continue
|
||||
}
|
||||
|
||||
//turn urls into links, don't process if its already an <a href=
|
||||
const containsUrls = ProcessText.getUrlsFromString(cleanLine)
|
||||
if(containsUrls && containsUrls.length == 1 && lines[i].indexOf('</a>') == -1){
|
||||
const url = containsUrls[0]
|
||||
lines[i] = lines[i].replace(url, `<a href="${url}">${url}</a>`)
|
||||
}
|
||||
|
||||
//Insert target=_blank into links if set, do it for every link in line
|
||||
if(lines[i].indexOf('</a>') > 0){
|
||||
lines[i] = lines[i].replace(/<a /g, '<a target="_blank" ')
|
||||
}
|
||||
|
||||
//Limit output characters
|
||||
//Check character limit
|
||||
if(charLimit <= 0 && listStart == false){
|
||||
|
||||
//Cut the string down to character limit
|
||||
const cutString = lines[i].substring(0, lines[i].length+charLimit)
|
||||
//Find last space and cut off everything after it
|
||||
let cleanCutString = cutString.substring(0, cutString.lastIndexOf(' '))
|
||||
|
||||
//Some strings may not contain a space resulting in no string
|
||||
if(cleanCutString.length == 0){
|
||||
cleanCutString = cutString
|
||||
}
|
||||
|
||||
finalLines.push(cleanCutString + '...')
|
||||
break;
|
||||
}
|
||||
|
||||
finalLines.push(lines[i])
|
||||
|
||||
}
|
||||
|
||||
//Pull out title if its not an empty string
|
||||
if(ProcessText.removeHtml(finalLines[0]).trim().replace(' ','').length > 0 && !noTitleJustList){
|
||||
title = finalLines.shift()
|
||||
}
|
||||
|
||||
sub = finalLines.join('')
|
||||
|
||||
//Return final display lengths
|
||||
let titleLength = ProcessText.removeHtml(title).trim().replace(' ','').length
|
||||
let subtextLength = ProcessText.removeHtml(sub).trim().replace(' ','').length
|
||||
|
||||
|
||||
return { title, sub, titleLength, subtextLength }
|
||||
}
|
105
server/index.js
@@ -7,20 +7,96 @@ const express = require('express')
|
||||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
var http = require('http').createServer(app);
|
||||
var io = require('socket.io')(http, {
|
||||
path:'/socket'
|
||||
});
|
||||
|
||||
// Make io accessible to our router
|
||||
app.use(function(req,res,next){
|
||||
req.io = io;
|
||||
next();
|
||||
});
|
||||
|
||||
io.on('connection', function(socket){
|
||||
|
||||
// console.log('New user ', socket.id)
|
||||
|
||||
//When a user connects, add them to their own room
|
||||
// This allows the server to emit events to that specific user
|
||||
// access socket.io in the controller with req.io
|
||||
socket.on('user_connect', token => {
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
socket.join(userData.id)
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('join_room', roomId => {
|
||||
// console.log('Join room ', roomId)
|
||||
socket.join(roomId)
|
||||
|
||||
const usersInRoom = io.sockets.adapter.rooms[roomId]
|
||||
if(usersInRoom){
|
||||
// console.log('Users in room', usersInRoom.length)
|
||||
io.to(roomId).emit('update_user_count', usersInRoom.length)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('leave_room', roomId => {
|
||||
socket.leave(roomId)
|
||||
// console.log('User Left room')
|
||||
|
||||
const usersInRoom = io.sockets.adapter.rooms[roomId]
|
||||
if(usersInRoom){
|
||||
// console.log('Users in room', usersInRoom.length)
|
||||
io.to(roomId).emit('update_user_count', usersInRoom.length)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
socket.on('note_diff', data => {
|
||||
|
||||
//Each user joins a room when they open the app.
|
||||
io.in(data.id).clients((error, clients) => {
|
||||
if (error) throw error;
|
||||
//Go through each client in note room and send them the diff
|
||||
clients.forEach(socketId => {
|
||||
if(socketId != socket.id){
|
||||
io.to(socketId).emit('incoming_diff', data.diff)
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
socket.on('disconnect', function(){
|
||||
// console.log('user disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
http.listen(3001, function(){
|
||||
console.log('socket.io liseting on port 3001');
|
||||
});
|
||||
|
||||
//Enable json body parsing in requests. Allows me to post data in ajax calls
|
||||
app.use(express.json())
|
||||
app.use(express.json({limit: '2mb'}))
|
||||
|
||||
|
||||
//Prefix defied by route in nginx config
|
||||
const prefix = '/api'
|
||||
|
||||
//App Auth, all requests will come in with a token, decode the token and set global var
|
||||
app.use(function(req, res, next){
|
||||
|
||||
let token = req.headers.authorization
|
||||
//auth token set by axios in headers
|
||||
let token = req.headers.authorizationtoken
|
||||
if(token && token != null && typeof token === 'string'){
|
||||
Auth.decodeToken(token)
|
||||
.then(userData => {
|
||||
|
||||
req.headers.userId = userData.id //Update headers for the rest of the application
|
||||
next()
|
||||
}).catch(error => {
|
||||
@@ -36,17 +112,32 @@ app.use(function(req, res, next){
|
||||
//Test
|
||||
app.get(prefix, (req, res) => res.send('The api is running'))
|
||||
|
||||
//Init user endpoint
|
||||
//Serve up uploaded files
|
||||
app.use(prefix+'/static', express.static( __dirname+'/../staticFiles' ))
|
||||
|
||||
//Public routes
|
||||
var public = require('@routes/publicController')
|
||||
app.use(prefix+'/public', public)
|
||||
|
||||
//user endpoint
|
||||
var user = require('@routes/userController')
|
||||
app.use(prefix+'/user', user)
|
||||
|
||||
//Init notes endpoint
|
||||
//notes endpoint
|
||||
var notes = require('@routes/noteController')
|
||||
app.use(prefix+'/note', notes)
|
||||
|
||||
//Init tags endpoint
|
||||
//tags endpoint
|
||||
var tags = require('@routes/tagController')
|
||||
app.use(prefix+'/tag', tags)
|
||||
|
||||
//notes endpoint
|
||||
var attachment = require('@routes/attachmentController')
|
||||
app.use(prefix+'/attachment', attachment)
|
||||
|
||||
//quick notes endpoint
|
||||
var quickNote = require('@routes/quicknoteController')
|
||||
app.use(prefix+'/quick-note', quickNote)
|
||||
|
||||
//Output running status
|
||||
app.listen(port, () => console.log(`Listening on port ${port}!`))
|
@@ -2,48 +2,268 @@ let db = require('@config/database')
|
||||
|
||||
let Attachment = module.exports = {}
|
||||
|
||||
const cheerio = require('cheerio');
|
||||
const rp = require('request-promise');
|
||||
const cheerio = require('cheerio')
|
||||
const rp = require('request-promise')
|
||||
const request = require('request')
|
||||
const fs = require('fs')
|
||||
|
||||
const gm = require('gm')
|
||||
|
||||
const tesseract = require("node-tesseract-ocr")
|
||||
const filePath = '../staticFiles/'
|
||||
|
||||
// Attachment.migrateOld
|
||||
|
||||
Attachment.textSearch = (userId, searchTerm) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const front = 5
|
||||
const tail = 150
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
*,
|
||||
substring(
|
||||
text,
|
||||
IF(LOCATE(?, text) > ${tail}, LOCATE(?, text) - ${front}, 1),
|
||||
${tail} + LENGTH(?) + ${front}
|
||||
) as snippet
|
||||
FROM attachment
|
||||
WHERE user_id = ?
|
||||
AND MATCH(text)
|
||||
AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||
LIMIT 1000`
|
||||
|
||||
db.promise()
|
||||
.query(query, [searchTerm, searchTerm, searchTerm, userId, searchTerm])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all attachments found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.search = (userId, noteId, attachmentType) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let params = [userId]
|
||||
let query = 'SELECT * FROM attachment WHERE user_id = ? AND visible = 1 '
|
||||
|
||||
if(noteId && noteId > 0){
|
||||
query += 'AND note_id = ? '
|
||||
params.push(noteId)
|
||||
}
|
||||
|
||||
if(Number.isInteger(attachmentType)){
|
||||
query += 'AND attachment_type = ? '
|
||||
params.push(attachmentType)
|
||||
}
|
||||
|
||||
query += 'ORDER BY last_indexed DESC '
|
||||
|
||||
db.promise()
|
||||
.query(query, params)
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all attachments found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
//Returns all attachments
|
||||
Attachment.forNote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND attachment_type = 1;`, [userId, noteId])
|
||||
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all tags found by query
|
||||
resolve(rows[0]) //Return all attachments found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.delete = (attachmentId) => {
|
||||
Attachment.urlForNote = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`DELETE FROM attachment WHERE id = ?`, [attachmentId])
|
||||
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND attachment_type = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all tags found by query
|
||||
resolve(rows[0]) //Return all attachments found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
|
||||
//Update attachment in database
|
||||
Attachment.update = (userId, attachmentId, updatedText, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET text = ? WHERE id = ? AND user_id = ?`,
|
||||
[updatedText, attachmentId, userId])
|
||||
.then((rows, fields) => {
|
||||
resolve(true)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.delete = (userId, attachmentId, urlDelete = false) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query('SELECT * FROM attachment WHERE id = ? AND user_id = ? LIMIT 1', [attachmentId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Attachment doesn't exist, return done
|
||||
if(rows[0].length == 0){
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
//Pull data we want out of
|
||||
let row = rows[0][0]
|
||||
let url = row.url
|
||||
const noteId = row.note_id
|
||||
|
||||
//Try to delete file and thumbnail
|
||||
try {
|
||||
fs.unlinkSync(filePath+row.file_location)
|
||||
} catch(err) { console.error('File Does not exist') }
|
||||
try {
|
||||
fs.unlinkSync(filePath+'thumb_'+row.file_location)
|
||||
} catch(err) { console.error('Thumbnail Does not exist') }
|
||||
|
||||
//Do not delete link attachments, just hide them. They will be deleted if removed from note
|
||||
if(row.attachment_type == 1 && !urlDelete){
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET visible = 0 WHERE id = ?`, [attachmentId])
|
||||
.then((rows, fields) => { })
|
||||
.catch(console.log)
|
||||
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
db.promise()
|
||||
.query(`DELETE FROM attachment WHERE id = ?`, [attachmentId])
|
||||
.then((rows, fields) => { })
|
||||
.catch(console.log)
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.processUploadedFile = (userId, noteId, fileObject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const rawFilename = fileObject.filename
|
||||
const extension = '.'+fileObject.originalname.split('.').pop()
|
||||
const goodFileName = rawFilename+extension
|
||||
const fileName = fileObject.originalname //Actual name of the file, dog.jpg
|
||||
|
||||
//Rename random file name to one with an extension
|
||||
fs.rename(filePath+rawFilename, filePath+goodFileName, (err) => {
|
||||
|
||||
const created = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
INSERT INTO attachment
|
||||
(note_id, user_id, attachment_type, \`text\`, last_indexed, file_location)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)
|
||||
`, [noteId, userId, 2, 'Add a description to -> '+fileName, created, goodFileName])
|
||||
.then((rows, fields) => {
|
||||
|
||||
Attachment.generateThumbnail(goodFileName)
|
||||
|
||||
//If its an image, scrape text
|
||||
if(true){
|
||||
|
||||
// https://github.com/tesseract-ocr/tesseract/wiki/ImproveQuality
|
||||
//psm 3 - default, 11 - as much text as possible
|
||||
const config = { lang: "eng", oem: 1, psm: 3 }
|
||||
|
||||
tesseract.recognize(filePath+goodFileName, config)
|
||||
.then(text => {
|
||||
|
||||
text = text.slice(0, -1).trim()
|
||||
|
||||
if(text.length > 5){
|
||||
console.log('Inserting text')
|
||||
db.promise().query(
|
||||
`UPDATE attachment SET text = ? WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
[text, rows[0].insertId, userId]
|
||||
).then(results => {
|
||||
resolve({ fileName, goodFileName })
|
||||
})
|
||||
} else {
|
||||
return resolve({ fileName, goodFileName })
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error.message)
|
||||
})
|
||||
|
||||
} else {
|
||||
resolve({ fileName, goodFileName })
|
||||
}
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.generateThumbnail = (fileName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
gm(filePath+fileName)
|
||||
.resize(550) //Resize to width of 550 px
|
||||
.quality(75) //compression level 0 - 100 (best)
|
||||
.write(filePath + 'thumb_'+fileName, function (err) {
|
||||
resolve(fileName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//Scans text for websites, returns all attachments
|
||||
Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let solrAttachmentText = '' //Final searchable scrape text for note
|
||||
|
||||
if(noteText.length == 0){ resolve(solrAttachmentText) }
|
||||
|
||||
Attachment.forNote(userId, noteId).then(attachments => {
|
||||
Attachment.urlForNote(userId, noteId).then(attachments => {
|
||||
|
||||
//Find all URLs in text
|
||||
//@TODO - Use the process text library for this function
|
||||
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
|
||||
let allUrls = noteText.match(urlPattern)
|
||||
|
||||
//Remove all duplicates
|
||||
let foundUrls = [...new Set(allUrls)]
|
||||
if(allUrls == null){
|
||||
allUrls = []
|
||||
}
|
||||
|
||||
//Go through each attachment, check for existing URLs
|
||||
//Every URL needs HTTPS!!!
|
||||
let foundUrls = []
|
||||
allUrls.forEach( (item, index) => {
|
||||
//Every URL should have HTTPS
|
||||
if(item.indexOf('https://') == -1 && item.indexOf('http://') == -1){
|
||||
allUrls[index] = 'https://'+item
|
||||
}
|
||||
//URLs should all have HTTPS!!!
|
||||
if(item.indexOf('http://') >= 0){
|
||||
allUrls[index] = item.replace('http://','https://')
|
||||
}
|
||||
})
|
||||
|
||||
//Remove all duplicates
|
||||
foundUrls = [...new Set(allUrls)]
|
||||
|
||||
|
||||
|
||||
//Go through each saved URL, remove new URLs from saved URLs
|
||||
//If a URL is not found, delete it
|
||||
attachments.forEach(attachment => {
|
||||
//URL already scraped, push text and continue
|
||||
let urlIndex = foundUrls.indexOf( attachment.url )
|
||||
@@ -52,18 +272,24 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
|
||||
solrAttachmentText += attachment.text
|
||||
foundUrls.splice(urlIndex, 1) //Remove existing from set of found
|
||||
} else {
|
||||
Attachment.delete(attachment.id)
|
||||
//If existing attachment is not found in note, remove it
|
||||
Attachment.delete(userId, attachment.id, true)
|
||||
}
|
||||
})
|
||||
|
||||
//No newly scraped URLs, resolve with looked up attachment text
|
||||
if(foundUrls == null || foundUrls.length == 0){
|
||||
resolve(solrAttachmentText)
|
||||
return resolve(solrAttachmentText)
|
||||
}
|
||||
|
||||
//Process the remaining URLs into attachments
|
||||
Attachment.scrapeUrlsCreateAttachments(userId, noteId, foundUrls).then( freshlyScrapedText => {
|
||||
|
||||
//Once everything is done being scraped, emit new attachment events
|
||||
if(io){
|
||||
io.to(userId).emit('update_counts')
|
||||
}
|
||||
|
||||
solrAttachmentText += freshlyScrapedText
|
||||
resolve(solrAttachmentText)
|
||||
})
|
||||
@@ -75,11 +301,13 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
|
||||
Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(foundUrls == null || foundUrls.length == 0){
|
||||
return resolve('')
|
||||
}
|
||||
|
||||
console.log('About to scrape')
|
||||
console.log(foundUrls)
|
||||
|
||||
if(foundUrls == null || foundUrls.length == 0){resolve('')}
|
||||
|
||||
let processedCount = 0
|
||||
let scrapedText = ''
|
||||
|
||||
@@ -99,8 +327,56 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.downloadFileFromUrl = (url) => {
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(url == null){
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||
const extension = '.'+url.split('.').pop() //This is throwing an error
|
||||
let fileName = random+'_scrape'+extension
|
||||
const thumbPath = 'thumb_'+fileName
|
||||
|
||||
console.log('Scraping image url')
|
||||
console.log(url)
|
||||
|
||||
console.log('Getting ready to scrape ', url)
|
||||
|
||||
request(url)
|
||||
.on('error', error => {
|
||||
console.log(error)
|
||||
resolve(null)
|
||||
})
|
||||
.on('response', res => {
|
||||
console.log(res.statusCode)
|
||||
console.log(res.headers['content-type'])
|
||||
})
|
||||
.pipe(fs.createWriteStream(filePath+thumbPath))
|
||||
.on('close', () => {
|
||||
|
||||
//resize image if its real big
|
||||
gm(filePath+thumbPath)
|
||||
.resize(550) //Resize to width of 550 px
|
||||
.quality(75) //compression level 0 - 100 (best)
|
||||
.write(filePath+thumbPath, function (err) {
|
||||
if(err){ console.log(err) }
|
||||
})
|
||||
|
||||
|
||||
console.log('Saved Image')
|
||||
resolve(fileName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.processUrl = (userId, noteId, url) => {
|
||||
|
||||
const scrapeTime = 20*1000;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const excludeWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
|
||||
@@ -112,12 +388,10 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
|
||||
var removeWhitespace = /\s+/g
|
||||
|
||||
|
||||
// console.log('Scraping ', website)
|
||||
const options = {
|
||||
uri: url,
|
||||
simple: true,
|
||||
timeout: 1000 * 10, // 10 seconds
|
||||
timeout: scrapeTime,
|
||||
headers: {
|
||||
'User-Agent':'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' //Simulate google headers
|
||||
},
|
||||
@@ -127,8 +401,24 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
}
|
||||
|
||||
let requestTimeout = null
|
||||
let thumbnail = null
|
||||
let request = null
|
||||
let created = Math.round((+new Date)/1000)
|
||||
let insertedId = null
|
||||
|
||||
let request = rp(options)
|
||||
//Create a shell attachment for each URL, put in processing state
|
||||
db.promise()
|
||||
.query(`INSERT INTO attachment
|
||||
(note_id, user_id, attachment_type, text, url, last_indexed, file_location)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[noteId, userId, 1, 'Processing...', url, created, null])
|
||||
.then((rows, fields) => {
|
||||
//Set two bigger variables then return request for processing
|
||||
request = rp(options)
|
||||
insertedId = rows[0].insertId
|
||||
|
||||
return request
|
||||
})
|
||||
.then($ => {
|
||||
|
||||
clearTimeout(requestTimeout)
|
||||
@@ -138,8 +428,15 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
let pageTitle = $('title').text().replace(removeWhitespace, " ")
|
||||
desiredSearchText += pageTitle + "\n"
|
||||
|
||||
let header = $('h1').text().replace(removeWhitespace, " ")
|
||||
desiredSearchText += header + "\n"
|
||||
// let header = $('h1').text().replace(removeWhitespace, " ")
|
||||
// desiredSearchText += header + "\n"
|
||||
|
||||
//Scrape metadata for page image
|
||||
let metadata = $('meta[property="og:image"]')
|
||||
if(metadata && metadata[0] && metadata[0].attribs){
|
||||
thumbnail = metadata[0].attribs.content
|
||||
}
|
||||
|
||||
|
||||
let majorContent = ''
|
||||
majorContent += $('[class*=content]').text()
|
||||
@@ -179,27 +476,52 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
});
|
||||
|
||||
let finalWords = []
|
||||
for(let i=0; i<15; i++){
|
||||
for(let i=0; i<5; i++){
|
||||
if(sortable[i] && sortable[i][0]){
|
||||
finalWords.push(sortable[i][0])
|
||||
}
|
||||
}
|
||||
|
||||
desiredSearchText += finalWords.join(', ')
|
||||
console.log('TexT Scraped')
|
||||
console.log(desiredSearchText)
|
||||
if(finalWords.length > 0){
|
||||
desiredSearchText += 'Keywords: ' + finalWords.join(', ')
|
||||
}
|
||||
|
||||
const created = Math.round((+new Date)/1000)
|
||||
|
||||
//Create attachment in DB with scrape text and provided data
|
||||
db.promise()
|
||||
.query(`INSERT INTO attachment
|
||||
(note_id, user_id, attachment_type, text, url, last_indexed)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
|
||||
.then((rows, fields) => {
|
||||
resolve(desiredSearchText) //Return found text
|
||||
// console.log('TexT Scraped')
|
||||
// console.log(desiredSearchText)
|
||||
|
||||
created = Math.round((+new Date)/1000)
|
||||
|
||||
//Scrape URL for thumbnail - take filename and save in attachment
|
||||
Attachment.downloadFileFromUrl(thumbnail)
|
||||
.then(thumbnailFilename => {
|
||||
|
||||
//Update text and thumbnail filename
|
||||
created = Math.round((+new Date)/1000)
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET
|
||||
text = ?,
|
||||
last_indexed = ?,
|
||||
file_location = ?
|
||||
WHERE id = ?
|
||||
`, [desiredSearchText, created, thumbnailFilename, insertedId])
|
||||
.then((rows, fields) => {
|
||||
resolve(desiredSearchText) //Return found text
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
|
||||
//Create attachment in DB with scrape text and provided data
|
||||
// db.promise()
|
||||
// .query(`INSERT INTO attachment
|
||||
// (note_id, user_id, attachment_type, text, url, last_indexed, file_location)
|
||||
// VALUES (?, ?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created, thumbnailFilename])
|
||||
// .then((rows, fields) => {
|
||||
|
||||
// resolve(desiredSearchText) //Return found text
|
||||
// })
|
||||
// .catch(console.log)
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -212,19 +534,30 @@ Attachment.processUrl = (userId, noteId, url) => {
|
||||
console.log('Cancel the request, its taking to long.')
|
||||
request.cancel()
|
||||
|
||||
desiredSearchText = 'Unable to Scrape URL at this time'
|
||||
const created = Math.round((+new Date)/1000)
|
||||
desiredSearchText = 'No Description for -> '+url
|
||||
|
||||
//Create attachment in DB with scrape text and provided data
|
||||
created = Math.round((+new Date)/1000)
|
||||
db.promise()
|
||||
.query(`INSERT INTO attachment
|
||||
(note_id, user_id, attachment_type, text, url, last_indexed)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
|
||||
.query(`UPDATE attachment SET
|
||||
text = ?,
|
||||
last_indexed = ?,
|
||||
WHERE id = ?
|
||||
`, [desiredSearchText, created, insertedId])
|
||||
.then((rows, fields) => {
|
||||
resolve(desiredSearchText) //Return found text
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
}, (5000))
|
||||
//Create attachment in DB with scrape text and provided data
|
||||
// db.promise()
|
||||
// .query(`INSERT INTO attachment
|
||||
// (note_id, user_id, attachment_type, text, url, last_indexed)
|
||||
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
|
||||
// .then((rows, fields) => {
|
||||
// resolve(desiredSearchText) //Return found text
|
||||
// })
|
||||
// .catch(console.log)
|
||||
|
||||
}, scrapeTime )
|
||||
})
|
||||
}
|
@@ -2,21 +2,103 @@ let db = require('@config/database')
|
||||
|
||||
let Tags = require('@models/Tag')
|
||||
let Attachment = require('@models/Attachment')
|
||||
let ShareNote = require('@models/ShareNote')
|
||||
|
||||
let ProcessText = require('@helpers/ProcessText')
|
||||
|
||||
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
|
||||
|
||||
var rp = require('request-promise');
|
||||
var SolrNode = require('solr-node');
|
||||
const fs = require('fs')
|
||||
|
||||
let Note = module.exports = {}
|
||||
|
||||
// Create client
|
||||
var client = new SolrNode({
|
||||
host: '127.0.0.1',
|
||||
port: '8983',
|
||||
core: 'note',
|
||||
protocol: 'http'
|
||||
});
|
||||
const gm = require('gm')
|
||||
|
||||
Note.create = (userId, noteText) => {
|
||||
// --------------
|
||||
|
||||
Note.migrateNoteTextToNewTable = () => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query('SELECT id, text FROM note WHERE note_raw_text_id IS NULL')
|
||||
.then((rows, fields) => {
|
||||
rows[0].forEach( ({id, text}) => {
|
||||
|
||||
db.promise()
|
||||
.query('INSERT INTO note_raw_text (text) VALUES (?)', [text])
|
||||
.then((rows, fields) => {
|
||||
|
||||
db.promise()
|
||||
.query(`UPDATE note SET note_raw_text_id = ? WHERE (id = ?)`, [rows[0].insertId, id])
|
||||
.then((rows, fields) => {
|
||||
|
||||
return 'Nice'
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
resolve('Its probably running... :-D')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Note.fixAttachmentThumbnails = () => {
|
||||
const filePath = '../staticFiles/'
|
||||
db.promise()
|
||||
.query(`SELECT * FROM attachment WHERE file_location NOT LIKE "%.%"`)
|
||||
.then( (rows, fields) => {
|
||||
|
||||
rows[0].forEach(line => {
|
||||
|
||||
const rawFilename = line['file_location']
|
||||
const goodFileName = rawFilename+'.jpg'
|
||||
|
||||
//Rename file to have jpg extension, create thumbnail, update database
|
||||
fs.rename(filePath+rawFilename, filePath+goodFileName, (err) => {
|
||||
|
||||
db.promise()
|
||||
.query(`UPDATE attachment SET file_location = ? WHERE id = ?`,[goodFileName, line['id'] ])
|
||||
.then( (rows, fields) => {
|
||||
gm(filePath+goodFileName)
|
||||
.resize(550) //Resize to width of 550 px
|
||||
.quality(75) //compression level 0 - 100 (best)
|
||||
.write(filePath + 'thumb_'+goodFileName, function (err) {
|
||||
console.log('Done for -> ', goodFileName)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Note.stressTest = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`
|
||||
|
||||
SELECT text FROM note;
|
||||
|
||||
`)
|
||||
.then((rows, fields) => {
|
||||
console.log()
|
||||
|
||||
rows[0].forEach(item => {
|
||||
|
||||
Note.create(68, item['text'])
|
||||
})
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
// --------------
|
||||
|
||||
Note.create = (userId, noteText, quickNote = 0) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(userId == null || userId < 10){ reject('User Id required to create note') }
|
||||
@@ -24,48 +106,94 @@ Note.create = (userId, noteText) => {
|
||||
const created = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise()
|
||||
.query('INSERT INTO note (user_id, text, created) VALUES (?,?,?)', [userId, noteText, created])
|
||||
.query(`INSERT INTO note_raw_text (text) VALUE (?)`, [noteText])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
const rawTextId = rows[0].insertId
|
||||
|
||||
return db.promise()
|
||||
.query('INSERT INTO note (user_id, note_raw_text_id, updated, created, quick_note) VALUES (?,?,?,?,?)',
|
||||
[userId, rawTextId, created, created, quickNote])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
// Indexing is done on save
|
||||
resolve(rows[0].insertId) //Only return the new note ID when creating a new note
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Note.update = (userId, noteId, noteText, fancyInput, color, pinned, archived) => {
|
||||
Note.reindex = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Note.get(userId, noteId)
|
||||
.then(note => {
|
||||
|
||||
const noteText = note.text
|
||||
|
||||
//
|
||||
// Update Solr index
|
||||
//
|
||||
Tags.string(userId, noteId)
|
||||
.then(tagString => {
|
||||
|
||||
const fullText = ProcessText.removeHtml(noteText) +' '+ tagString
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
|
||||
INSERT INTO note_text_index (note_id, user_id, text)
|
||||
VALUES (?,?,?)
|
||||
ON DUPLICATE KEY UPDATE text = ?
|
||||
|
||||
`, [noteId, userId, fullText, fullText])
|
||||
.then((rows, fields) => {
|
||||
resolve(true)
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Note.update = (io, userId, noteId, noteText, color, pinned, archived) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Prevent note loss if it saves with empty text
|
||||
if(ProcessText.removeHtml(noteText) == ''){
|
||||
console.log('Not saving empty note')
|
||||
resolve(false)
|
||||
}
|
||||
|
||||
const now = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise()
|
||||
.query('UPDATE note SET text = ?, raw_input = ?, pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
|
||||
[noteText, fancyInput, pinned, archived, now, color, noteId, userId])
|
||||
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Process note text and attachment data
|
||||
Attachment.scanTextForWebsites(userId, noteId, noteText).then( attachmentText => {
|
||||
//
|
||||
// Update Solr index
|
||||
//
|
||||
Tags.string(userId, noteId).then(tagString => {
|
||||
// JSON Data
|
||||
var data = {
|
||||
'id': noteId,//string - ID of note
|
||||
'user_id': userId,//int
|
||||
'note_text': noteText,
|
||||
'note_tag': tagString,
|
||||
'attachment_text': attachmentText,
|
||||
};
|
||||
// Update document to Solr server
|
||||
client.update(data, function(err, result) {
|
||||
if (err) { console.log(err); return; }
|
||||
console.log('Note Solr Update, node/solrid ('+noteId+'):');
|
||||
console.log(result.responseHeader)
|
||||
});
|
||||
const textId = rows[0][0]['note_raw_text_id']
|
||||
|
||||
})
|
||||
})
|
||||
//Update Note text
|
||||
return db.promise()
|
||||
.query('UPDATE note_raw_text SET text = ? WHERE id = ?', [noteText, textId])
|
||||
})
|
||||
.then( (rows, fields) => {
|
||||
|
||||
//Update other note attributes
|
||||
return db.promise()
|
||||
.query('UPDATE note SET pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
|
||||
[pinned, archived, now, color, noteId, userId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Async solr note reindex
|
||||
Note.reindex(userId, noteId)
|
||||
|
||||
//Async attachment reindex
|
||||
Attachment.scanTextForWebsites(io, userId, noteId, noteText)
|
||||
|
||||
//Send back updated response
|
||||
resolve(rows[0])
|
||||
@@ -74,25 +202,164 @@ Note.update = (userId, noteId, noteText, fancyInput, color, pinned, archived) =>
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Delete a note and all its remaining parts
|
||||
//
|
||||
Note.delete = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId])
|
||||
.then((rows, fields) => {
|
||||
db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
|
||||
.then((rows, fields)=> {
|
||||
db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
||||
.then((rows, fields)=> {
|
||||
|
||||
//
|
||||
// Delete, note, text, search index and associated tags
|
||||
// Leave the attachments, they can be deleted on their own
|
||||
// Leave Tags, their text is shared
|
||||
|
||||
let rawTextId = null
|
||||
let noteTextCount = 0
|
||||
|
||||
// Lookup the note text ID, we need this to count usages
|
||||
db.promise()
|
||||
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Save the raw text ID
|
||||
rawTextId = rows[0][0]['note_raw_text_id']
|
||||
|
||||
return db.promise()
|
||||
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Save the number of times the note is used
|
||||
noteTextCount = rows[0][0]['count']
|
||||
|
||||
//Don't delete text if its shared
|
||||
if(noteTextCount == 1){
|
||||
//If text is only used on one note, delete it (its not shared)
|
||||
return db.promise()
|
||||
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
.then( results => {
|
||||
// Delete Note entry for this user.
|
||||
return db.promise()
|
||||
.query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId, userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
// Delete search index
|
||||
return db.promise()
|
||||
.query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
// delete tags
|
||||
return db.promise()
|
||||
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//IF there are nots with a matching raw text id, we want to under their share status
|
||||
db.promise().query('SELECT id FROM note WHERE note_raw_text_id = ?',[rawTextId])
|
||||
.then((rows, fields) => {
|
||||
if(rows[0].length == 1){
|
||||
db.promise().query('UPDATE note SET shared = 0 WHERE id = ?', [rows[0][0]['id']])
|
||||
}
|
||||
})
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//text is the current text for the note that will be compared to the text in the database
|
||||
Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Note.get(userId, noteId)
|
||||
.then(noteObject => {
|
||||
|
||||
|
||||
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
|
||||
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
|
||||
|
||||
if(noteObject.updated == lastUpdated){
|
||||
// console.log('No note diff')
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
if(noteObject.updated > lastUpdated){
|
||||
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
|
||||
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
|
||||
}
|
||||
|
||||
const dmp = new DiffMatchPatch.diff_match_patch()
|
||||
const diff = dmp.diff_main(oldText, newText)
|
||||
|
||||
dmp.diff_cleanupSemantic(diff)
|
||||
const patch_list = dmp.patch_make(oldText, newText, diff);
|
||||
const patch_text = dmp.patch_toText(patch_list);
|
||||
|
||||
//Patch text - shows a list of changes
|
||||
var patches = dmp.patch_fromText(patch_text);
|
||||
// console.log(patch_text)
|
||||
|
||||
//results[1] - contains diagnostic data for patch apply, its possible it can fail
|
||||
var results = dmp.patch_apply(patches, oldText);
|
||||
|
||||
//Compile return data for front end
|
||||
const returnData = {
|
||||
updatedText: results[0],
|
||||
diffs: results[1].length, //Only use length for now
|
||||
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
|
||||
|
||||
}
|
||||
|
||||
//Final change in notes
|
||||
// console.log(returnData)
|
||||
|
||||
resolve(returnData)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Note.get = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query('SELECT text, updated, raw_input, pinned, archived, color FROM note WHERE user_id = ? AND id = ? LIMIT 1', [userId,noteId])
|
||||
.query(`
|
||||
SELECT
|
||||
note_raw_text.text,
|
||||
note.updated,
|
||||
note.pinned,
|
||||
note.archived,
|
||||
note.color,
|
||||
count(distinct attachment.id) as attachment_count,
|
||||
note.note_raw_text_id as rawTextId
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
||||
LEFT JOIN user ON (note.share_user_id = user.id)
|
||||
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const created = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [created, noteId])
|
||||
|
||||
//Return note data
|
||||
resolve(rows[0][0])
|
||||
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
//Public note share action -> may not be used
|
||||
Note.getShared = (noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query('SELECT text, updated, color FROM note WHERE id = ? AND shared = 1 LIMIT 1', [noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Return note data
|
||||
@@ -103,94 +370,136 @@ Note.get = (userId, noteId) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Searches text index, returns nothing if there is no search query
|
||||
Note.solrQuery = (userId, searchQuery, searchTags) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(searchQuery != '' && searchQuery != null){
|
||||
let urlQuery = `/solr/note/select?hl.fl=note_text&hl=on&q=user_id:${userId} AND note_text:${searchQuery}&wt=json`
|
||||
urlQuery = `/solr/note/select?
|
||||
hl.fl=note_text,attachment_text,note_tag&
|
||||
hl=on&
|
||||
q=user_id:${userId} AND (note_text:${searchQuery} OR attachment_text:${searchQuery} OR note_tag:${searchQuery})&
|
||||
wt=json&
|
||||
fl=id&
|
||||
hl.fl=note_text,attachment_text,note_tag&
|
||||
hl.snippets=20&
|
||||
hl.maxAnalyzedChars=100000`
|
||||
|
||||
rp('http://127.0.0.1:8983'+urlQuery)
|
||||
.then(function (htmlString) {
|
||||
let solrResult = JSON.parse(htmlString)
|
||||
resolve(solrResult)
|
||||
})
|
||||
if(searchQuery.length == 0){
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve([])
|
||||
|
||||
//Number of characters before and after search word
|
||||
const front = 5
|
||||
const tail = 150
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
|
||||
SELECT
|
||||
note_id,
|
||||
substring(
|
||||
text,
|
||||
IF(LOCATE(?, text) > ${tail}, LOCATE(?, text) - ${front}, 1),
|
||||
${tail} + LENGTH(?) + ${front}
|
||||
) as snippet
|
||||
FROM note_text_index
|
||||
WHERE user_id = ?
|
||||
AND MATCH(text)
|
||||
AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||
LIMIT 1000
|
||||
;
|
||||
|
||||
`, [searchQuery, searchQuery, searchQuery, userId, searchQuery])
|
||||
.then((rows, fields) => {
|
||||
|
||||
let results = []
|
||||
let snippets = {}
|
||||
rows[0].forEach(item => {
|
||||
let noteId = parseInt(item['note_id'])
|
||||
//Setup array of ids to use for query
|
||||
results.push( noteId )
|
||||
//Get text snippet and highlight the key word
|
||||
snippets[noteId] = item['snippet'].replace(new RegExp(searchQuery,"ig"), '<em>'+searchQuery+'</em>');
|
||||
//.replace(searchQuery,'<em>'+searchQuery+'</em>')
|
||||
})
|
||||
|
||||
resolve({ 'ids':results, 'snippets':snippets })
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Define return data objects
|
||||
let returnData = {
|
||||
'notes':[],
|
||||
'tags':[]
|
||||
}
|
||||
|
||||
Note.solrQuery(userId, searchQuery, searchTags).then( solrResult => {
|
||||
Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
|
||||
|
||||
let highlights = solrResult.highlighting
|
||||
//Pull out search results from previous query
|
||||
let textSearchIds = []
|
||||
let highlights = {}
|
||||
let returnTagResults = false
|
||||
|
||||
//Parse Note ID's from solr search
|
||||
let solrNoteIds = []
|
||||
if(solrResult.response){
|
||||
solrResult.response.docs.forEach(item => {
|
||||
solrNoteIds.push(parseInt(item.id))
|
||||
})
|
||||
}
|
||||
|
||||
//No results, return empty data
|
||||
if(solrNoteIds.length == 0 && searchQuery.length > 0){
|
||||
resolve(returnData)
|
||||
if(textSearchResults != null){
|
||||
textSearchIds = textSearchResults['ids']
|
||||
highlights = textSearchResults['snippets']
|
||||
}
|
||||
|
||||
//Default note lookup gets all notes
|
||||
//No results, return empty data
|
||||
if(textSearchIds.length == 0 && searchQuery.length > 0){
|
||||
return resolve(returnData)
|
||||
}
|
||||
|
||||
// Base of the query, modified with fastFilters
|
||||
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
|
||||
let searchParams = [userId]
|
||||
let noteSearchQuery = `
|
||||
SELECT note.id,
|
||||
SUBSTRING(note.text, 1, 400) as text,
|
||||
updated, color,
|
||||
SUBSTRING(note_raw_text.text, 1, 1500) as text,
|
||||
updated,
|
||||
color,
|
||||
count(distinct note_tag.id) as tag_count,
|
||||
count(distinct attachment.id) as attachment_count,
|
||||
note.pinned,
|
||||
note.archived
|
||||
note.archived,
|
||||
GROUP_CONCAT(DISTINCT tag.text) as tags,
|
||||
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
|
||||
shareUser.username as shareUsername,
|
||||
note.shared
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
LEFT JOIN note_tag ON (note.id = note_tag.note_id)
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.attachment_type = 1)
|
||||
WHERE note.user_id = ?`
|
||||
let searchParams = [userId]
|
||||
LEFT JOIN tag ON (tag.id = note_tag.tag_id)
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1)
|
||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||
WHERE note.user_id = ?
|
||||
`
|
||||
|
||||
if(solrNoteIds.length > 0){
|
||||
searchParams.push(solrNoteIds)
|
||||
//Show shared notes
|
||||
if(fastFilters.onlyShowSharedNotes == 1){
|
||||
//share_user_id means your shared them, a note with a shared user id filled in means it was shared
|
||||
noteSearchQuery += ` AND share_user_id IS NOT NULL OR (note.shared = 2 AND note.user_id = ?)`
|
||||
searchParams.push(userId)
|
||||
//Show notes shared with you
|
||||
} else {
|
||||
noteSearchQuery += ' AND note.share_user_id IS NULL'
|
||||
}
|
||||
|
||||
//If text search returned results, limit search to those ids
|
||||
if(textSearchIds.length > 0){
|
||||
searchParams.push(textSearchIds)
|
||||
noteSearchQuery += ' AND note.id IN (?)'
|
||||
}
|
||||
|
||||
// if(searchQuery != ''){
|
||||
// //If a search query is defined, search notes for that word
|
||||
// searchParams.push('%'+searchQuery+'%')
|
||||
// noteSearchQuery += ' AND text LIKE ?'
|
||||
// }
|
||||
if(fastFilters.noteIdSet && fastFilters.noteIdSet.length > 0){
|
||||
searchParams.push(fastFilters.noteIdSet)
|
||||
noteSearchQuery += ' AND note.id IN (?)'
|
||||
}
|
||||
|
||||
//If tags are passed, use those tags in search
|
||||
if(searchTags.length > 0){
|
||||
//If tags are passed, use those tags in search
|
||||
searchParams.push(searchTags)
|
||||
noteSearchQuery += ' AND note_tag.tag_id IN (?)'
|
||||
}
|
||||
|
||||
//Toggle archived, show archived if tags are searched
|
||||
// - archived will show archived in search results
|
||||
// - onlyArchive will exclude notes that are not archived
|
||||
if(fastFilters.archived == 1 || searchTags.length > 0 || fastFilters.onlyArchived == 1){
|
||||
//Do nothing
|
||||
//Show archived notes, only if fast filter is set, default to not archived
|
||||
if(fastFilters.onlyArchived == 1){
|
||||
noteSearchQuery += ' AND note.archived = 1' //Show Archived
|
||||
} else {
|
||||
noteSearchQuery += ' AND note.archived = 0' //Exclude archived
|
||||
}
|
||||
@@ -200,32 +509,54 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
|
||||
//Only show notes with Tags
|
||||
if(fastFilters.withTags == 1){
|
||||
returnTagResults = true
|
||||
noteSearchQuery += ' HAVING tag_count > 0'
|
||||
}
|
||||
//Only show notes with links
|
||||
if(fastFilters.withLinks == 1){
|
||||
returnTagResults = true
|
||||
noteSearchQuery += ' HAVING attachment_count > 0'
|
||||
}
|
||||
//Only show archived notes
|
||||
if(fastFilters.onlyArchived == 1){
|
||||
returnTagResults = true
|
||||
noteSearchQuery += ' HAVING note.archived = 1'
|
||||
}
|
||||
|
||||
//
|
||||
// Always prioritize pinned notes in searches.
|
||||
|
||||
//Default Sort, order by last updated
|
||||
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, created DESC, opened DESC, id DESC'
|
||||
let defaultOrderBy = ' ORDER BY note.pinned DESC, note.updated DESC, note.created DESC, note.opened DESC, id DESC'
|
||||
|
||||
//Order by Last Created Date
|
||||
if(fastFilters.lastCreated == 1){
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, created DESC, updated DESC, opened DESC, id DESC'
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, note.created DESC, note.updated DESC, note.opened DESC, id DESC'
|
||||
}
|
||||
//Order by last Opened Date
|
||||
if(fastFilters.lastOpened == 1){
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, updated DESC, created DESC, id DESC'
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, note.updated DESC, note.created DESC, id DESC'
|
||||
}
|
||||
|
||||
//Append Order by to query
|
||||
noteSearchQuery += defaultOrderBy
|
||||
|
||||
|
||||
//Manage limit params if set
|
||||
if(fastFilters.limitSize > 0 || fastFilters.limitOffset > 0){
|
||||
|
||||
const limitSize = parseInt(fastFilters.limitSize, 10) || 10 //Use int or default to 10
|
||||
const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero
|
||||
|
||||
|
||||
// console.log(` LIMIT ${limitOffset}, ${limitSize}`)
|
||||
noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}`
|
||||
}
|
||||
|
||||
// console.log('------------- Final Query --------------')
|
||||
// console.log(noteSearchQuery)
|
||||
// console.log('------------- ----------- --------------')
|
||||
|
||||
db.promise()
|
||||
.query(noteSearchQuery, searchParams)
|
||||
.then((noteRows, noteFields) => {
|
||||
@@ -240,54 +571,42 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
//Grab note ID for finding tags
|
||||
noteIds.push(note.id)
|
||||
|
||||
//Attempt to pull string out of first tag in note
|
||||
let reg = note.text.match(/<([\w]+)[^>]*>(.*?)<\/\1>/g)
|
||||
if(note.text == null){ note.text = '' }
|
||||
|
||||
//Pull out first html tag contents, that is the title
|
||||
if(reg != null && reg[0]){
|
||||
note.title = reg[0] //First line from HTML
|
||||
} else {
|
||||
note.title = note.text //Entire note
|
||||
}
|
||||
//Deduce note title
|
||||
const textData = ProcessText.deduceNoteTitle(note.text)
|
||||
// console.log(textData)
|
||||
|
||||
//Clean up html title
|
||||
note.title = note.title
|
||||
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
|
||||
|
||||
//Generate Subtext
|
||||
note.subtext = ''
|
||||
if(note.text != '' && note.title != ''){
|
||||
note.subtext = note.text
|
||||
.replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
|
||||
.replace(/\s+/g, ' ') //Remove all whitespace
|
||||
.substring(note.title.length + 2)
|
||||
}
|
||||
// console.log(textData)
|
||||
|
||||
note.title = textData.title
|
||||
note.subtext = textData.sub
|
||||
note.titleLength = textData.titleLength
|
||||
note.subtextLength = textData.subtextLength
|
||||
|
||||
note.note_highlights = []
|
||||
note.attachment_highlights = []
|
||||
note.tag_highlights = []
|
||||
|
||||
//Push in solr highlights
|
||||
if(highlights && highlights[note.id] && highlights[note.id].note_text){
|
||||
note['note_highlights'] = highlights[note.id].note_text
|
||||
}
|
||||
if(highlights && highlights[note.id] && highlights[note.id].attachment_text){
|
||||
note['attachment_highlights'] = highlights[note.id].attachment_text
|
||||
}
|
||||
if(highlights && highlights[note.id] && highlights[note.id].note_tag){
|
||||
note['tag_highlights'] = highlights[note.id].note_tag
|
||||
//Push in search highlights
|
||||
if(highlights && highlights[note.id]){
|
||||
note['note_highlights'] = [highlights[note.id]]
|
||||
}
|
||||
|
||||
//Clear out note.text before sending it to front end
|
||||
//Clear out note.text before sending it to front end, its being used in title and subtext
|
||||
delete note.text
|
||||
})
|
||||
|
||||
//If no notes are returned, there are no tags, return empty
|
||||
if(noteIds.length == 0){
|
||||
resolve(returnData)
|
||||
return resolve(returnData)
|
||||
}
|
||||
|
||||
//Return all notes, tags are not being searched
|
||||
// if tags are being searched, continue
|
||||
// if notes are being filtered, return tags
|
||||
if(searchTags.length == 0 && returnTagResults == false){
|
||||
return resolve(returnData)
|
||||
}
|
||||
|
||||
//Only show tags of selected notes
|
||||
|
101
server/models/QuickNote.js
Normal file
@@ -0,0 +1,101 @@
|
||||
let db = require('@config/database')
|
||||
|
||||
let Note = require('@models/Note')
|
||||
|
||||
let QuickNote = module.exports = {}
|
||||
|
||||
|
||||
QuickNote.get = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT note.id, text FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
WHERE quick_note = 1 AND user_id = ? LIMIT 1
|
||||
`, [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Quick Note is set, return note text
|
||||
if(rows[0].length == 1){
|
||||
resolve({
|
||||
id: rows[0][0].id,
|
||||
text: rows[0][0].text
|
||||
})
|
||||
}
|
||||
|
||||
resolve({
|
||||
id: null,
|
||||
text: 'Enter something to create a quick note.'
|
||||
})
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
QuickNote.update = (userId, pushText) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Process pushText, split at \n (new lines), put <p> tags around each new line
|
||||
let broken = '<blockquote>' +
|
||||
pushText.split(/\r?\n/).map( (line, index) => {
|
||||
|
||||
let clean = line
|
||||
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
|
||||
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
|
||||
|
||||
if(clean == ''){ clean = ' ' }
|
||||
let newLine = ''
|
||||
if(index > 0){ newLine = '<br>' }
|
||||
|
||||
//Return line wrapped in p tags
|
||||
return `${newLine}<span>${clean}</span>`
|
||||
|
||||
}).join('') + '</blockquote>'
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT note.id, text, color, pinned, archived
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
WHERE quick_note = 1 AND user_id = ? LIMIT 1
|
||||
`, [userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Quick Note is set, push it the front of existing note
|
||||
if(rows[0].length == 1){
|
||||
|
||||
let d = rows[0][0] //Get row data
|
||||
|
||||
//Push old text behind fresh new text
|
||||
let newText = broken +''+ d.text
|
||||
|
||||
//Save that, then return the new text
|
||||
Note.update(null, userId, d.id, newText, d.color, d.pinned, d.archived)
|
||||
.then( saveResults => {
|
||||
resolve({
|
||||
id:d.id,
|
||||
text:newText
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
//Create a new note with the quick text submitted.
|
||||
Note.create(userId, broken, 1)
|
||||
.then( insertId => {
|
||||
resolve({
|
||||
id:insertId,
|
||||
text:broken
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
|
||||
//Lookup quick note,
|
||||
|
||||
//Note.create(userId, 'Quick Note', 1)
|
||||
|
||||
}
|
147
server/models/ShareNote.js
Normal file
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// All actions in noteController.js
|
||||
//
|
||||
|
||||
|
||||
const db = require('@config/database')
|
||||
|
||||
const Note = require('@models/Note')
|
||||
|
||||
let ShareNote = module.exports = {}
|
||||
|
||||
// Share a note with a user, given the correct username
|
||||
ShareNote.addUser = (userId, noteId, rawTextId, username) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let shareUserId = null
|
||||
let newNoteShare = null
|
||||
const cleanUser = username.toLowerCase().trim()
|
||||
|
||||
//Check that user actually exists
|
||||
db.promise().query(`SELECT id FROM user WHERE LOWER(username) = ?`, [cleanUser])
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length == 0){
|
||||
throw new Error('User Does Not Exist')
|
||||
}
|
||||
|
||||
shareUserId = rows[0][0]['id']
|
||||
|
||||
//Check if note has already been added for user
|
||||
return db.promise()
|
||||
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, rawTextId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length != 0){
|
||||
throw new Error('User Already Has Note')
|
||||
}
|
||||
|
||||
//Lookup note to share with user, clone this data to create users new note
|
||||
return db.promise()
|
||||
.query(`SELECT * FROM note WHERE id = ? LIMIT 1`, [noteId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
newNoteShare = rows[0][0]
|
||||
|
||||
//Modify note with the share attributes we want
|
||||
delete newNoteShare['id']
|
||||
delete newNoteShare['opened']
|
||||
newNoteShare['share_user_id'] = userId //User who shared the note
|
||||
newNoteShare['user_id'] = shareUserId //User who gets note
|
||||
|
||||
//Setup db colums, db values and number of '?' to put into prepared statement
|
||||
let dbColumns = []
|
||||
let dbValues = []
|
||||
let escapeChars = []
|
||||
|
||||
//Pull out all the data we need from object to create prepared statemnt
|
||||
Object.keys(newNoteShare).forEach( key => {
|
||||
escapeChars.push('?')
|
||||
dbColumns.push(key)
|
||||
dbValues.push(newNoteShare[key])
|
||||
})
|
||||
|
||||
//Stick all the note value back into query, insert updated note
|
||||
return db.promise()
|
||||
.query(`INSERT INTO note (${dbColumns.join()}) VALUES (${escapeChars.join()})`, dbValues)
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Update note share status to 2
|
||||
return db.promise()
|
||||
.query('UPDATE note SET shared = 2 WHERE id = ?', [noteId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
//Success!
|
||||
return resolve({'success':true, shareUserId})
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get users who see a shared note
|
||||
ShareNote.getUsers = (userId, rawTextId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT username, note.id as noteId
|
||||
FROM note
|
||||
JOIN user ON (user.id = note.user_id)
|
||||
WHERE note_raw_text_id = ?
|
||||
AND share_user_id = ?
|
||||
AND user_id != ?
|
||||
`, [rawTextId, userId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Return a list of user names
|
||||
return resolve (rows[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Remove a user from a shared note
|
||||
ShareNote.removeUser = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const Note = require('@models/Note')
|
||||
|
||||
let rawTextId = null
|
||||
let removeUserId = null
|
||||
|
||||
//note.id = noteId, share_user_id = userId
|
||||
db.promise()
|
||||
.query('SELECT note_raw_text_id, user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
rawTextId = rows[0][0]['note_raw_text_id']
|
||||
removeUserId = rows[0][0]['user_id']
|
||||
|
||||
//Delete note entry for other user - remove users access
|
||||
if(removeUserId && Number.isInteger(removeUserId)){
|
||||
//Delete this users access to the note
|
||||
return Note.delete(removeUserId, noteId)
|
||||
|
||||
} else {
|
||||
|
||||
return new Promise((resolve, reject) => { resolve(true) })
|
||||
}
|
||||
|
||||
})
|
||||
.then(stuff => {
|
||||
resolve(true)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
@@ -2,6 +2,45 @@ let db = require('@config/database')
|
||||
|
||||
let Tag = module.exports = {}
|
||||
|
||||
Tag.userTags = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
tag.id,
|
||||
text,
|
||||
COUNT(note_tag.note_id) as usages
|
||||
FROM tag
|
||||
JOIN note_tag ON tag.id = note_tag.tag_id
|
||||
JOIN note On note.id = note_tag.note_id
|
||||
WHERE note_tag.user_id = ?
|
||||
`
|
||||
|
||||
//Show shared notes
|
||||
if(fastFilters && fastFilters.onlyShowSharedNotes == 1){
|
||||
query += ' AND note.share_user_id IS NOT NULL' //Show Archived
|
||||
} else {
|
||||
query += ' AND note.share_user_id IS NULL'
|
||||
}
|
||||
|
||||
//Show archived notes, only if fast filter is set, default to not archived
|
||||
if(fastFilters && fastFilters.onlyArchived == 1){
|
||||
query += ' AND note.archived = 1' //Show Archived
|
||||
} else {
|
||||
query += ' AND note.archived = 0' //Exclude archived
|
||||
}
|
||||
|
||||
query += ` GROUP BY tag.id
|
||||
ORDER BY usages DESC, text ASC`
|
||||
|
||||
|
||||
db.promise()
|
||||
.query(query, [userId])
|
||||
.then( (rows, fields) => {
|
||||
resolve(rows[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Tag.removeTagFromNote = (userId, tagId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -83,39 +122,50 @@ Tag.add = (tagText) => {
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Get all tags AND tags associated to note
|
||||
//
|
||||
Tag.get = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Update last opened date of note
|
||||
const now = Math.round((+new Date)/1000)
|
||||
db.promise()
|
||||
.query('UPDATE note SET opened = ? WHERE id = ? AND user_id = ? LIMIT 1', [now, noteId, userId])
|
||||
.then((rows, fields) => {})
|
||||
Tag.userTags(userId).then(userTags => {
|
||||
db.promise()
|
||||
.query(`SELECT tag_id as tagId, id as entryId
|
||||
FROM note_tag
|
||||
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
db.promise()
|
||||
.query(`SELECT note_tag.id, tag.text FROM note_tag
|
||||
JOIN tag ON (tag.id = note_tag.tag_id)
|
||||
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all tags found by query
|
||||
//pull IDs out of returned results
|
||||
// let ids = rows[0].map( item => {})
|
||||
|
||||
resolve({'noteTagIds':rows[0], 'allTags':userTags }) //Return all tags found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
|
||||
//
|
||||
Tag.string = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Tag.get(userId, noteId).then(tagArray => {
|
||||
db.promise()
|
||||
.query(`SELECT GROUP_CONCAT(DISTINCT tag.text SEPARATOR ', ') as text
|
||||
FROM note_tag
|
||||
JOIN tag ON note_tag.tag_id = tag.id
|
||||
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
let tagString = ''
|
||||
tagArray.forEach( (tag, i) => {
|
||||
if(i > 0){ tagString += ',' }
|
||||
tagString += tag.text
|
||||
})
|
||||
//Output comma delimited list of tag strings
|
||||
resolve(tagString)
|
||||
let finalText = rows[0][0]['text']
|
||||
if(finalText == null){
|
||||
finalText = ''
|
||||
}
|
||||
|
||||
return resolve(finalText) //Return all tags found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -113,3 +113,56 @@ User.create = (username, password) => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types
|
||||
User.getCounts = (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let countTotals = {}
|
||||
|
||||
db.promise().query(
|
||||
`SELECT
|
||||
SUM(pinned = 1 && archived = 0 && share_user_id IS NULL) AS pinnedNotes,
|
||||
SUM(archived = 1 && share_user_id IS NULL) AS archivedNotes,
|
||||
SUM(share_user_id IS NULL) AS totalNotes,
|
||||
SUM(share_user_id != ?) AS sharedToNotes,
|
||||
SUM( (share_user_id != ? && opened IS null) || (share_user_id != ? && updated > opened) ) AS unreadNotes
|
||||
FROM note
|
||||
WHERE user_id = ?`, [userId, userId, userId, userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
|
||||
return db.promise().query(
|
||||
`SELECT count(id) AS sharedFromNotes
|
||||
FROM note WHERE share_user_id = ?`, [userId]
|
||||
)
|
||||
})
|
||||
.then( (rows, fields) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
|
||||
return db.promise().query(
|
||||
`SELECT
|
||||
SUM(attachment_type = 1) as linkFiles,
|
||||
SUM(attachment_type != 1) as otherFiles,
|
||||
COUNT(id) as totalFiles
|
||||
FROM attachment WHERE visible = 1
|
||||
AND user_id = ?
|
||||
`, [userId]
|
||||
)
|
||||
}).then( (rows, fields) => {
|
||||
|
||||
Object.assign(countTotals, rows[0][0]) //combine results
|
||||
|
||||
//Convert everything to an int or 0
|
||||
Object.keys(countTotals).forEach( key => {
|
||||
const count = parseInt(countTotals[key])
|
||||
countTotals[key] = count ? count : 0
|
||||
})
|
||||
|
||||
resolve(countTotals)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
63
server/routes/attachmentController.js
Normal file
@@ -0,0 +1,63 @@
|
||||
let express = require('express')
|
||||
|
||||
var multer = require('multer')
|
||||
var upload = multer({ dest: '../staticFiles/' }) //@TODO make this a global value
|
||||
let router = express.Router()
|
||||
|
||||
let Attachment = require('@models/Attachment');
|
||||
let Note = require('@models/Note')
|
||||
let userId = null
|
||||
|
||||
// middleware that is specific to this router
|
||||
router.use(function setUserId (req, res, next) {
|
||||
if(userId = req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.post('/search', function (req, res) {
|
||||
Attachment.search(userId, req.body.noteId, req.body.attachmentType)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/textsearch', function (req, res) {
|
||||
Attachment.textSearch(userId, req.body.searchTerm)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/get', function (req, res) {
|
||||
Attachment.forNote(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/update', function (req, res) {
|
||||
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
|
||||
.then( result => {
|
||||
Note.reindex(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/delete', function (req, res) {
|
||||
Attachment.delete(userId, req.body.attachmentId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
router.post('/upload', upload.single('file'), function (req, res, next) {
|
||||
|
||||
//Create attachment with file information and node id
|
||||
const noteId = parseInt(req.body.noteId)
|
||||
|
||||
Attachment.processUploadedFile(userId, noteId, req.file)
|
||||
.then( uploadResults => {
|
||||
//Reindex note, attachment may have had text
|
||||
Note.reindex(userId, noteId)
|
||||
.then( data => {res.send(uploadResults)})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
@@ -2,20 +2,34 @@ var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let Notes = require('@models/Note');
|
||||
let ShareNote = require('@models/ShareNote');
|
||||
|
||||
let userId = null
|
||||
let socket = null
|
||||
|
||||
// middleware that is specific to this router
|
||||
router.use(function setUserId (req, res, next) {
|
||||
if(userId = req.headers.userId){
|
||||
if(req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
}
|
||||
if(req.headers.socket){
|
||||
// socket = req.
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//
|
||||
// Note actions
|
||||
//
|
||||
router.post('/get', function (req, res) {
|
||||
// req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
|
||||
Notes.get(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
.then( data => {
|
||||
//Join room when user opens note
|
||||
// req.io.join('note_room')
|
||||
res.send(data)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/delete', function (req, res) {
|
||||
@@ -29,16 +43,62 @@ router.post('/create', function (req, res) {
|
||||
})
|
||||
|
||||
router.post('/update', function (req, res) {
|
||||
Notes.update(userId, req.body.noteId, req.body.text, req.body.fancyInput, req.body.color, req.body.pinned, req.body.archived)
|
||||
Notes.update(req.io, userId, req.body.noteId, req.body.text, req.body.color, req.body.pinned, req.body.archived)
|
||||
.then( id => res.send({id}) )
|
||||
})
|
||||
|
||||
router.post('/search', function (req, res) {
|
||||
Notes.search(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
|
||||
.then( notesAndTags => res.send(notesAndTags))
|
||||
.then( notesAndTags => {
|
||||
res.send(notesAndTags)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/difftext', function (req, res) {
|
||||
Notes.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
|
||||
.then( fullDiffText => {
|
||||
//Response should be full diff text
|
||||
res.send(fullDiffText)
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Share Note Actions
|
||||
//
|
||||
router.post('/getshareusers', function (req, res) {
|
||||
ShareNote.getUsers(userId, req.body.rawTextId)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
router.post('/shareadduser', function (req, res) {
|
||||
ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username)
|
||||
.then( ({success, shareUserId}) => {
|
||||
|
||||
//Emit update count event to user shared with - so they see the note in real time
|
||||
req.io.to(shareUserId).emit('update_counts')
|
||||
|
||||
res.send(success)
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/shareremoveuser', function (req, res) {
|
||||
ShareNote.removeUser(userId, req.body.noteId)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
|
||||
//
|
||||
// Testing Action
|
||||
//
|
||||
//Reindex all notes. Not a very good function, not public
|
||||
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||
|
||||
Notes.migrateNoteTextToNewTable().then(status => {
|
||||
return res.send(status)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router
|
15
server/routes/publicController.js
Normal file
@@ -0,0 +1,15 @@
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let Notes = require('@models/Note')
|
||||
|
||||
router.post('/note', function (req, res) {
|
||||
|
||||
Notes.getShared(req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router
|
35
server/routes/quicknoteController.js
Normal file
@@ -0,0 +1,35 @@
|
||||
var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let QuickNote = require('@models/QuickNote');
|
||||
let userId = null
|
||||
|
||||
// middleware that is specific to this router
|
||||
router.use(function setUserId (req, res, next) {
|
||||
if(userId = req.headers.userId){
|
||||
userId = req.headers.userId
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
//Get quick note text
|
||||
router.post('/get', function (req, res) {
|
||||
QuickNote.get(userId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Push text to quick note
|
||||
router.post('/update', function (req, res) {
|
||||
QuickNote.update(userId, req.body.pushText)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Change quick note to a new note
|
||||
router.post('/change', function (req, res) {
|
||||
QuickNote.change(userId, req.body.noteId)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
@@ -42,4 +42,10 @@ router.post('/get', function (req, res) {
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
//Get all the tags for this user in order of usage
|
||||
router.post('/usertags', function (req, res) {
|
||||
Tags.userTags(userId, req.body.searchQuery, req.body.searchTags, req.body.fastFilters)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
module.exports = router
|
@@ -46,4 +46,11 @@ router.post('/login', function (req, res) {
|
||||
})
|
||||
})
|
||||
|
||||
// fetch counts of users notes
|
||||
router.post('/totals', function (req, res) {
|
||||
User.getCounts(req.headers.userId)
|
||||
.then( countsObject => res.send( countsObject ))
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
@@ -2,9 +2,9 @@
|
||||
|
||||
echo 'Make sure this is being run from root folder of project'
|
||||
|
||||
echo 'Starting API server (/api), watching for file changes...'
|
||||
pm2 start server/index.js --watch
|
||||
|
||||
echo 'Starting Client webpack dev server (/app), in a screen, watching for file changes...'
|
||||
screen -dm bash -c "cd client/; npm run watch"
|
||||
|
||||
echo 'Starting API server (/api), watching for file changes...'
|
||||
cd server
|
||||
pm2 start ecosystem.config.js
|
||||
|
4
staticFiles/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
*/
|
||||
!.gitignore
|
||||
!assets
|
BIN
staticFiles/assets/favicon.ico
Normal file
After Width: | Height: | Size: 144 KiB |
1
staticFiles/assets/marketing/add.svg
Normal file
After Width: | Height: | Size: 13 KiB |
1
staticFiles/assets/marketing/cloud.svg
Normal file
After Width: | Height: | Size: 7.7 KiB |
1
staticFiles/assets/marketing/gardening.svg
Normal file
After Width: | Height: | Size: 13 KiB |
1
staticFiles/assets/marketing/grandma.svg
Normal file
After Width: | Height: | Size: 12 KiB |
1
staticFiles/assets/marketing/growth.svg
Normal file
After Width: | Height: | Size: 13 KiB |
1
staticFiles/assets/marketing/hamburger.svg
Normal file
After Width: | Height: | Size: 26 KiB |
1
staticFiles/assets/marketing/icecream.svg
Normal file
After Width: | Height: | Size: 9.8 KiB |
1
staticFiles/assets/marketing/idea.svg
Normal file
After Width: | Height: | Size: 9.9 KiB |
1
staticFiles/assets/marketing/investing.svg
Normal file
After Width: | Height: | Size: 11 KiB |
1
staticFiles/assets/marketing/notebook.svg
Normal file
After Width: | Height: | Size: 8.5 KiB |
1
staticFiles/assets/marketing/onboarding.svg
Normal file
After Width: | Height: | Size: 20 KiB |
1
staticFiles/assets/marketing/plan.svg
Normal file
After Width: | Height: | Size: 18 KiB |
1
staticFiles/assets/marketing/robot.svg
Normal file
After Width: | Height: | Size: 49 KiB |
1
staticFiles/assets/marketing/secure.svg
Normal file
After Width: | Height: | Size: 6.7 KiB |
1
staticFiles/assets/marketing/solution.svg
Normal file
After Width: | Height: | Size: 6.6 KiB |
1
staticFiles/assets/marketing/void.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="bac3cfc7-b61b-48ce-8441-8100e40ddaa6" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="797.5" height="834.5" viewBox="0 0 797.5 834.5"><title>void</title><ellipse cx="308.5" cy="780" rx="308.5" ry="54.5" fill="#3f3d56"/><circle cx="496" cy="301.5" r="301.5" fill="#3f3d56"/><circle cx="496" cy="301.5" r="248.89787" opacity="0.05"/><circle cx="496" cy="301.5" r="203.99362" opacity="0.05"/><circle cx="496" cy="301.5" r="146.25957" opacity="0.05"/><path d="M398.42029,361.23224s-23.70394,66.72221-13.16886,90.42615,27.21564,46.52995,27.21564,46.52995S406.3216,365.62186,398.42029,361.23224Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><path d="M398.42029,361.23224s-23.70394,66.72221-13.16886,90.42615,27.21564,46.52995,27.21564,46.52995S406.3216,365.62186,398.42029,361.23224Z" transform="translate(-201.25 -32.75)" opacity="0.1"/><path d="M415.10084,515.74682s-1.75585,16.68055-2.63377,17.55847.87792,2.63377,0,5.26754-1.75585,6.14547,0,7.02339-9.65716,78.13521-9.65716,78.13521-28.09356,36.8728-16.68055,94.81576l3.51169,58.82089s27.21564,1.75585,27.21564-7.90132c0,0-1.75585-11.413-1.75585-16.68055s4.38962-5.26754,1.75585-7.90131-2.63377-4.38962-2.63377-4.38962,4.38961-3.51169,3.51169-4.38962,7.90131-63.2105,7.90131-63.2105,9.65716-9.65716,9.65716-14.92471v-5.26754s4.38962-11.413,4.38962-12.29093,23.70394-54.43127,23.70394-54.43127l9.65716,38.62864,10.53509,55.3092s5.26754,50.04165,15.80262,69.356c0,0,18.4364,63.21051,18.4364,61.45466s30.72733-6.14547,29.84941-14.04678-18.4364-118.5197-18.4364-118.5197L533.62054,513.991Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><path d="M391.3969,772.97846s-23.70394,46.53-7.90131,48.2858,21.94809,1.75585,28.97148-5.26754c3.83968-3.83968,11.61528-8.99134,17.87566-12.87285a23.117,23.117,0,0,0,10.96893-21.98175c-.463-4.29531-2.06792-7.83444-6.01858-8.16366-10.53508-.87792-22.826-10.53508-22.826-10.53508Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><path d="M522.20753,807.21748s-23.70394,46.53-7.90131,48.28581,21.94809,1.75584,28.97148-5.26754c3.83968-3.83969,11.61528-8.99134,17.87566-12.87285a23.117,23.117,0,0,0,10.96893-21.98175c-.463-4.29531-2.06792-7.83444-6.01857-8.16367-10.53509-.87792-22.826-10.53508-22.826-10.53508Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><circle cx="295.90488" cy="215.43252" r="36.90462" fill="#ffb8b8"/><path d="M473.43048,260.30832S447.07,308.81154,444.9612,308.81154,492.41,324.62781,492.41,324.62781s13.70743-46.39439,15.81626-50.61206Z" transform="translate(-201.25 -32.75)" fill="#ffb8b8"/><path d="M513.86726,313.3854s-52.67543-28.97148-57.943-28.09356-61.45466,50.04166-60.57673,70.2339,7.90131,53.55335,7.90131,53.55335,2.63377,93.05991,7.90131,93.93783-.87792,16.68055.87793,16.68055,122.90931,0,123.78724-2.63377S513.86726,313.3854,513.86726,313.3854Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><path d="M543.2777,521.89228s16.68055,50.91958,2.63377,49.16373-20.19224-43.89619-20.19224-43.89619Z" transform="translate(-201.25 -32.75)" fill="#ffb8b8"/><path d="M498.50359,310.31267s-32.48318,7.02339-27.21563,50.91957,14.9247,87.79237,14.9247,87.79237l32.48318,71.11182,3.51169,13.16886,23.70394-6.14547L528.353,425.32067s-6.14547-108.86253-14.04678-112.37423A33.99966,33.99966,0,0,0,498.50359,310.31267Z" transform="translate(-201.25 -32.75)" fill="#d0cde1"/><polygon points="277.5 414.958 317.885 486.947 283.86 411.09 277.5 414.958" opacity="0.1"/><path d="M533.896,237.31585l.122-2.82012,5.6101,1.39632a6.26971,6.26971,0,0,0-2.5138-4.61513l5.97581-.33413a64.47667,64.47667,0,0,0-43.1245-26.65136c-12.92583-1.87346-27.31837.83756-36.182,10.43045-4.29926,4.653-7.00067,10.57018-8.92232,16.60685-3.53926,11.11821-4.26038,24.3719,3.11964,33.40938,7.5006,9.18513,20.602,10.98439,32.40592,12.12114,4.15328.4,8.50581.77216,12.35457-.83928a29.721,29.721,0,0,0-1.6539-13.03688,8.68665,8.68665,0,0,1-.87879-4.15246c.5247-3.51164,5.20884-4.39635,8.72762-3.9219s7.74984,1.20031,10.062-1.49432c1.59261-1.85609,1.49867-4.559,1.70967-6.99575C521.28248,239.785,533.83587,238.70653,533.896,237.31585Z" transform="translate(-201.25 -32.75)" fill="#2f2e41"/><circle cx="559" cy="744.5" r="43" fill="#21ba45"/><circle cx="54" cy="729.5" r="43" fill="#21ba45"/><circle cx="54" cy="672.5" r="31" fill="#21ba45"/><circle cx="54" cy="624.5" r="22" fill="#21ba45"/></svg>
|
After Width: | Height: | Size: 4.2 KiB |
1
staticFiles/assets/marketing/watching.svg
Normal file
After Width: | Height: | Size: 24 KiB |