Compare commits
3 Commits
9b4e482e3a
...
6ac7ba8fd0
Author | SHA1 | Date |
---|---|---|
Chris Sexton | 6ac7ba8fd0 | |
Chris Sexton | f1f3c3dd5b | |
Chris Sexton | f7dfee399e |
|
@ -0,0 +1,249 @@
|
||||||
|
package entry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.chrissexton.org/cws/cabinet/db"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
db *db.Database
|
||||||
|
ID int64
|
||||||
|
Slug string
|
||||||
|
Content string
|
||||||
|
Tags []string
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
AuthorID int64 `db:"author_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareTable(tx *sqlx.Tx) error {
|
||||||
|
q := `create table if not exists entries (
|
||||||
|
id integer primary key,
|
||||||
|
slug text unique not null,
|
||||||
|
content text not null,
|
||||||
|
created datetime not null,
|
||||||
|
updated datetime not null,
|
||||||
|
author_id integer
|
||||||
|
)`
|
||||||
|
_, err := tx.Exec(q)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q = `create table if not exists tags (
|
||||||
|
id integer primary key,
|
||||||
|
name text not null,
|
||||||
|
entry_id integer,
|
||||||
|
foreign key(entry_id) references entries(id),
|
||||||
|
constraint unique_name_id unique (name, entry_id)
|
||||||
|
)`
|
||||||
|
_, err = tx.Exec(q)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *db.Database) Entry {
|
||||||
|
return Entry{
|
||||||
|
db: db,
|
||||||
|
ID: -1,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBySlug(db *db.Database, slug string) (Entry, error) {
|
||||||
|
e := Entry{db: db}
|
||||||
|
q := `select * from entries where slug = ?`
|
||||||
|
if err := db.Get(&e, q, slug); err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
return e, e.populateTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetByID(db *db.Database, id int64) (Entry, error) {
|
||||||
|
e := Entry{db: db}
|
||||||
|
q := `select * from entries where id = ?`
|
||||||
|
if err := db.Get(&e, q, id); err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
return e, e.populateTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Search(db *db.Database, query string) ([]*Entry, error) {
|
||||||
|
entries := []*Entry{}
|
||||||
|
if query != "" {
|
||||||
|
q := `select * from entries where content like '%?%'`
|
||||||
|
err := db.Select(&entries, q, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q := `select * from entries`
|
||||||
|
err := db.Select(&entries, q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
e.db = db
|
||||||
|
e.populateTags()
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveBySlug(db *db.Database, slug string) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e, err := GetBySlug(db, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q := `delete from tags where entry_id = ?`
|
||||||
|
if _, err := tx.Exec(q, e.ID); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q = `delete from entries where entry_id = ?`
|
||||||
|
if _, err := tx.Exec(q, e.ID); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) populateTags() error {
|
||||||
|
q := `select name from tags where entry_id = ?`
|
||||||
|
err := e.db.Select(&e.Tags, q, e.ID)
|
||||||
|
log.Debug().Interface("entry", e).Msg("populating tags")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) addTag(tag string) error {
|
||||||
|
q := `insert into tags (name,entry_id) values (?,?)`
|
||||||
|
_, err := e.db.Exec(q, tag, e.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) removeTag(tag string) error {
|
||||||
|
q := `delete from tags where name=? and entry_id=?`
|
||||||
|
_, err := e.db.Exec(q, tag, e.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) UniqueSlug() string {
|
||||||
|
candidate := strings.Split(e.Content, "\n")[0]
|
||||||
|
candidateNumber := 0
|
||||||
|
|
||||||
|
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
|
||||||
|
candidate = r.ReplaceAllString(candidate, "")
|
||||||
|
candidate = strings.TrimSpace(candidate)
|
||||||
|
candidate = strings.ReplaceAll(candidate, " ", "-")
|
||||||
|
if len(candidate) == 0 {
|
||||||
|
candidate = "untitled"
|
||||||
|
}
|
||||||
|
candidate = strings.ToLower(candidate)
|
||||||
|
|
||||||
|
q := `select slug from entries where slug like ?`
|
||||||
|
slugs := []string{}
|
||||||
|
if err := e.db.Select(&slugs, q, candidate+"%"); err != nil {
|
||||||
|
log.Debug().Err(err).Msgf("Could not get candidate slugs: %s", err)
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
contains := func(s string, ss []string) bool {
|
||||||
|
for _, e := range ss {
|
||||||
|
if s == e {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpCandidate := candidate
|
||||||
|
for contains(tmpCandidate, slugs) {
|
||||||
|
candidateNumber++
|
||||||
|
tmpCandidate = fmt.Sprintf("%s-%d", candidate, candidateNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) Update() error {
|
||||||
|
if e.ID == -1 {
|
||||||
|
return e.Create()
|
||||||
|
}
|
||||||
|
old, err := GetByID(e.db, e.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Slug = e.UniqueSlug()
|
||||||
|
|
||||||
|
q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?`
|
||||||
|
if _, err = e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, t := range e.Tags {
|
||||||
|
if !contains(old.Tags, t) {
|
||||||
|
e.addTag(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, t := range old.Tags {
|
||||||
|
if !contains(e.Tags, t) {
|
||||||
|
e.removeTag(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(items []string, entry string) bool {
|
||||||
|
for _, e := range items {
|
||||||
|
if e == entry {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) Create() error {
|
||||||
|
if e.ID != -1 {
|
||||||
|
return e.Update()
|
||||||
|
}
|
||||||
|
tx, err := e.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q := `insert into entries (slug,content,created,updated,author_id)
|
||||||
|
values (?,?,?,?,?)`
|
||||||
|
res, err := tx.Exec(q, e.Slug, e.Content, e.Created.Unix(), e.Updated.Unix(), e.AuthorID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.ID, err = res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
e.ID = -1
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, t := range e.Tags {
|
||||||
|
q = `insert into tags (name,entry_id) values (?,?)`
|
||||||
|
_, err = tx.Exec(q, t, e.ID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -8,14 +8,17 @@
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"asciidoctor": "^2.0.3",
|
||||||
|
"axios": "^0.19.0",
|
||||||
"bootstrap": "^4.3.1",
|
"bootstrap": "^4.3.1",
|
||||||
"bootstrap-vue": "^2.0.4",
|
"bootstrap-vue": "^2.0.4",
|
||||||
|
"brace": "latest",
|
||||||
"core-js": "^3.3.2",
|
"core-js": "^3.3.2",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.3",
|
||||||
"vue2-ace-editor": "^0.0.15",
|
"vue2-ace-editor": "^0.0.15",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1"
|
||||||
"brace": "latest"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.0.0",
|
"@vue/cli-plugin-babel": "^4.0.0",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<b-navbar type="dark" variant="dark">
|
<b-navbar type="dark" variant="dark">
|
||||||
<b-navbar-brand>Cabinet</b-navbar-brand>
|
<b-navbar-brand>🗄 Cabinet</b-navbar-brand>
|
||||||
<b-navbar-nav>
|
<b-navbar-nav>
|
||||||
<b-nav-item to="/">Home</b-nav-item>
|
<b-nav-item to="/">Home</b-nav-item>
|
||||||
<b-nav-item to="/console">Console</b-nav-item>
|
<b-nav-item to="/console">Console</b-nav-item>
|
||||||
<b-nav-item to="/about">About</b-nav-item>
|
<b-nav-item to="/about">About</b-nav-item>
|
||||||
</b-navbar-nav>
|
</b-navbar-nav>
|
||||||
</b-navbar>
|
</b-navbar>
|
||||||
|
<Error/>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,3 +34,14 @@
|
||||||
color: #42b983;
|
color: #42b983;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Error from "./components/Error";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app',
|
||||||
|
components: {
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<editor v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" />
|
<editor ref="myEditor" v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,7 +20,13 @@
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
content: function(newValue) {
|
content: function(newValue) {
|
||||||
|
console.log('text update\n'+newValue)
|
||||||
|
let editor = this.$refs.myEditor.editor
|
||||||
|
editor.renderer.updateFull()
|
||||||
this.text = newValue
|
this.text = newValue
|
||||||
|
},
|
||||||
|
text: function() {
|
||||||
|
this.$emit('change', this.text)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-alert
|
||||||
|
v-for="(e, i) in errs"
|
||||||
|
v-bind:key="i"
|
||||||
|
variant="danger"
|
||||||
|
dismissible
|
||||||
|
show="true"
|
||||||
|
v-show="errs.length > 0">{{e}}</b-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapState} from 'vuex';
|
||||||
|
export default {
|
||||||
|
name: "Error",
|
||||||
|
computed: mapState({
|
||||||
|
errs: state => state.errs
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -3,20 +3,13 @@
|
||||||
<b-container fluid>
|
<b-container fluid>
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col>
|
<b-col>
|
||||||
<Editor :content="content" />
|
<Editor :content="content" @change="updateContent"/>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
<b-row>
|
<b-row>
|
||||||
|
<b-col cols="1"><label style="padding-top: 0.5em" for="tagList">Tags</label></b-col>
|
||||||
<b-col cols="10">
|
<b-col cols="10">
|
||||||
<label for="tagList">Tags</label>
|
<TagList id="tagList" :tags="tags" @change="tagUpdate"/>
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="10">
|
|
||||||
<TagList id="tagList" :tags="tags" />
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="1">
|
|
||||||
<b-button variant="primary">Save</b-button>
|
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
</b-container>
|
||||||
|
@ -24,8 +17,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Editor from "./Editor";
|
import Editor from "./Editor"
|
||||||
import TagList from "./TagList";
|
import TagList from "./TagList"
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MainEditor",
|
name: "MainEditor",
|
||||||
props: {
|
props: {
|
||||||
|
@ -35,23 +30,18 @@
|
||||||
if (this.$props.slug) {
|
if (this.$props.slug) {
|
||||||
this.getFile(this.$props.slug)
|
this.getFile(this.$props.slug)
|
||||||
}
|
}
|
||||||
},
|
this.save = _.debounce(this.save, 2000)
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
file: ''
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tags: function() {
|
tags: function () {
|
||||||
if (this.file) {
|
if (this.$store.state.file) {
|
||||||
return this.file.tags
|
return this.$store.state.file.Tags
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
content: function () {
|
content: function () {
|
||||||
console.log('file:' + this.file)
|
if (this.$store.state.file) {
|
||||||
if (this.file) {
|
return this.$store.state.file.Content
|
||||||
return this.file.contents
|
|
||||||
}
|
}
|
||||||
return "= Main Editor"
|
return "= Main Editor"
|
||||||
}
|
}
|
||||||
|
@ -62,10 +52,35 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getFile: function(slug) {
|
getFile: function (slug) {
|
||||||
this.$store.dispatch('getFile', slug)
|
this.$store.dispatch('getFile', slug)
|
||||||
.then(file => {
|
},
|
||||||
this.file = file
|
tagUpdate: function(newTags) {
|
||||||
|
if (JSON.stringify(newTags) === JSON.stringify(this.file.Tags))
|
||||||
|
return
|
||||||
|
this.file.Tags = newTags
|
||||||
|
this.$emit('markDirty', true)
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
updateContent: function (newContent) {
|
||||||
|
if (this.$store.state.file.Content === newContent)
|
||||||
|
return
|
||||||
|
this.$store.state.file.Content = newContent
|
||||||
|
this.$emit('markDirty', true)
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
this.$store.state.file.Content = this.content
|
||||||
|
console.log("Saving file: " + this.file.Slug)
|
||||||
|
this.$store.dispatch('saveFile', this.file)
|
||||||
|
.then(res => {
|
||||||
|
this.$emit('markDirty', false)
|
||||||
|
this.$store.dispatch('updateSearch')
|
||||||
|
if (res.data.Slug != this.$route.params.slug)
|
||||||
|
this.$router.replace({params: { slug: res.data.Slug }})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log('err:' + err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<div :hidden="!content">
|
||||||
|
<b-container fluid>
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<Viewer :content="content" />
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="10">
|
||||||
|
<label for="tagList" >Tags</label>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="10">
|
||||||
|
<TagList id="tagList" :tags="tags" :readOnly="true" />
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Viewer from "./Viewer";
|
||||||
|
import TagList from "./TagList";
|
||||||
|
export default {
|
||||||
|
name: "MainView",
|
||||||
|
props: {
|
||||||
|
slug: String
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.$props.slug) {
|
||||||
|
this.getFile(this.$props.slug)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
file: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tags: function() {
|
||||||
|
if (this.$store.state.file) {
|
||||||
|
return this.$store.state.file.Tags
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
content: function () {
|
||||||
|
if (this.$store.state.file) {
|
||||||
|
return this.$store.state.file.Content
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
slug: function (newValue) {
|
||||||
|
this.getFile(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFile: function(slug) {
|
||||||
|
this.$store.dispatch('getFile', slug)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {TagList, Viewer}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<b-container fluid>
|
||||||
|
<b-row>
|
||||||
|
<b-input placeholder="Search" @update="runQuery" v-model="queryText" />
|
||||||
|
</b-row>
|
||||||
|
<b-row v-for="item in results" v-bind:key="item.ID">
|
||||||
|
<b-col>
|
||||||
|
<b-link
|
||||||
|
:to="{ name: target, params: { slug: item.Slug } }"
|
||||||
|
>{{item.Slug}}</b-link>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const _ = require('lodash');
|
||||||
|
export default {
|
||||||
|
name: "SearchResults",
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
queryText: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
target: String,
|
||||||
|
query: String
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
query: function(newValue) {
|
||||||
|
this.queryText = newValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
results: function() {
|
||||||
|
console.log("results:"+this.$store.state.searchResults)
|
||||||
|
return this.$store.state.searchResults
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getResults()
|
||||||
|
this.runQuery = _.debounce(this.runQuery, 1000)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getResults: function () {
|
||||||
|
this.$store.dispatch('getSearchResults', null)
|
||||||
|
},
|
||||||
|
runQuery: function() {
|
||||||
|
this.$store.dispatch('getSearchResults', this.query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,35 +0,0 @@
|
||||||
<template>
|
|
||||||
<b-container fluid>
|
|
||||||
<b-row v-for="item in results" v-bind:key="item.id">
|
|
||||||
<b-col>
|
|
||||||
<b-link
|
|
||||||
:to="{ name: 'console-slug', params: { slug: item.slug } }"
|
|
||||||
>{{item.title}}</b-link>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: "SearchResults",
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
results: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.getResults()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getResults: function () {
|
|
||||||
console.log('getResults')
|
|
||||||
this.$store.dispatch('getSearchResults')
|
|
||||||
.then(res => {
|
|
||||||
console.log('then'+res)
|
|
||||||
this.results = res
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,21 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<b-input type="text" placeholder="tags" :value="tagString"/>
|
<div>
|
||||||
|
<b-input type="text" placeholder="tags" :value="tagString" @update="changed" :hidden="readOnly"/>
|
||||||
|
<div :hidden="!readOnly">
|
||||||
|
<b-badge pill v-for="(tag, idx) in tags" v-bind:key="idx" class="tag">
|
||||||
|
{{tag}}
|
||||||
|
</b-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "TagList",
|
name: "TagList",
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
internalTags: []
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
tags: Array
|
tags: Array,
|
||||||
|
readOnly: Boolean
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
tags: function (newValue) {
|
||||||
|
this.internalTags = newValue
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tagString: function() {
|
tagString: function () {
|
||||||
return this.tags.join(' ')
|
return this.internalTags.join(',')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changed: function (value) {
|
||||||
|
this.internalTags = value.split(',')
|
||||||
|
this.$emit('change', this.internalTags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.tag {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div v-html="adoc">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let asciidoctor = require('asciidoctor')()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Viewer",
|
||||||
|
props: {
|
||||||
|
content: String
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
adoc: function () {
|
||||||
|
if (!this.content)
|
||||||
|
return ''
|
||||||
|
let doc = asciidoctor.convert(this.content, {
|
||||||
|
'safe': 'safe',
|
||||||
|
'attributes': {'showtitle': true, 'icons': 'font'}
|
||||||
|
})
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -7,10 +7,20 @@ Vue.use(VueRouter)
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/view/:slug',
|
||||||
name: 'home',
|
name: 'home-slug',
|
||||||
component: Home
|
component: Home
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/search/:query',
|
||||||
|
name: 'home-search',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/console/search/:query',
|
||||||
|
name: 'console-search',
|
||||||
|
component: Console
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/console/:slug',
|
path: '/console/:slug',
|
||||||
name: 'console-slug',
|
name: 'console-slug',
|
||||||
|
@ -28,6 +38,11 @@ const routes = [
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
// this generates a separate chunk (about.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: Home
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,79 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
// import _ from 'lodash'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
function getRandomInt(min, max) {
|
// function getRandomInt(min, max) {
|
||||||
min = Math.ceil(min)
|
// min = Math.ceil(min)
|
||||||
max = Math.floor(max)
|
// max = Math.floor(max)
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
// return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
}
|
// }
|
||||||
|
|
||||||
var files = {
|
// var files = {
|
||||||
'test-1': {id: 0, title: 'test 1', contents: '= test 1', slug: 'test-1', tags: ['a']},
|
// 'test-1': {id: 0, title: 'test 1', contents: '= test 1', slug: 'test-1', tags: ['a']},
|
||||||
'test-2': {id: 1, title: 'test 2', contents: '= test 2', slug: 'test-2', tags: ['b']},
|
// 'test-2': {id: 1, title: 'test 2', contents: '= test 2', slug: 'test-2', tags: ['b']},
|
||||||
'test-3': {id: 2, title: 'test 3', contents: '= test 3', slug: 'test-3', tags: ['a','b']},
|
// 'test-3': {id: 2, title: 'test 3', contents: '= test 3', slug: 'test-3', tags: ['a','b']},
|
||||||
'test-4': {id: 3, title: 'test 4', contents: '= test 4', slug: 'test-4', tags: ['c']}
|
// 'test-4': {id: 3, title: 'test 4', contents: '= test 4', slug: 'test-4', tags: ['c']}
|
||||||
}
|
// }
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
|
errs: [],
|
||||||
|
searchResults: [],
|
||||||
|
query: null,
|
||||||
|
file: null
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
clearError(state) {
|
||||||
|
state.errs = []
|
||||||
|
},
|
||||||
|
addError(state, err) {
|
||||||
|
state.errs.push(err)
|
||||||
|
},
|
||||||
|
setResults(state, results) {
|
||||||
|
state.searchResults = results
|
||||||
|
},
|
||||||
|
setQuery(state, query) {
|
||||||
|
state.query = query
|
||||||
|
},
|
||||||
|
setFile(state, file) {
|
||||||
|
state.file = file
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
getFile: function(_, slug) {
|
getFile: function({ commit }, slug) {
|
||||||
console.log('getFile('+slug+')')
|
if (slug)
|
||||||
return new Promise(function (resolve) {
|
return axios.get('/v1/entries/'+slug)
|
||||||
setTimeout(() => resolve(files[slug]), getRandomInt(0, 1000))
|
.catch(err => commit('addError', err))
|
||||||
})
|
.then(res => {
|
||||||
|
commit('setFile', res.data)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
getSearchResults: function () {
|
getSearchResults: function ({dispatch, commit}, query) {
|
||||||
return new Promise(function (resolve) {
|
commit('setQuery', query)
|
||||||
setTimeout(() => resolve(Object.values(files)), getRandomInt(0, 1000))
|
dispatch('updateSearch')
|
||||||
})
|
},
|
||||||
|
updateSearch: function ({commit, state}) {
|
||||||
|
let query = state.query
|
||||||
|
if (query) {
|
||||||
|
axios.get('/v1/entries?query='+query)
|
||||||
|
.catch(err => state.addError(err))
|
||||||
|
.then(res =>{
|
||||||
|
console.log("getSearchResults:"+res.data)
|
||||||
|
commit('setResults', res.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
axios.get('/v1/entries')
|
||||||
|
.catch(err => state.addError(err))
|
||||||
|
.then(res =>{
|
||||||
|
console.log("getSearchResults:"+res.data)
|
||||||
|
commit('setResults', res.data)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
saveFile: function(state, file) {
|
||||||
|
return axios.put('/v1/entries/'+file.Slug, file)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
|
|
|
@ -1,22 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<b-container fluid>
|
<b-container fluid>
|
||||||
<b-row>
|
|
||||||
<b-col>
|
|
||||||
<h1>Console</h1>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col md="5">
|
<b-col md="5">
|
||||||
<h2>Scratchpad</h2>
|
<h2>Scratchpad</h2>
|
||||||
<ScratchPad />
|
<ScratchPad />
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col md="5">
|
<b-col md="5">
|
||||||
<h2>Editor</h2>
|
|
||||||
<MainEditor :slug="$route.params.slug" />
|
<div>
|
||||||
|
<b-tabs content-class="mt-3">
|
||||||
|
<b-tab active>
|
||||||
|
<template v-slot:title>
|
||||||
|
Editor <span v-bind:class="{ dirty: isDirty, clean: !isDirty }">•</span>
|
||||||
|
</template>
|
||||||
|
<MainEditor :slug="$route.params.slug" @markDirty="markDirty"/>
|
||||||
|
</b-tab>
|
||||||
|
<b-tab title="Viewer">
|
||||||
|
<MainView :slug="$route.params.slug" />
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col md="2">
|
<b-col md="2">
|
||||||
<h2>Search Results</h2>
|
<h2>Search Results</h2>
|
||||||
<SearchResults />
|
<SearchResults target="console-slug" :query="query"/>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
</b-container>
|
||||||
|
@ -25,15 +34,63 @@
|
||||||
<script>
|
<script>
|
||||||
// @ is an alias to /src
|
// @ is an alias to /src
|
||||||
import MainEditor from "../components/MainEditor";
|
import MainEditor from "../components/MainEditor";
|
||||||
|
import MainView from "../components/MainView";
|
||||||
import ScratchPad from "../components/ScratchPad";
|
import ScratchPad from "../components/ScratchPad";
|
||||||
import SearchResults from "../components/SearchResults";
|
import SearchResults from "../components/Search";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'home',
|
name: 'home',
|
||||||
components: {
|
components: {
|
||||||
SearchResults,
|
SearchResults,
|
||||||
ScratchPad,
|
ScratchPad,
|
||||||
MainEditor
|
MainEditor,
|
||||||
|
MainView
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
isDirty: false,
|
||||||
|
query: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
update: function() {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
markDirty: function(dirty) {
|
||||||
|
console.log('markDirty:'+dirty)
|
||||||
|
this.isDirty = dirty
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeRouteEnter (to, from, next) {
|
||||||
|
// called before the route that renders this component is confirmed.
|
||||||
|
// does NOT have access to `this` component instance,
|
||||||
|
// because it has not been created yet when this guard is called!
|
||||||
|
console.log('beforeRouteEnter'+to+from)
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
beforeRouteUpdate (to, from, next) {
|
||||||
|
// called when the route that renders this component has changed,
|
||||||
|
// but this component is reused in the new route.
|
||||||
|
// For example, for a route with dynamic params `/foo/:id`, when we
|
||||||
|
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
|
||||||
|
// will be reused, and this hook will be called when that happens.
|
||||||
|
// has access to `this` component instance.
|
||||||
|
if (this.isDirty) {
|
||||||
|
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
|
||||||
|
if (answer) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
beforeRouteLeave (to, from, next) {
|
||||||
|
// called when the route that renders this component is about to
|
||||||
|
// be navigated away from.
|
||||||
|
// has access to `this` component instance.
|
||||||
|
console.log('beforeRouteLeave'+to+from)
|
||||||
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -42,4 +99,10 @@ export default {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
}
|
}
|
||||||
|
.dirty {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.clean {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,18 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="home">
|
<b-container fluid>
|
||||||
<img alt="Vue logo" src="../assets/logo.png">
|
<b-row><b-col>
|
||||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
<h1>Home</h1>
|
||||||
</div>
|
</b-col></b-row>
|
||||||
|
<b-row>
|
||||||
|
<b-col md="2">
|
||||||
|
<SearchResults target="home-slug" :query="$route.params.query" />
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<MainView :slug="$route.params.slug" />
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// @ is an alias to /src
|
import SearchResults from '../components/Search.vue'
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'
|
import MainView from '../components/MainView.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'home',
|
name: 'home',
|
||||||
components: {
|
components: {
|
||||||
HelloWorld
|
SearchResults,
|
||||||
|
MainView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h2 {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -2,6 +2,21 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@asciidoctor/cli@2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@asciidoctor/cli/-/cli-2.0.1.tgz#5e0154ff06da21aedb1840bff5d49ca6ee7d520d"
|
||||||
|
integrity sha512-XNqnHPMVTRMDlKk6EEiA61P0WImHqOecoky1p8HIek+0u1jbKbiDku8WraC2EaNy8IWyEjfrdnrDYGvogBVqzA==
|
||||||
|
dependencies:
|
||||||
|
yargs "13.2.2"
|
||||||
|
|
||||||
|
"@asciidoctor/core@2.0.3":
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@asciidoctor/core/-/core-2.0.3.tgz#95630424eb2e346acc51dede1e6599159fbd5d44"
|
||||||
|
integrity sha512-iN0zV/tsL36nTltJyk5IdcRqDW34HHiwb9PrgSoTGDCIEML6C3HYnJOQi+jKq7A//Gt1nL1SHohaCHkyw3swEg==
|
||||||
|
dependencies:
|
||||||
|
asciidoctor-opal-runtime "0.3.0"
|
||||||
|
unxhr "1.0.1"
|
||||||
|
|
||||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
|
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
|
||||||
|
@ -1385,6 +1400,22 @@ array-unique@^0.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
||||||
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
|
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
|
||||||
|
|
||||||
|
asciidoctor-opal-runtime@0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.0.tgz#df327a870ddd3cd5eb0e162d64ed4dcdd3fe3fee"
|
||||||
|
integrity sha512-YapVwl2qbbs6sIe1dvAlMpBzQksFVTSa2HOduOKFNhZlE9bNmn+moDgGVvjWPbzMPo/g8gItyTHfWB2u7bQxag==
|
||||||
|
dependencies:
|
||||||
|
glob "7.1.3"
|
||||||
|
unxhr "1.0.1"
|
||||||
|
|
||||||
|
asciidoctor@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/asciidoctor/-/asciidoctor-2.0.3.tgz#350a41d051c5f99d1d8fa4abdc7a9c80da48f90a"
|
||||||
|
integrity sha512-6UB/oB0jRQOy6/DoFBH1nUzAAGt7Aa6snfXKZyvaxKxQHcuvFuJcvS2zVjAH2eURQidkBLy+J0VHBVjRWs8CiQ==
|
||||||
|
dependencies:
|
||||||
|
"@asciidoctor/cli" "2.0.1"
|
||||||
|
"@asciidoctor/core" "2.0.3"
|
||||||
|
|
||||||
asn1.js@^4.0.0:
|
asn1.js@^4.0.0:
|
||||||
version "4.10.1"
|
version "4.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
|
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
|
||||||
|
@ -1474,6 +1505,14 @@ aws4@^1.8.0:
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||||
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
|
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
|
||||||
|
|
||||||
|
axios@^0.19.0:
|
||||||
|
version "0.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
|
||||||
|
integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "1.5.10"
|
||||||
|
is-buffer "^2.0.2"
|
||||||
|
|
||||||
babel-eslint@^10.0.3:
|
babel-eslint@^10.0.3:
|
||||||
version "10.0.3"
|
version "10.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
|
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
|
||||||
|
@ -1641,7 +1680,7 @@ brace-expansion@^1.1.7:
|
||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
brace@^0.11.0:
|
brace@^0.11.0, brace@latest:
|
||||||
version "0.11.1"
|
version "0.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
|
resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58"
|
||||||
integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=
|
integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=
|
||||||
|
@ -2609,6 +2648,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
|
debug@=3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||||
|
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
|
debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
|
||||||
version "3.2.6"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||||
|
@ -3517,6 +3563,13 @@ flush-write-stream@^1.0.0:
|
||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
readable-stream "^2.3.6"
|
||||||
|
|
||||||
|
follow-redirects@1.5.10:
|
||||||
|
version "1.5.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||||
|
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
|
||||||
|
dependencies:
|
||||||
|
debug "=3.1.0"
|
||||||
|
|
||||||
follow-redirects@^1.0.0:
|
follow-redirects@^1.0.0:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
|
||||||
|
@ -3692,6 +3745,18 @@ glob-to-regexp@^0.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
||||||
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
|
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
|
||||||
|
|
||||||
|
glob@7.1.3:
|
||||||
|
version "7.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||||
|
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
|
||||||
|
dependencies:
|
||||||
|
fs.realpath "^1.0.0"
|
||||||
|
inflight "^1.0.4"
|
||||||
|
inherits "2"
|
||||||
|
minimatch "^3.0.4"
|
||||||
|
once "^1.3.0"
|
||||||
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
|
glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
|
||||||
version "7.1.5"
|
version "7.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
|
||||||
|
@ -4282,6 +4347,11 @@ is-buffer@^1.1.5:
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||||
|
|
||||||
|
is-buffer@^2.0.2:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
|
||||||
|
integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
|
||||||
|
|
||||||
is-callable@^1.1.4:
|
is-callable@^1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
|
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
|
||||||
|
@ -5590,7 +5660,7 @@ os-homedir@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||||
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
|
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
|
||||||
|
|
||||||
os-locale@^3.0.0:
|
os-locale@^3.0.0, os-locale@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
|
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
|
||||||
integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
|
integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
|
||||||
|
@ -7753,6 +7823,11 @@ unset-value@^1.0.0:
|
||||||
has-value "^0.3.1"
|
has-value "^0.3.1"
|
||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
|
|
||||||
|
unxhr@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/unxhr/-/unxhr-1.0.1.tgz#92200322d66c728993de771f9e01eeb21f41bc7b"
|
||||||
|
integrity sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==
|
||||||
|
|
||||||
upath@^1.1.1:
|
upath@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
|
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
|
||||||
|
@ -8228,7 +8303,7 @@ yargs-parser@^11.1.1:
|
||||||
camelcase "^5.0.0"
|
camelcase "^5.0.0"
|
||||||
decamelize "^1.2.0"
|
decamelize "^1.2.0"
|
||||||
|
|
||||||
yargs-parser@^13.1.1:
|
yargs-parser@^13.0.0, yargs-parser@^13.1.1:
|
||||||
version "13.1.1"
|
version "13.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
|
||||||
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
|
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
|
||||||
|
@ -8254,6 +8329,23 @@ yargs@12.0.5:
|
||||||
y18n "^3.2.1 || ^4.0.0"
|
y18n "^3.2.1 || ^4.0.0"
|
||||||
yargs-parser "^11.1.1"
|
yargs-parser "^11.1.1"
|
||||||
|
|
||||||
|
yargs@13.2.2:
|
||||||
|
version "13.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993"
|
||||||
|
integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==
|
||||||
|
dependencies:
|
||||||
|
cliui "^4.0.0"
|
||||||
|
find-up "^3.0.0"
|
||||||
|
get-caller-file "^2.0.1"
|
||||||
|
os-locale "^3.1.0"
|
||||||
|
require-directory "^2.1.1"
|
||||||
|
require-main-filename "^2.0.0"
|
||||||
|
set-blocking "^2.0.0"
|
||||||
|
string-width "^3.0.0"
|
||||||
|
which-module "^2.0.0"
|
||||||
|
y18n "^4.0.0"
|
||||||
|
yargs-parser "^13.0.0"
|
||||||
|
|
||||||
yargs@^13.0.0:
|
yargs@^13.0.0:
|
||||||
version "13.3.0"
|
version "13.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
|
||||||
|
|
11
main.go
11
main.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.chrissexton.org/cws/cabinet/entry"
|
||||||
"flag"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
@ -16,9 +17,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dbPath = flag.String("db", "happy.db", "path to db")
|
dbPath = flag.String("db", "cabinet.db", "path to db")
|
||||||
httpAddr = flag.String("httpAddr", "0.0.0.0:8080", "http address")
|
httpAddr = flag.String("httpAddr", "0.0.0.0:8080", "http address")
|
||||||
salt = flag.String("salt", "happy", "salt for IDs")
|
salt = flag.String("salt", "c4b1n3t", "salt for IDs")
|
||||||
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
|
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
|
||||||
develop = flag.Bool("develop", false, "turn on develop mode")
|
develop = flag.Bool("develop", false, "turn on develop mode")
|
||||||
)
|
)
|
||||||
|
@ -38,6 +39,12 @@ func main() {
|
||||||
Msg("could not connect to database")
|
Msg("could not connect to database")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx := db.MustBegin()
|
||||||
|
if err := entry.PrepareTable(tx); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("could not create database")
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
s := web.New(*httpAddr, db, box)
|
s := web.New(*httpAddr, db, box)
|
||||||
s.Serve()
|
s.Serve()
|
||||||
}
|
}
|
||||||
|
|
12
todo.adoc
12
todo.adoc
|
@ -9,13 +9,19 @@
|
||||||
* Vue Frontend
|
* Vue Frontend
|
||||||
** [ ] spend some time learning about TypeScript/Vue.js style
|
** [ ] spend some time learning about TypeScript/Vue.js style
|
||||||
** Documents
|
** Documents
|
||||||
*** [ ] adoc editor widget
|
*** [x] adoc editor widget
|
||||||
** Authentication
|
** Authentication
|
||||||
*** [ ] some kind of user auth
|
*** [ ] some kind of user auth
|
||||||
** Views
|
** Views
|
||||||
*** [ ] editor view
|
*** [ ] editor view
|
||||||
*** [ ] public index/search view
|
*** [ ] public index/search view
|
||||||
* Backend
|
* Backend
|
||||||
** [ ] save endpoint
|
** [?] save endpoint
|
||||||
|
*** [ ] need to generate a slug for entries
|
||||||
|
*** [ ] add authentication/authorization
|
||||||
|
*** [ ] convert document to adoc (give format?)
|
||||||
|
*** [x] test in frontend
|
||||||
|
*** [ ] check for unique tags
|
||||||
|
*** [ ] set some db fields not null
|
||||||
** [ ] search endpoint
|
** [ ] search endpoint
|
||||||
* CLI Frontend
|
* CLI Frontend
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"code.chrissexton.org/cws/cabinet/entry"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (web *Web) editEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
slug := vars["slug"]
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
newEntry := entry.New(web.db)
|
||||||
|
err := dec.Decode(&newEntry)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldEntry, err := entry.GetBySlug(web.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldEntry.Content = newEntry.Content
|
||||||
|
oldEntry.Tags = newEntry.Tags
|
||||||
|
oldEntry.Updated = time.Now()
|
||||||
|
|
||||||
|
err = oldEntry.Update()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := json.Marshal(oldEntry)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Interface("oldEntry", oldEntry).Msg("Got a PUT")
|
||||||
|
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
fmt.Fprint(w, string(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) newEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dec := json.NewDecoder(r.Body)
|
||||||
|
newEntry := entry.New(web.db)
|
||||||
|
err := dec.Decode(&newEntry)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = newEntry.Create()
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := json.Marshal(newEntry)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
fmt.Fprint(w, string(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
query := r.Form.Get("query")
|
||||||
|
entries, err := entry.Search(web.db, query)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := json.Marshal(entries)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
fmt.Fprint(w, string(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
slug := vars["slug"]
|
||||||
|
|
||||||
|
entry, err := entry.GetBySlug(web.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := json.Marshal(entry)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
fmt.Fprint(w, string(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) removeEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
slug := vars["slug"]
|
||||||
|
|
||||||
|
err := entry.RemoveBySlug(web.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
|
@ -41,7 +41,12 @@ func New(addr string, db *db.Database, box *packr.Box) *Web {
|
||||||
|
|
||||||
func (web *Web) routeSetup() http.Handler {
|
func (web *Web) routeSetup() http.Handler {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
//api := r.PathPrefix("/v1/").Subrouter()
|
api := r.PathPrefix("/v1/").Subrouter()
|
||||||
|
api.HandleFunc("/entries", web.allEntries).Methods(http.MethodGet)
|
||||||
|
api.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost)
|
||||||
|
api.HandleFunc("/entries", web.removeEntry).Methods(http.MethodDelete)
|
||||||
|
api.HandleFunc("/entries/{slug}", web.editEntry).Methods(http.MethodPut)
|
||||||
|
api.HandleFunc("/entries/{slug}", web.getEntry).Methods(http.MethodGet)
|
||||||
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))
|
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))
|
||||||
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
||||||
return loggedRouter
|
return loggedRouter
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func search(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue