This commit is contained in:
Meutel 2017-08-06 12:50:43 +02:00
parent 297d54fdc1
commit ecdf3d5ae8
12 changed files with 420 additions and 117 deletions

View File

@ -4,7 +4,8 @@ Bouquins in Go
## TODO
* search
* search (table results: authors, tags...)
* search in header
* About
* translations
* tests

View File

@ -1,7 +1,53 @@
var app = new Vue({
el: '#app',
Vue.component('results-list', {
template: '#results-list-template',
props: ['results', 'count', 'type'],
methods: {
url: function(item) {
return '/'+this.type+'/'+item.id;
},
label: function(item) {
switch (this.type) {
case 'books':
return item.title;
case 'authors':
case 'series':
return item.name;
default:
return '';
}
},
iconClass: function() {
return 'glyphicon glyphicon-' + this.icon();
},
icon: function() {
switch (this.type) {
case 'books':
return 'icon';
case 'authors':
return 'user';
case 'series':
return 'list';
default:
return '';
}
},
countlabel: function() {
switch (this.type) {
case 'books':
return this.count > 1 ? 'livres' : 'livre';
case 'authors':
return this.count > 1 ? 'auteurs' : 'auteur';
case 'series':
return this.count > 1 ? 'series' : 'serie';
default:
return '';
}
}
}
});
var search = new Vue({
el: '#search',
data: {
urlParams: {},
authors: [],
books: [],
series: [],
@ -14,43 +60,6 @@ var app = new Vue({
perpage: 10
},
methods: {
urlParse: function() {
var match,
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
query = window.location.search.substring(1);
while (match = search.exec(query))
this.urlParams[decode(match[1])] = decode(match[2]);
},
sendQuery: function(url, error, success) {
var xmh = new XMLHttpRequest();
var v;
xmh.onreadystatechange = function() {
v = xmh.responseText;
if (xmh.readyState === 4 && xmh.status === 200) {
var res;
try {
res = JSON.parse(v);
} catch (err) {
if (null !== error)
error(err.name, err.message);
}
if (null !== success)
success(res);
} else if (xmh.readyState === 4) {
if (null !== error)
error(xmh.status, v);
}
};
xmh.open('GET', url, true);
xmh.send(null);
},
stdError: function(code, resp) {
console.log('ERROR ' + code + ': ' + resp);
},
searchParams: function(url) {
var res = url;
res += '?perpage=' + this.perpage;
@ -63,24 +72,24 @@ var app = new Vue({
},
searchAuthorsSuccess: function(res) {
this.authorsCount = res.count;
this.authors = res.authors;
this.authors = res.results;
},
searchAuthors: function() {
this.sendQuery(this.searchParams('cgi-bin/bouquins/authors'), this.stdError, this.searchAuthorsSuccess);
this.sendQuery(this.searchParams('/authors/'), this.stdError, this.searchAuthorsSuccess);
},
searchBooksSuccess: function(res) {
this.booksCount = res.count;
this.books = res.books;
this.books = res.results;
},
searchBooks: function() {
this.sendQuery(this.searchParams('cgi-bin/bouquins/books'), this.stdError, this.searchBooksSuccess);
this.sendQuery(this.searchParams('/books/'), this.stdError, this.searchBooksSuccess);
},
searchSeriesSuccess: function(res) {
this.seriesCount = res.count;
this.series = res.series;
this.series = res.results;
},
searchSeries: function() {
this.sendQuery(this.searchParams('cgi-bin/bouquins/series'), this.stdError, this.searchSeriesSuccess);
this.sendQuery(this.searchParams('/series/'), this.stdError, this.searchSeriesSuccess);
},
searchAll: function() {
this.clear();
@ -125,12 +134,33 @@ var app = new Vue({
this.searchAll();
this.q = this.urlParams.q;
}
}
},
created: function() {
this.urlParse();
},
mounted: function() {
this.searchUrl();
sendQuery: function(url, error, success) {
var xmh = new XMLHttpRequest();
var v;
xmh.onreadystatechange = function() {
v = xmh.responseText;
if (xmh.readyState === 4 && xmh.status === 200) {
var res;
try {
res = JSON.parse(v);
} catch (err) {
if (null !== error)
error(err.name, err.message);
}
})
if (null !== success)
success(res);
} else if (xmh.readyState === 4) {
if (null !== error)
error(xmh.status, v);
}
};
xmh.open('GET', url, true);
xmh.setRequestHeader('Accept','application/json');
xmh.send(null);
},
stdError: function(code, resp) {
console.log('ERROR ' + code + ': ' + resp);
}
}
});

View File

@ -20,12 +20,14 @@ const (
TPL_AUTHORS = "author.html"
TPL_SERIES = "series.html"
TPL_INDEX = "index.html"
TPL_SEARCH = "search.html"
PARAM_LIST = "list"
PARAM_ORDER = "order"
PARAM_SORT = "sort"
PARAM_PAGE = "page"
PARAM_PERPAGE = "perpage"
PARAM_TERM = "term"
LIST_AUTHORS = "authors"
LIST_SERIES = "series"
@ -35,6 +37,7 @@ const (
URL_BOOKS = "/books/"
URL_AUTHORS = "/authors/"
URL_SERIES = "/series/"
URL_SEARCH = "/search/"
URL_JS = "/js/"
URL_CSS = "/css/"
URL_FONTS = "/fonts/"
@ -159,17 +162,26 @@ func NewIndexModel(title, js string, count int64) *IndexModel {
}
}
type SearchModel struct {
BouquinsModel
}
func NewSearchModel() *SearchModel {
return &SearchModel{*NewBouquinsModelJs("Recherche", "search.js")}
}
type ResultsModel struct {
Type string `json:"type,omitempty"`
More bool `json:"more"`
CountResults int `json:"count,omitempty"`
}
type BooksResultsModel struct {
ResultsModel
Results []*BookAdv `json:"results,omitempty"`
}
func NewBooksResultsModel(books []*BookAdv, more bool) *BooksResultsModel {
return &BooksResultsModel{ResultsModel{"books", more}, books}
func NewBooksResultsModel(books []*BookAdv, more bool, count int) *BooksResultsModel {
return &BooksResultsModel{ResultsModel{"books", more, count}, books}
}
type AuthorsResultsModel struct {
@ -177,8 +189,8 @@ type AuthorsResultsModel struct {
Results []*AuthorAdv `json:"results,omitempty"`
}
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool) *AuthorsResultsModel {
return &AuthorsResultsModel{ResultsModel{"authors", more}, authors}
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool, count int) *AuthorsResultsModel {
return &AuthorsResultsModel{ResultsModel{"authors", more, count}, authors}
}
type SeriesResultsModel struct {
@ -186,8 +198,8 @@ type SeriesResultsModel struct {
Results []*SeriesAdv `json:"results,omitempty"`
}
func NewSeriesResultsModel(series []*SeriesAdv, more bool) *SeriesResultsModel {
return &SeriesResultsModel{ResultsModel{"series", more}, series}
func NewSeriesResultsModel(series []*SeriesAdv, more bool, count int) *SeriesResultsModel {
return &SeriesResultsModel{ResultsModel{"series", more, count}, series}
}
type BookModel struct {
@ -205,6 +217,15 @@ type AuthorModel struct {
*AuthorFull
}
type ReqParams struct {
Limit int
Offset int
Sort string
Order string
Terms []string
AllWords bool
}
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
return app.Template.ExecuteTemplate(res, tpl, model)
}
@ -235,6 +256,9 @@ func isJson(req *http.Request) bool {
}
func paramInt(name string, req *http.Request) int {
val := req.URL.Query().Get(name)
if val == "" {
return 0
}
valInt, err := strconv.Atoi(val)
if err != nil {
log.Println("Invalid value for", name, ":", val)
@ -250,8 +274,7 @@ func paramOrder(req *http.Request) string {
return ""
}
// return limit, offset, sort, order
func paramPaginate(req *http.Request) (int, int, string, string) {
func params(req *http.Request) *ReqParams {
page, perpage := paramInt(PARAM_PAGE, req), paramInt(PARAM_PERPAGE, req)
limit := perpage
if perpage == 0 {
@ -263,7 +286,8 @@ func paramPaginate(req *http.Request) (int, int, string, string) {
}
sort := req.URL.Query().Get(PARAM_SORT)
order := paramOrder(req)
return limit, offset, sort, order
terms := req.URL.Query()[PARAM_TERM]
return &ReqParams{limit, offset, sort, order, terms, false}
}
// ROUTES //
@ -281,16 +305,19 @@ func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) {
http.Error(res, err.Error(), 500)
}
} else {
app.render(res, TPL_INDEX, model)
err = app.render(res, TPL_INDEX, model)
if err != nil {
log.Println(err)
}
}
}
func (app *Bouquins) BooksListPage(res http.ResponseWriter, req *http.Request) error {
if isJson(req) {
books, more, err := app.BooksAdv(paramPaginate(req))
books, count, more, err := app.BooksAdv(params(req))
if err != nil {
return err
}
return writeJson(res, NewBooksResultsModel(books, more))
return writeJson(res, NewBooksResultsModel(books, more, count))
}
return errors.New("Invalid mime")
}
@ -325,11 +352,11 @@ func (app *Bouquins) BooksPage(res http.ResponseWriter, req *http.Request) {
}
func (app *Bouquins) AuthorsListPage(res http.ResponseWriter, req *http.Request) error {
if isJson(req) {
authors, more, err := app.AuthorsAdv(paramPaginate(req))
authors, count, more, err := app.AuthorsAdv(params(req))
if err != nil {
return err
}
return writeJson(res, NewAuthorsResultsModel(authors, more))
return writeJson(res, NewAuthorsResultsModel(authors, more, count))
}
return errors.New("Invalid mime")
}
@ -364,11 +391,11 @@ func (app *Bouquins) AuthorsPage(res http.ResponseWriter, req *http.Request) {
}
func (app *Bouquins) SeriesListPage(res http.ResponseWriter, req *http.Request) error {
if isJson(req) {
series, more, err := app.SeriesAdv(paramPaginate(req))
series, count, more, err := app.SeriesAdv(params(req))
if err != nil {
return err
}
return writeJson(res, NewSeriesResultsModel(series, more))
return writeJson(res, NewSeriesResultsModel(series, more, count))
}
return errors.New("Invalid mime")
}
@ -401,3 +428,10 @@ func (app *Bouquins) SeriesPage(res http.ResponseWriter, req *http.Request) {
http.Error(res, err.Error(), 500)
}
}
func (app *Bouquins) SearchPage(res http.ResponseWriter, req *http.Request) {
model := NewSearchModel()
err := app.render(res, TPL_SEARCH, model)
if err != nil {
log.Println(err)
}
}

View File

@ -37,7 +37,10 @@ const (
STMT_WHERE = " WHERE "
STMT_BOOL_AND = " AND "
STMT_BOOL_OR = " OR "
STMT_SEARCH_ORDER = " ORDER BY books.sort"
STMT_SEARCH_ORDER_BOOKS = " ORDER BY books.sort"
STMT_SEARCH_ORDER_AUTHORS = " ORDER BY authors.sort"
STMT_SEARCH_ORDER_SERIES = " ORDER BY series.sort"
STMT_BOOKS_COUNT = "SELECT count(id) FROM books"
STMT_BOOK = `SELECT books.id AS id,title, series_index, series.name AS series_name, series.id AS series_id,

View File

@ -2,10 +2,54 @@ package bouquins
import (
"database/sql"
"log"
)
// SUB QUERIES //
func (app *Bouquins) searchAuthors(limit int, terms []string, all bool) ([]*AuthorAdv, int, error) {
authors := make([]*AuthorAdv, 0, limit)
count := 0
query := STMT_AUTHORS_SEARCH
queryTerms := make([]interface{}, 0, len(terms))
for i, term := range terms {
queryTerms = append(queryTerms, "%"+term+"%")
query += STMT_SEARCH_TERM_AUTHOR
if i < len(terms)-1 && all {
query += STMT_BOOL_AND
}
if i < len(terms)-1 && !all {
query += STMT_BOOL_OR
}
}
query += STMT_SEARCH_ORDER_AUTHORS
log.Println("Search:", query)
stmt, err := app.DB.Prepare(query)
if err != nil {
return nil, 0, err
}
rows, err := stmt.Query(queryTerms...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
for rows.Next() {
if len(authors) <= limit {
author := new(AuthorAdv)
if err := rows.Scan(&author.Id, &author.Name); err != nil {
return nil, 0, err
}
authors = append(authors, author)
}
count++
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return authors, count, nil
}
func (app *Bouquins) queryAuthors(limit, offset int, sort, order string) ([]*AuthorAdv, bool, error) {
authors := make([]*AuthorAdv, 0, limit)
stmt, err := app.psSortAuthors(AUTHORS, sort, order)
@ -110,12 +154,17 @@ func (app *Bouquins) queryAuthor(id int64) (*AuthorFull, error) {
// DB LOADS //
func (app *Bouquins) AuthorsAdv(limit, offset int, sort, order string) ([]*AuthorAdv, bool, error) {
func (app *Bouquins) AuthorsAdv(params *ReqParams) ([]*AuthorAdv, int, bool, error) {
limit, offset, sort, order := params.Limit, params.Offset, params.Sort, params.Order
if len(params.Terms) > 0 {
authors, count, err := app.searchAuthors(limit, params.Terms, params.AllWords)
return authors, count, count > limit, err
}
authors, more, err := app.queryAuthors(limit, offset, sort, order)
if err != nil {
return nil, false, err
return nil, 0, false, err
}
return authors, more, nil
return authors, 0, more, nil
}
func (app *Bouquins) AuthorFull(id int64) (*AuthorFull, error) {

View File

@ -1,6 +1,9 @@
package bouquins
import "database/sql"
import (
"database/sql"
"log"
)
// MERGE SUB QUERIES //
func assignAuthorsTagsBooks(books []*BookAdv, authors map[int64][]*Author, tags map[int64][]string) {
@ -12,6 +15,59 @@ func assignAuthorsTagsBooks(books []*BookAdv, authors map[int64][]*Author, tags
// SUB QUERIES //
func (app *Bouquins) searchBooks(limit int, terms []string, all bool) ([]*BookAdv, int, error) {
books := make([]*BookAdv, 0, limit)
// FIXME factorize searchAuthors,searchSeries
count := 0
query := STMT_BOOKS0 + STMT_WHERE
queryTerms := make([]interface{}, 0, len(terms))
for i, term := range terms {
queryTerms = append(queryTerms, "%"+term+"%")
query += STMT_SEARCH_TERM_BOOKS
if i < len(terms)-1 && all {
query += STMT_BOOL_AND
}
if i < len(terms)-1 && !all {
query += STMT_BOOL_OR
}
}
query += STMT_SEARCH_ORDER_BOOKS
log.Println("Search:", query)
stmt, err := app.DB.Prepare(query)
if err != nil {
return nil, 0, err
}
rows, err := stmt.Query(queryTerms...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
// FIXME factorize queryBooks
for rows.Next() {
if len(books) <= limit {
book := new(BookAdv)
var series_name sql.NullString
var series_id sql.NullInt64
if err := rows.Scan(&book.Id, &book.Title, &book.SeriesIndex, &series_name, &series_id); err != nil {
return nil, 0, err
}
if series_name.Valid && series_id.Valid {
book.Series = &Series{
series_id.Int64,
series_name.String,
}
}
books = append(books, book)
}
count++
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return books, count, nil
}
func (app *Bouquins) queryBooks(limit, offset int, sort, order string) ([]*BookAdv, bool, error) {
books := make([]*BookAdv, 0, limit)
stmt, err := app.psSortBooks(BOOKS, sort, order)
@ -254,19 +310,24 @@ func (app *Bouquins) BookFull(id int64) (*BookFull, error) {
return book, nil
}
func (app *Bouquins) BooksAdv(limit, offset int, sort, order string) ([]*BookAdv, bool, error) {
func (app *Bouquins) BooksAdv(params *ReqParams) ([]*BookAdv, int, bool, error) {
limit, offset, sort, order := params.Limit, params.Offset, params.Sort, params.Order
if len(params.Terms) > 0 {
books, count, err := app.searchBooks(limit, params.Terms, params.AllWords)
return books, count, count > limit, err
}
books, more, err := app.queryBooks(limit, offset, sort, order)
if err != nil {
return nil, false, err
return nil, 0, false, err
}
authors, err := app.queryBooksAuthors(limit, offset, sort, order)
if err != nil {
return nil, false, err
return nil, 0, false, err
}
tags, err := app.queryBooksTags(limit, offset, sort, order)
if err != nil {
return nil, false, err
return nil, 0, false, err
}
assignAuthorsTagsBooks(books, authors, tags)
return books, more, nil
return books, 0, more, nil
}

View File

@ -1,5 +1,7 @@
package bouquins
import "log"
// MERGE SUB QUERIES //
func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
@ -10,6 +12,49 @@ func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
// SUB QUERIES //
func (app *Bouquins) searchSeries(limit int, terms []string, all bool) ([]*SeriesAdv, int, error) {
series := make([]*SeriesAdv, 0, limit)
count := 0
query := STMT_SERIES_SEARCH
queryTerms := make([]interface{}, 0, len(terms))
for i, term := range terms {
queryTerms = append(queryTerms, "%"+term+"%")
query += STMT_SEARCH_TERM_SERIES
if i < len(terms)-1 && all {
query += STMT_BOOL_AND
}
if i < len(terms)-1 && !all {
query += STMT_BOOL_OR
}
}
query += STMT_SEARCH_ORDER_SERIES
log.Println("Search:", query)
stmt, err := app.DB.Prepare(query)
if err != nil {
return nil, 0, err
}
rows, err := stmt.Query(queryTerms...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
for rows.Next() {
if len(series) <= limit {
serie := new(SeriesAdv)
if err := rows.Scan(&serie.Id, &serie.Name); err != nil {
return nil, 0, err
}
series = append(series, serie)
}
count++
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return series, count, nil
}
func (app *Bouquins) querySeriesList(limit, offset int, sort, order string) ([]*SeriesAdv, bool, error) {
series := make([]*SeriesAdv, 0, limit)
stmt, err := app.psSortSeries(SERIES, sort, order)
@ -139,15 +184,20 @@ func (app *Bouquins) SeriesFull(id int64) (*SeriesFull, error) {
return series, nil
}
func (app *Bouquins) SeriesAdv(limit, offset int, sort, order string) ([]*SeriesAdv, bool, error) {
func (app *Bouquins) SeriesAdv(params *ReqParams) ([]*SeriesAdv, int, bool, error) {
limit, offset, sort, order := params.Limit, params.Offset, params.Sort, params.Order
if len(params.Terms) > 0 {
series, count, err := app.searchSeries(limit, params.Terms, params.AllWords)
return series, count, count > limit, err
}
series, more, err := app.querySeriesList(limit, offset, sort, order)
if err != nil {
return nil, false, err
return nil, 0, false, err
}
authors, err := app.querySeriesListAuthors(limit, offset, sort, order)
if err != nil {
return nil, false, err
return nil, 0, false, err
}
assignAuthorsSeries(series, authors)
return series, more, nil
return series, 0, more, nil
}

View File

@ -88,6 +88,7 @@ func router(app *Bouquins) {
http.HandleFunc(URL_BOOKS, app.BooksPage)
http.HandleFunc(URL_AUTHORS, app.AuthorsPage)
http.HandleFunc(URL_SERIES, app.SeriesPage)
http.HandleFunc(URL_SEARCH, app.SearchPage)
}
func main() {

38
templates/components.html Normal file
View File

@ -0,0 +1,38 @@
<script type="text/x-template" id="results-template">
<table class="table table-striped" v-if="results.length > 0">
<tbody>
<tr>
<th v-for="col in cols">
<template v-if="col.sort">
<a href="#" @click="sortBy(col.sort)">{{ "{{" }}col.name{{ "}}" }}</a>
<span v-if="sort_by == col.id" :class="['glyphicon', { 'glyphicon-chevron-up': order_desc , 'glyphicon-chevron-down': !order_desc}]"></span>
</template>
<template v-else>{{ "{{" }}col.name{{ "}}" }}</template>
</th>
</tr>
<tr v-for="item in results">
<td is="result-cell" :col="col" :item="item" v-for="col in cols"></td>
</tr>
</tbody>
</table>
</script>
<script type="text/x-template" id="paginate-template">
<nav aria-label="Pages" v-if="page > 0">
<ul class="pager">
<li class="previous" v-bind:class="{ disabled: page <= 1 }"><a href="#" @click="prevPage"><span aria-hidden="true">&larr;</span> Précédents</a></li>
<li class="next" v-bind:class="{ disabled: !more }"><a href="#" @click="nextPage">Suivants <span aria-hidden="true">&rarr;</span></a></li>
</ul>
</nav>
</script>
<script type="text/x-template" id="results-list-template">
<div v-if="count > 0">
<h2>{{ "{{ count }} {{ countlabel() }}" }}</h2>
<ul>
<li v-for="item in results" class="list-unstyled">
<span :class="iconClass()"></span>
<a :href="url(item)">{{ "{{ label(item) }}" }}</a>
</li>
<li v-if="results.length < count" class="list-unstyled">...</li>
</ul>
</div>
</script>

View File

@ -17,7 +17,7 @@
<div class="container">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Accueil</a></li>
<li><a href="search.html">Recherche</a></li>
<li><a href="/search">Recherche</a></li>
<li><a href="#">A propos</a></li>
</ul>
<form class="navbar-form navbar-right" role="search" method="get" action="search.html">

View File

@ -13,30 +13,5 @@
<paginate :page="page" :more="more"></paginate>
</div>
</div>
<script type="text/x-template" id="results-template">
<table class="table table-striped" v-if="results.length > 0">
<tbody>
<tr>
<th v-for="col in cols">
<template v-if="col.sort">
<a href="#" @click="sortBy(col.sort)">{{ "{{" }}col.name{{ "}}" }}</a>
<span v-if="sort_by == col.id" :class="['glyphicon', { 'glyphicon-chevron-up': order_desc , 'glyphicon-chevron-down': !order_desc}]"></span>
</template>
<template v-else>{{ "{{" }}col.name{{ "}}" }}</template>
</th>
</tr>
<tr v-for="item in results">
<td is="result-cell" :col="col" :item="item" v-for="col in cols"></td>
</tr>
</tbody>
</table>
</script>
<script type="text/x-template" id="paginate-template">
<nav aria-label="Pages" v-if="page > 0">
<ul class="pager">
<li class="previous" v-bind:class="{ disabled: page <= 1 }"><a href="#" @click="prevPage"><span aria-hidden="true">&larr;</span> Précédents</a></li>
<li class="next" v-bind:class="{ disabled: !more }"><a href="#" @click="nextPage">Suivants <span aria-hidden="true">&rarr;</span></a></li>
</ul>
</nav>
</script>
{{ template "components.html" }}
{{ template "footer.html" . }}

61
templates/search.html Normal file
View File

@ -0,0 +1,61 @@
{{ template "header.html" . }}
<div class="container" id="search">
<div class="panel panel-primary">
<div class="panel-heading">
<h3>Recherche</h3>
</div>
<div class="panel-body">
<form id="searchForm" @submit="searchFull" v-on:submit.prevent>
<div class="form-group">
<input type="text" class="form-control" placeholder="Recherche" v-model="q">
</div>
<div class="form-group">
<label>Parmi</label><br/>
<label class="radio-inline">
<input type="radio" value="books" v-model="which"> livres
</label>
<label class="radio-inline">
<input type="radio" value="authors" v-model="which"> auteurs
</label>
<label class="radio-inline">
<input type="radio" value="series" v-model="which"> series
</label>
<label class="radio-inline">
<input type="radio" value="all" v-model="which"> tous
</label>
</div>
<div class="form-group">
<label>Nombre de resultats</label><br/>
<label class="radio-inline">
<input type="radio" value="10" v-model="perpage"> 10
</label>
<label class="radio-inline">
<input type="radio" value="20" v-model="perpage"> 20
</label>
<label class="radio-inline">
<input type="radio" value="50" v-model="perpage"> 50
</label>
<label class="radio-inline">
<input type="radio" value="100" v-model="perpage"> 100
</label>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" v-model="all" disabled> Tous les mots
</label>
<p class="help-block">Cocher pour rechercher les élements contenant tous les mots saisis</p>
</div>
</div>
<button type="submit" class="btn btn-primary">Rechercher</button>
</form>
</div>
</div>
<div class="table-responsive">
<results-list type="books" :count="booksCount" :results="books"></results-list>
<results-list type="authors" :count="authorsCount" :results="authors"></results-list>
<results-list type="series" :count="seriesCount" :results="series"></results-list>
</div>
</div>
{{ template "components.html" }}
{{ template "footer.html" . }}