implement search

This commit is contained in:
2023-04-29 14:31:31 +02:00
parent b7055eb8d6
commit 1d6826e255
14 changed files with 508 additions and 279 deletions

552
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,14 +11,20 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@types/marked": "^4.0.8",
"@vueuse/components": "^10.1.0",
"@vueuse/core": "^10.1.0", "@vueuse/core": "^10.1.0",
"bootstrap-icons": "^1.10.5", "bootstrap-icons": "^1.10.5",
"daisyui": "^2.51.6",
"dompurify": "^3.0.2",
"firebase": "^9.20.0", "firebase": "^9.20.0",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"hamburgers": "^1.2.1", "hamburgers": "^1.2.1",
"marked": "^4.3.0",
"vue": "^3.2.47" "vue": "^3.2.47"
}, },
"devDependencies": { "devDependencies": {
"@types/dompurify": "^3.0.2",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.23", "postcss": "^8.4.23",

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import TopBar from './components/TopBar.vue' import TopBar from '@/components/TopBar.vue'
import SideBar from './components/SideBar.vue' import SideBar from '@/components/SideBar.vue'
import Note from './components/Note.vue' import Note from '@/components/Note.vue'
import { activeNote } from './composables/useNotes' import { activeNote } from '@/composables/useNotes'
const sideBarCollapsed = ref(false) const sideBarCollapsed = ref(false)

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { notes, findNotes } from '@/composables/useNotes'
const active = ref(false)
watch(active, () => {
if (!active.value) query.value = ''
})
const query = ref('')
const results = computed<Note[]>(() => {
return query.value ? findNotes(query.value) : notes.value
})
const click = (note: Note) => {
console.log(note.title)
}
</script>
<template>
<div id="search-container" class="relative h-full flex-1">
<input
type="text"
placeholder="Search for notes"
class="h-full w-full rounded border-0 bg-[#355fd3] px-2 text-white outline-none placeholder:text-white focus:bg-white focus:text-black"
@focus="active = true"
@mousedown="active = true"
@blur="active = false"
v-model="query"
/>
<div
class="z-1000 dropdown absolute left-0 right-0 top-[100%]"
v-if="active && results.length > 0"
>
<ul
tabindex="0"
class="menu mt-1 w-full rounded-md bg-base-100 p-2 text-black shadow"
>
<li v-for="note in results">
<a
class="px-2 py-1"
@click.stop.prevent="() => click(note)"
@mousedown.prevent
>{{ note.title }}</a
>
</li>
</ul>
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { rootNote } from '../composables/useNotes' import { rootNote } from '@/composables/useNotes'
import SideBarMenu from './SideBar/SideBarMenu.vue' import SideBarMenu from '@/components/SideBar/SideBarMenu.vue'
import SideBarMenuItem from './SideBar/SideBarMenuItem.vue' import SideBarMenuItem from '@/components/SideBar/SideBarMenuItem.vue'
const props = defineProps<{ const props = defineProps<{
viewModes: ViewMode[] viewModes: ViewMode[]

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Hamburger from './Hamburger.vue' import Hamburger from '@/components/Hamburger.vue'
import SearchBar from '@/components/SearchBar.vue'
import Logo from './Logo.vue' import Logo from './Logo.vue'
const props = defineProps<{ const props = defineProps<{
sideBarCollapsed: boolean sideBarCollapsed: boolean
@@ -24,20 +25,13 @@ const emit = defineEmits<{
/> />
<Logo class="ml-auto px-3 text-2xl" /> <Logo class="ml-auto px-3 text-2xl" />
</div> </div>
<div class="mr-3 flex h-full flex-1 items-center gap-3 py-2.5"> <div class="mr-3 flex h-full flex-1 items-center gap-2 py-2.5">
<input <SearchBar />
type="text" <button class="btn-outline btn-sm btn h-full text-white">+</button>
placeholder="Search for notes" <button class="btn-outline btn-sm btn h-full text-white">
class="h-full flex-1 rounded border-0 bg-[#355fd3] px-2 text-white outline-none placeholder:text-white focus:bg-white focus:text-black" Sign in
/> </button>
<button>+</button>
<button>Sign in</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="postcss">
button {
@apply h-full flex items-center rounded-md border-[1px] border-solid border-white bg-primary px-3 py-1 text-sm font-medium text-white hover:bg-white hover:text-black;
}
</style>

View File

@@ -1,7 +1,12 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
import { mdToHtml } from '@/utils/markdown'
export const notes = ref<Note[]>([]) const baseNotes = ref<BaseNote[]>([])
export const notes = computed<Note[]>(() => {
// extract links and add word count
return baseNotes.value as Note[]
})
export const activeNote = ref<Note>() export const activeNote = ref<Note>()
watch(activeNote, () => { watch(activeNote, () => {
@@ -19,12 +24,32 @@ watch(
{ immediate: true } { immediate: true }
) )
export const setDefaultNotes = (defaultNotes: Note[]) => { export const setDefaultNotes = (defaultNotes: BaseNote[]) => {
notes.value = defaultNotes.map( baseNotes.value = defaultNotes.map(
(note): Note => ({ (note): BaseNote => ({
...note, ...note,
created: new Date().getTime(), created: new Date().getTime(),
modified: new Date().getTime(), modified: new Date().getTime(),
}) })
) )
} }
export const getNoteById = (noteId: string) => {
return notes.value.find((note) => note.id === noteId)
}
export const findNotes = (query: string): Note[] => {
const removeMdFromText = (mdText: string): string => {
const div = document.createElement('div')
div.innerHTML = mdToHtml(mdText, 'c@')
const textWithoutMd = div.textContent || div.innerText || ''
return textWithoutMd
}
return notes.value.filter((note) => {
const matchTitle = note.title.toLowerCase().includes(query.toLowerCase())
const matchContent = removeMdFromText(note.content)
.toLowerCase()
.includes(query.toLowerCase())
return matchTitle || matchContent
})
}

11
src/global.d.ts vendored
View File

@@ -1,13 +1,16 @@
declare global { declare global {
interface Note { interface BaseNote {
id?: string id?: string
title: string title: string
content: string content: string
created: number created: number
modified: number modified: number
isRoot: boolean isRoot?: boolean
wordCount?: number }
links?: {
interface Note extends BaseNote {
wordCount: number
links: {
to: string[] to: string[]
from: string[] from: string[]
} }

View File

@@ -1,11 +1,11 @@
import { createApp } from 'vue' import { createApp } from 'vue'
// import './assets/style.scss' // import './assets/style.scss'
import './style.css' import '@/style.css'
import '@fortawesome/fontawesome-free/css/all.min.css' import '@fortawesome/fontawesome-free/css/all.min.css'
import 'bootstrap-icons/font/bootstrap-icons.css' import 'bootstrap-icons/font/bootstrap-icons.css'
import App from './App.vue' import App from './App.vue'
import { setDefaultNotes } from './composables/useNotes' import { setDefaultNotes } from '@/composables/useNotes'
import { defaultNotes } from './utils/defaultNotes' import { defaultNotes } from '@/utils/defaultNotes'
setDefaultNotes(defaultNotes) setDefaultNotes(defaultNotes)
createApp(App).mount('#app') createApp(App).mount('#app')

View File

@@ -1,4 +1,4 @@
export const defaultNotes: Note[] = [ export const defaultNotes: BaseNote[] = [
{ {
isRoot: true, isRoot: true,
title: 'Your first note', title: 'Your first note',

39
src/utils/markdown.ts Normal file
View File

@@ -0,0 +1,39 @@
import { marked } from 'marked'
import DOMPurify from 'dompurify'
export function mdToHtml(
mdText: string,
contextedPrefix: string,
getNoteById: (id: string) => Note | undefined
) {
const renderer = new marked.Renderer()
renderer.link = (href, _, text) => {
const isContextedLink = href?.startsWith(contextedPrefix)
if (isContextedLink) {
const re = new RegExp(`${contextedPrefix}([^]+)`)
const match = re.exec(href || '')
const contextedLinkOptions = match ? match[1].split(';') : []
const noteId = contextedLinkOptions[0] || ''
const note = getNoteById(noteId)
const contextedHref = ''
const contextedLink = `<a data-contexted-link="${noteId}" title="${note?.title}"${contextedHref}>${text}</a>`
return note?.title ? contextedLink : text
} else {
return `<a target="_blank" href="${href}">${text}</a>`
}
}
const html = DOMPurify.sanitize(marked.parse(mdText, { renderer }))
const re = /(\[\[)(.*?)(\]\])/g
const doc = new DOMParser().parseFromString(html, 'text/html')
doc.querySelectorAll('p, b, u, i, li, h1, h2, h3').forEach((element) => {
// if (element.childElementCount === 0) {
element.innerHTML = element.innerHTML.replace(re, (_, p1, p2, p3) => {
// const { id: noteId } = getters.getNoteByTitle(p2) || {}
return `${p1}<a data-contexted-link="true">${p2}</a>${p3}`
})
// }
})
return doc.body.innerHTML
}

View File

@@ -27,5 +27,22 @@ export default {
}, },
}, },
}, },
plugins: [], plugins: [require('daisyui')],
daisyui: {
themes: [
{
contexted: {
primary: '#1E4BC4',
secondary: '#F000B8',
accent: '#37CDBE',
neutral: '#3D4451',
'base-100': '#FFFFFF',
info: '#3ABFF8',
success: '#36D399',
warning: '#FBBD23',
error: '#F87272',
},
},
],
},
} }

View File

@@ -3,9 +3,12 @@
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -13,13 +16,26 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "preserve",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"paths": {
"@/*": [
"./src/*"
]
}
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "include": [
"references": [{ "path": "./tsconfig.node.json" }] "src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
} }

View File

@@ -1,7 +1,13 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
}) })