implement search
This commit is contained in:
552
package-lock.json
generated
552
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
49
src/components/SearchBar.vue
Normal file
49
src/components/SearchBar.vue
Normal 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>
|
||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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
11
src/global.d.ts
vendored
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
39
src/utils/markdown.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user