Compare commits
No commits in common. "main" and "v1.0" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,8 +11,6 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
release
|
|
||||||
*.env
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
22
README.md
22
README.md
@ -4,34 +4,20 @@ Just another note editor written in VUE.js for learning purposes.
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
To get a better understanding of the VUE.js framework, I wanted to create a tool for my personal usage.
|
This is version 1.0.0 - with the basic setup to creating, deleting and editing new notes.
|
||||||
I am a big fan of the apple notes app, sadly there is no linux port, therefore I started this project not to create such port,
|
There is no persistent storage a reload deletes all notes.
|
||||||
but to have a portfolio application to learn more about typescript and VUE.js and to have an excuse why I have to use Vite for a project.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This Project is based on a YouTube Video by Tylor Potts (https://www.youtube.com/watch?v=YwUvSa9Ckqo) and therfore I am releasing this code under the GPL 3 license.
|
|
||||||
|
|
||||||
## Versions
|
|
||||||
|
|
||||||
- Version 1.0.2 - introducing markdown rendering (did you saw the easter egg??). Minor refactoring and rework of the readme
|
|
||||||
- Version 1.0.1 - basic CRUD for notes, persitancy with browser based localstorage. Minor refactoring and rework of the readme
|
|
||||||
- Version 1.0.0 - with the basic setup to creating, deleting and editing new notes. There is no persistent storage a reload deletes all notes.
|
|
||||||
|
|
||||||
## Coming next
|
## Coming next
|
||||||
|
|
||||||
* [x] script to compress the release
|
|
||||||
* [ ] script to upload the release
|
|
||||||
* [ ] (s)ftp based deployment
|
|
||||||
* [ ] runner based release pipeline
|
* [ ] runner based release pipeline
|
||||||
* [ ] release note generator based on the commits
|
* [ ] release note generator based on the commits
|
||||||
* [ ] better UI
|
* [ ] better UI
|
||||||
* [ ] better Icons
|
* [ ] better Icons
|
||||||
* [x] markdown rendering
|
* [ ] markdown rendering
|
||||||
* [ ] link rendering
|
* [ ] link rendering
|
||||||
* [ ] code rendering
|
* [ ] code rendering
|
||||||
* [ ] emoji rendering
|
* [ ] emoji rendering
|
||||||
* [x] persistent storage based on something like local storage of the browser
|
* [ ] persistent storage based on something like local storage of the browser
|
||||||
* [ ] persistent storage based on backend api (can be edited by the user)
|
* [ ] persistent storage based on backend api (can be edited by the user)
|
||||||
* [ ] PWA Setup so that the "app" can be installed on the users system
|
* [ ] PWA Setup so that the "app" can be installed on the users system
|
||||||
* [ ] Website to inform the user about and give the user an entry point for JANE
|
* [ ] Website to inform the user about and give the user an entry point for JANE
|
||||||
|
76
compress.js
76
compress.js
@ -1,76 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import archiver from 'archiver'
|
|
||||||
import * as tar from 'tar'
|
|
||||||
|
|
||||||
// read the package json
|
|
||||||
const packageJsonPath = path.resolve('./package.json')
|
|
||||||
// parse the content
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
||||||
// read the project name
|
|
||||||
const projectName = packageJson.name.toUpperCase() || 'project'
|
|
||||||
// read the project version
|
|
||||||
const version = packageJson.version || '0.0.1'
|
|
||||||
// define the source dir
|
|
||||||
const sourceDir = './dist'
|
|
||||||
// define the target dir
|
|
||||||
const outputDir = './release'
|
|
||||||
// create the base output filename
|
|
||||||
const baseFileName = `${projectName}-${version}`
|
|
||||||
// create the output filename for the zip file
|
|
||||||
const zipOutput = path.join(outputDir, `${baseFileName}.zip`)
|
|
||||||
// create the output filename for the tar zip file
|
|
||||||
const tarOutput = path.join(outputDir, `${baseFileName}.tar.gz`)
|
|
||||||
|
|
||||||
// function to read the content of the source dir and put them in the zip file
|
|
||||||
async function createZip() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// create an output file to stream the zip content into
|
|
||||||
const output = fs.createWriteStream(zipOutput)
|
|
||||||
// configure the zip file
|
|
||||||
const archive = archiver('zip', {
|
|
||||||
zlib: { level: 9 },
|
|
||||||
})
|
|
||||||
// define an async function to print a success message
|
|
||||||
output.on('close', () => {
|
|
||||||
console.log(`[√] ZIP created: ${zipOutput} (${archive.pointer()} bytes)`)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
// define an async function to print an error message
|
|
||||||
archive.on('error', (err) => reject(err))
|
|
||||||
// pipe the output stream
|
|
||||||
archive.pipe(output)
|
|
||||||
// read the content of the source dir
|
|
||||||
archive.directory(sourceDir, false)
|
|
||||||
// close the zip stream and finish the file
|
|
||||||
archive.finalize()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// function to read the content of the source dir and put them in the tar.gz file
|
|
||||||
async function createTarGz() {
|
|
||||||
return tar.c(
|
|
||||||
{
|
|
||||||
gzip: true,
|
|
||||||
file: tarOutput,
|
|
||||||
cwd: path.dirname(sourceDir),
|
|
||||||
},
|
|
||||||
[path.basename(sourceDir)]
|
|
||||||
).then(() => {
|
|
||||||
console.log(`[√] tar.gz created: ${tarOutput}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ausführung
|
|
||||||
async function run() {
|
|
||||||
try {
|
|
||||||
await fs.promises.mkdir(outputDir, { recursive: true })
|
|
||||||
console.log(`[!] Using project: "${projectName}", version: "${version}"`)
|
|
||||||
await createZip()
|
|
||||||
await createTarGz()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[X] Error during compression:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run()
|
|
1057
package-lock.json
generated
1057
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jane",
|
"name": "jane",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.3",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Just another note editor written in VUE.js for learning purposes.",
|
"description": "Just another note editor written in VUE.js for learning purposes.",
|
||||||
"homepage": "https://projects.nisch.codes/nischcodes/JANE#readme",
|
"homepage": "https://projects.nisch.codes/nischcodes/JANE#readme",
|
||||||
@ -18,18 +18,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"pack-release": "node compress.js",
|
|
||||||
"upload-release": "echo test"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"@tailwindcss/vite": "^4.0.14",
|
"@tailwindcss/vite": "^4.0.14",
|
||||||
"archiver": "^7.0.1",
|
|
||||||
"dompurify": "^3.2.4",
|
|
||||||
"marked": "^15.0.7",
|
|
||||||
"tailwindcss": "^4.0.14",
|
"tailwindcss": "^4.0.14",
|
||||||
"tar": "^7.4.3",
|
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
This is the testing Note for all the Markdown features.
|
|
||||||
|
|
||||||
# Heading 1
|
|
||||||
## Heading 2
|
|
||||||
### Heading 3
|
|
||||||
#### Heading 4
|
|
||||||
##### Heading 5
|
|
||||||
###### Heading 6
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This is some text which includes **bold** text and some more __bold__ text. But also some *italic* text and more _italic_ text. There is even text with a mixture of ***bold and italic*** text.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Must texts have some quotes included, so this note also have some quotes...
|
|
||||||
|
|
||||||
> This is a single quote
|
|
||||||
|
|
||||||
...and quotes in quotes.
|
|
||||||
> This is the outer quote
|
|
||||||
>> This is a inner quote
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
When it comes to lists, there are quite a few.
|
|
||||||
|
|
||||||
First there is the unsorted list:
|
|
||||||
|
|
||||||
* list item 1
|
|
||||||
* list item 2
|
|
||||||
* list item 3
|
|
||||||
|
|
||||||
Second there are the sorted lists:
|
|
||||||
|
|
||||||
1. list item 1
|
|
||||||
2. list item 2
|
|
||||||
3. list item 3
|
|
||||||
|
|
||||||
Lastly you can combine both types of lists:
|
|
||||||
|
|
||||||
1. list item 1
|
|
||||||
- list item 1.1
|
|
||||||
1. list item
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Another big topic is links. You can specify the link text, or not and you can use mail-addresses to generate a mailto link.
|
|
||||||
|
|
||||||
* [Nisch's projects and git server](https://projects.nisch.codes)
|
|
||||||
* <https://nisch.codes>
|
|
||||||
* <info@nisch.codes>
|
|
||||||
|
|
||||||
Next reference links:
|
|
||||||
|
|
||||||
* [JANE's Repository][1]
|
|
||||||
* [JANE's License][2]
|
|
||||||
|
|
||||||
[1]: https://projects.nisch.codes/nischcodes/JANE "JANE - git repository"
|
|
||||||
[2]: https://projects.nisch.codes/nischcodes/JANE/src/branch/main/LICENSE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Images:
|
|
||||||
|
|
||||||
[](https://www.flickr.com/photos/beaurogers/31833779864/in/photolist-Qv3rFw-34mt9F-a9Cmfy-5Ha3Zi-9msKdv-o3hgjr-hWpUte-4WMsJ1-KUQ8N-deshUb-vssBD-6CQci6-8AFCiD-zsJWT-nNfsgB-dPDwZJ-bn9JGn-5HtSXY-6CUhAL-a4UTXB-ugPum-KUPSo-fBLNm-6CUmpy-4WMsc9-8a7D3T-83KJev-6CQ2bK-nNusHJ-a78rQH-nw3NvT-7aq2qf-8wwBso-3nNceh-ugSKP-4mh4kh-bbeeqH-a7biME-q3PtTf-brFpgb-cg38zw-bXMZc-nJPELD-f58Lmo-bXMYG-bz8AAi-bxNtNT-bXMYi-bXMY6-bXMYv)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
At the end, there is Code:
|
|
||||||
|
|
||||||
`` console.log('hello world') ``
|
|
Binary file not shown.
Before Width: | Height: | Size: 97 KiB |
100
src/App.vue
100
src/App.vue
@ -1,71 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watchEffect, nextTick, onMounted, onUnmounted} from 'vue'
|
import {ref} from 'vue'
|
||||||
import Sidebar from './components/Sidebar.vue'
|
import Sidebar from './components/Sidebar.vue'
|
||||||
import MarkdownEditor from './components/MarkdownEditor.vue'
|
|
||||||
|
|
||||||
const STORAGE_KEY_CONFIG = 'jane-config'
|
const notes = ref([])
|
||||||
const STORAGE_KEY_NOTES = 'jane-notes'
|
|
||||||
|
|
||||||
const configs = ref(JSON.parse(localStorage.getItem(STORAGE_KEY_CONFIG) || '[]'))
|
|
||||||
const notes = ref(JSON.parse(localStorage.getItem(STORAGE_KEY_NOTES) || '[]'))
|
|
||||||
const active_note = ref(null)
|
const active_note = ref(null)
|
||||||
|
|
||||||
const input_title = ref('')
|
const input_title = ref('')
|
||||||
const input_content = ref('')
|
const input_content = ref('')
|
||||||
|
|
||||||
const loadMarkdown = async () => {
|
|
||||||
// load markdown file
|
|
||||||
const response = await fetch('/data/markdown-test.md')
|
|
||||||
// save the content
|
|
||||||
let markdown = await response.text()
|
|
||||||
// create a new note
|
|
||||||
create_note('Markdown Test Note', markdown)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeydown = (event:any) => {
|
|
||||||
if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'm') {
|
|
||||||
// prevent the default behaviour
|
|
||||||
event.preventDefault()
|
|
||||||
// some messages for the easter egg
|
|
||||||
console.log('CTRL + SHIFT + M pressed!')
|
|
||||||
// call the load function
|
|
||||||
loadMarkdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => { window.addEventListener('keydown', handleKeydown) })
|
|
||||||
onUnmounted(() => { window.removeEventListener('keydown', handleKeydown) })
|
|
||||||
|
|
||||||
// find note Index by Id
|
|
||||||
function findNoteIndexById(id:any):any {
|
|
||||||
return notes.value.findIndex((note:any) => note.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create random id
|
|
||||||
function createRandomId():string {
|
|
||||||
return Math.random().toString(36).substring(2,9)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create note
|
// create note
|
||||||
function create_note(title:string = 'Untitled', content:string = '') {
|
function create_note() {
|
||||||
// generate random id
|
// generate random id
|
||||||
const id = createRandomId()
|
const id = Math.random().toString(36).substring(2,9)
|
||||||
// push new note to notes array
|
// push new note to notes array
|
||||||
notes.value.push({
|
notes.value.push({
|
||||||
id,
|
id,
|
||||||
title: title,
|
title: 'Untitled',
|
||||||
content: content
|
content: ''
|
||||||
})
|
})
|
||||||
// set active note to id
|
// set active note to id
|
||||||
set_active_note(id)
|
set_active_note(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete note
|
// delete note
|
||||||
function delete_note({id, evt}:any) {
|
function delete_note({id, evt}) {
|
||||||
// stop the propagation of the event
|
// stop the propagation of the event
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
// find the current note id in the notes array
|
// find the current note id in the notes array
|
||||||
let noteId = findNoteIndexById(id)
|
let noteId = notes.value.findIndex(note => note.id === id)
|
||||||
// delete the note out of the array
|
// delete the note out of the array
|
||||||
notes.value.splice(noteId, 1)
|
notes.value.splice(noteId, 1)
|
||||||
// unset the active note and the inputs
|
// unset the active note and the inputs
|
||||||
@ -77,42 +39,30 @@ function delete_note({id, evt}:any) {
|
|||||||
// update note
|
// update note
|
||||||
function update_note() {
|
function update_note() {
|
||||||
// find the current note id in the notes array
|
// find the current note id in the notes array
|
||||||
let noteId = findNoteIndexById(active_note.value)
|
let noteId = notes.value.findIndex(note => note.id === active_note.value)
|
||||||
// set the note properties to the current input values
|
// set the note properties to the current input values
|
||||||
notes.value[noteId].title = input_title.value
|
notes.value[noteId].title = input_title.value
|
||||||
notes.value[noteId].content = input_content.value
|
notes.value[noteId].content = input_content.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// update note content
|
|
||||||
function update_note_content({id, content}:any) {
|
|
||||||
// find the current note id in the notes array
|
|
||||||
let noteId = findNoteIndexById(id)
|
|
||||||
// set the note properties to the current input values
|
|
||||||
notes.value[noteId].content = content
|
|
||||||
// update the ref object
|
|
||||||
input_content.value = content
|
|
||||||
}
|
|
||||||
|
|
||||||
// set active note
|
// set active note
|
||||||
function set_active_note(id:any) {
|
function set_active_note(id) {
|
||||||
// set the active note property
|
// set the active note property
|
||||||
active_note.value = id
|
active_note.value = id
|
||||||
// find the current note in the notes array
|
// find the current note in the notes array
|
||||||
let noteId = findNoteIndexById(id)
|
let note = notes.value.find(note => note.id === id)
|
||||||
// set the input properties to the current note
|
// set the input properties to the current note
|
||||||
input_title.value = notes.value[noteId].title
|
input_title.value = note.title
|
||||||
input_content.value = notes.value[noteId].content
|
input_content.value = note.content
|
||||||
// wait a tick and focus the input field
|
// set timeout to wait a tick to get it ready
|
||||||
nextTick(() => { (document.querySelector('input#note-title') as HTMLInputElement)?.focus() })
|
setTimeout(() => {
|
||||||
|
document.querySelector('input#note-title').focus()
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist state
|
// save notes
|
||||||
watchEffect(() => {
|
|
||||||
// save the current array of notes
|
// load notes
|
||||||
localStorage.setItem(STORAGE_KEY_NOTES, JSON.stringify(notes.value))
|
|
||||||
// save the current array of configurations
|
|
||||||
localStorage.setItem(STORAGE_KEY_CONFIG, JSON.stringify(configs.value))
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -138,15 +88,13 @@ watchEffect(() => {
|
|||||||
id="note-title"
|
id="note-title"
|
||||||
class="block w-full text-3xl pb-2 font-bold border-b-2 border-gray-500 focus:border-white outline-none transition-colors duration-200"
|
class="block w-full text-3xl pb-2 font-bold border-b-2 border-gray-500 focus:border-white outline-none transition-colors duration-200"
|
||||||
/>
|
/>
|
||||||
|
<textarea
|
||||||
<MarkdownEditor
|
|
||||||
:active_note="active_note"
|
|
||||||
v-model="input_content"
|
v-model="input_content"
|
||||||
@update-note-content="update_note_content"
|
@input="update_note"
|
||||||
name="note-content"
|
name="note-content"
|
||||||
id="note-content"
|
id="note-content"
|
||||||
class="block w-full h-full mt-4 text-lg flex-1"
|
class="block w-full h-full mt-4 text-lg outline-none flex-1"
|
||||||
/>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, nextTick } from 'vue'
|
|
||||||
import { marked } from 'marked'
|
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
|
|
||||||
//const model = defineModel({ default: '' })
|
|
||||||
const props = defineProps(['active_note'])
|
|
||||||
const emit = defineEmits(['update-note-content'])
|
|
||||||
const markdownInput = defineModel({ default: '' })
|
|
||||||
|
|
||||||
const isEditing = ref(false)
|
|
||||||
|
|
||||||
// render the markdown and sanitize the result
|
|
||||||
const renderedMarkdown = computed(() => {
|
|
||||||
// parse raw text into markdown
|
|
||||||
const rawHTML = marked.parse(markdownInput.value)
|
|
||||||
// sanitize the raw html from marked
|
|
||||||
return DOMPurify.sanitize(rawHTML as string)
|
|
||||||
})
|
|
||||||
|
|
||||||
// function to start the edit state
|
|
||||||
function startEditing() {
|
|
||||||
// set the edit state to true
|
|
||||||
isEditing.value = true
|
|
||||||
// wait a tick and focus the input field
|
|
||||||
nextTick(() => { (document.querySelector('textarea#editor') as HTMLTextAreaElement)?.focus() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// function to stop the edit state
|
|
||||||
function stopEditing() {
|
|
||||||
// set the edit state to false
|
|
||||||
isEditing.value = false
|
|
||||||
// update the prop outside this component
|
|
||||||
emit('update-note-content', {id: props.active_note, content: markdownInput.value})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-if="!isEditing"
|
|
||||||
class="prose max-w-none w-full h-full text-lg cursor-pointer text-white prose-headings:text-white prose-strong:text-white prose-em:text-white prose-blockquote:text-white prose-code:text-white prose-a:text-white marker:text-white"
|
|
||||||
v-html="renderedMarkdown"
|
|
||||||
@click="startEditing"
|
|
||||||
></div>
|
|
||||||
<textarea
|
|
||||||
v-else
|
|
||||||
id="editor"
|
|
||||||
v-model="markdownInput"
|
|
||||||
@blur="stopEditing"
|
|
||||||
@keydown.esc.prevent="stopEditing"
|
|
||||||
class="w-full h-full text-lg outline-none"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps(['active_note', 'notes'])
|
const props = defineProps(['active_note', 'notes'])
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin "@tailwindcss/typography";
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.button {
|
.button {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user