feat: refactor post browse api

This commit is contained in:
Damillora 2021-05-11 03:25:33 +07:00
parent 79fe33496b
commit a09058ceb2
15 changed files with 3979 additions and 3017 deletions

View File

@ -3,7 +3,9 @@ package app
import (
"net/http"
"strconv"
"strings"
"github.com/Damillora/Shioriko/pkg/database"
"github.com/Damillora/Shioriko/pkg/middleware"
"github.com/Damillora/Shioriko/pkg/models"
"github.com/Damillora/Shioriko/pkg/services"
@ -16,7 +18,6 @@ func InitializePostRoutes(g *gin.Engine) {
{
unprotected.GET("/", postGet)
unprotected.GET("/:id", postGetOne)
unprotected.GET("/tag/:id", postGetTag)
}
protected := g.Group("/api/post").Use(middleware.AuthMiddleware())
{
@ -30,7 +31,22 @@ func InitializePostRoutes(g *gin.Engine) {
func postGet(c *gin.Context) {
pageParam := c.Query("page")
page, _ := strconv.Atoi(pageParam)
posts := services.GetPostAll(page)
tag := c.Query("tags")
tags := strings.Split(tag, " ")
var posts []database.Post
var postPages int
if tag != "" {
posts = services.GetPostTags(page, tags)
postPages = services.CountPostPagesTag(tags)
} else {
posts = services.GetPostAll(page)
postPages = services.CountPostPages()
}
var postResult []models.PostListItem
for _, post := range posts {
var tagStrings []string
@ -44,35 +60,6 @@ func postGet(c *gin.Context) {
Tags: tagStrings,
})
}
postPages := services.CountPostPages()
c.JSON(http.StatusOK, models.PostPaginationResponse{
CurrentPage: page,
TotalPage: postPages,
Posts: postResult,
})
}
func postGetTag(c *gin.Context) {
pageParam := c.Query("page")
page, _ := strconv.Atoi(pageParam)
tag := c.Param("id")
posts := services.GetPostTag(page, tag)
var postResult []models.PostListItem
for _, post := range posts {
var tagStrings []string
for _, tag := range post.Tags {
tagStrings = append(tagStrings, tag.TagType.Name+":"+tag.Name)
}
postResult = append(postResult, models.PostListItem{
ID: post.ID,
ImagePath: "/data/" + post.Blob.FilePath,
Tags: tagStrings,
})
}
postPages := services.CountPostPagesTag(tag)
c.JSON(http.StatusOK, models.PostPaginationResponse{
CurrentPage: page,
TotalPage: postPages,

View File

@ -1,6 +1,9 @@
package services
import (
"log"
"math"
"github.com/Damillora/Shioriko/pkg/database"
"github.com/Damillora/Shioriko/pkg/models"
"github.com/google/uuid"
@ -10,17 +13,18 @@ const perPage = 20
func GetPostAll(page int) []database.Post {
var posts []database.Post
database.DB.Joins("Blob").Preload("Tags").Preload("Tags.TagType").Offset((page - 1) * perPage).Limit(20).Find(&posts)
database.DB.Joins("Blob").Preload("Tags").Preload("Tags.TagType").Order("created_at desc").Offset((page - 1) * perPage).Limit(20).Find(&posts)
return posts
}
func GetPostTag(page int, tagSyntax string) []database.Post {
tag, err := GetTag(tagSyntax)
func GetPostTags(page int, tagSyntax []string) []database.Post {
tags, err := ParseReadTags(tagSyntax)
log.Println(tags)
if err != nil {
return []database.Post{}
}
var posts []database.Post
database.DB.Model(&tag).Joins("Blob").Preload("Tags").Preload("Tags.TagType").Offset((page - 1) * perPage).Limit(20).Association("Posts").Find(&posts)
database.DB.Model(&tags).Distinct().Joins("Blob").Preload("Tags").Preload("Tags.TagType").Order("created_at desc").Offset((page - 1) * perPage).Limit(20).Association("Posts").Find(&posts)
return posts
}
@ -59,34 +63,38 @@ func UpdatePost(id string, model models.PostUpdateModel) (*database.Post, error)
}
var post database.Post
result := database.DB.Where("id = ?", id).First(&post)
result := database.DB.Preload("Tags").Where("id = ?", id).First(&post)
if result.Error != nil {
return nil, result.Error
}
database.DB.Model(&post).Association("Tags").Replace(tags)
post.SourceURL = model.SourceURL
post.Tags = tags
result = database.DB.Save(&post)
if result.Error != nil {
return nil, result.Error
}
return &post, nil
}
func CountPostPages() int {
var count int64
database.DB.Model(&database.Post{}).Count(&count)
return int(count/perPage) + 1
return int(math.Abs(float64(count-1))/perPage) + 1
}
func CountPostPagesTag(tagSyntax string) int {
tag, err := GetTag(tagSyntax)
func CountPostPagesTag(tagSyntax []string) int {
tags, err := ParseReadTags(tagSyntax)
if err != nil {
return 0
}
var count int64
count = database.DB.Model(&tag).Joins("Blob").Preload("Tags").Preload("Tags.TagType").Association("Posts").Count()
return int(count/perPage) + 1
count = database.DB.Model(&tags).Distinct().Joins("Blob").Preload("Tags").Preload("Tags.TagType").Association("Posts").Count()
return int(math.Abs(float64(count-1))/perPage) + 1
}
func DeletePost(id string) error {

View File

@ -14,27 +14,15 @@ func GetTagAll() []database.Tag {
return tags
}
func CreateOrUpdateTagGeneric(tagName string) (*database.Tag, error) {
func FindTagGeneric(tagName string) (*database.Tag, error) {
var tag database.Tag
result := database.DB.Where("name = ?", tagName).First(&tag)
if result.Error != nil {
var tagType database.TagType
database.DB.Where("name = ?", "general").First(&tagType)
tag = database.Tag{
ID: uuid.NewString(),
Name: tagName,
TagTypeID: tagType.ID,
}
result = database.DB.Create(&tag)
if result.Error != nil {
return nil, result.Error
}
return nil, result.Error
}
return &tag, nil
}
func CreateOrUpdateTagComplex(tagName string, tagTypeString string) (*database.Tag, error) {
func FindTagComplex(tagName string, tagTypeString string) (*database.Tag, error) {
var tag database.Tag
var tagType database.TagType
result := database.DB.Where("name = ?", tagTypeString).First(&tagType)
@ -45,7 +33,40 @@ func CreateOrUpdateTagComplex(tagName string, tagTypeString string) (*database.T
result = database.DB.Where("name = ? AND tag_type_id = ? ", tagName, tagType.ID).First(&tag)
if result.Error != nil {
tag = database.Tag{
return nil, result.Error
}
return &tag, nil
}
func CreateOrUpdateTagGeneric(tagName string) (*database.Tag, error) {
tag, err := FindTagGeneric(tagName)
if err != nil {
var tagType database.TagType
database.DB.Where("name = ?", "general").First(&tagType)
tag = &database.Tag{
ID: uuid.NewString(),
Name: tagName,
TagTypeID: tagType.ID,
}
result := database.DB.Create(&tag)
if result.Error != nil {
return nil, result.Error
}
}
return tag, nil
}
func CreateOrUpdateTagComplex(tagName string, tagTypeString string) (*database.Tag, error) {
tag, err := FindTagComplex(tagName, tagTypeString)
if err != nil {
var tagType database.TagType
result := database.DB.Where("name = ?", tagTypeString).First(&tagType)
if result.Error != nil {
return nil, result.Error
}
tag = &database.Tag{
ID: uuid.NewString(),
Name: tagName,
TagTypeID: tagType.ID,
@ -56,7 +77,7 @@ func CreateOrUpdateTagComplex(tagName string, tagTypeString string) (*database.T
}
}
return &tag, nil
return tag, nil
}
func CreateOrUpdateTag(tagSyntax string) (*database.Tag, error) {
tagFields := strings.Split(tagSyntax, ":")
@ -73,30 +94,20 @@ func CreateOrUpdateTag(tagSyntax string) (*database.Tag, error) {
return nil, errors.New("Malformed tag syntax")
}
}
func GetTag(tagSyntax string) (*database.Tag, error) {
func FindTag(tagSyntax string) (*database.Tag, error) {
tagFields := strings.Split(tagSyntax, ":")
var tagName string
var tagType database.TagType
var tagType string
if len(tagFields) == 1 {
tagName = tagFields[0]
database.DB.Where("name = ?", "general").First(&tagType)
return FindTagGeneric(tagName)
} else if len(tagFields) == 2 {
tagType = tagFields[0]
tagName = tagFields[1]
result := database.DB.Where("name = ?", tagFields[0]).First(&tagType)
if result.Error != nil {
return nil, result.Error
}
return FindTagComplex(tagName, tagType)
} else {
return nil, errors.New("Malformed tag syntax")
}
var tag database.Tag
result := database.DB.Preload("Posts").Where("name = ? AND tag_type_id = ? ", tagName, tagType.ID).First(&tag)
if result.Error != nil {
return nil, result.Error
}
return &tag, nil
}
func ParseTags(tags []string) ([]database.Tag, error) {
@ -110,3 +121,15 @@ func ParseTags(tags []string) ([]database.Tag, error) {
}
return result, nil
}
func ParseReadTags(tags []string) ([]database.Tag, error) {
var result []database.Tag
for _, tagSyntax := range tags {
tag, err := FindTag(tagSyntax)
if err != nil {
return nil, err
}
result = append(result, *tag)
}
return result, nil
}

View File

@ -8,8 +8,8 @@
import Post from "./routes/Post.svelte";
import Login from "./routes/Login.svelte";
import Logout from "./routes/Logout.svelte";
import Tag from "./routes/Tag.svelte";
import Upload from "./routes/Upload.svelte";
import Edit from "./routes/Edit.svelte";
export let url = "";
let baseURL = window.BASE_URL;
@ -20,8 +20,8 @@
<div>
<Route path="/" component={Home} />
<Route path="/posts" component={Posts} />
<Route path="/tag/:id" component={Tag} />
<Route path="/post/:id" component={Post} />
<Route path="/post/edit/:id" component={Edit} />
<Route path="/auth/login" component={Login} />
<Route path="/auth/logout" component={Logout} />
<Route path="/upload" component={Upload} />

View File

@ -1,88 +1,101 @@
<script>
import { Link } from "svelte-routing";
import TagLink from "./TagLink.svelte";
export let posts = [];
let postChunks = [];
// split posts into 4 columns
$: postChunks = Array(Math.min(posts.length, 4))
.fill()
.map(function (_, i) {
let chunkSize = Math.floor(posts.length / 4);
if (chunkSize % 4 > i + 1) {
chunkSize += 1;
}
chunkSize = Math.max(chunkSize, 1);
return posts.slice(i * chunkSize, i * chunkSize + chunkSize);
});
export let page = 1;
export let totalPages = 1;
export let handlePage = (i) => { };
export let handlePage = (i) => {};
export let url = "/posts";
</script>
<section class="section">
<div class="container">
<nav class="pagination" role="navigation" aria-label="pagination">
{#if page > 1}
<nav class="pagination" role="navigation" aria-label="pagination">
{#if page > 1}
<Link
on:click={handlePage(page - 1)}
to="{url}page={page - 1}"
class="pagination-previous"
aria-label="Previous">Previous</Link
>
{/if}
{#if page < totalPages}
<Link
on:click={handlePage(page + 1)}
to="{url}page={page + 1}"
class="pagination-next"
aria-label="Next">Next</Link
>
{/if}
<ul class="pagination-list">
{#if page > 3}
<li>
<Link
on:click={handlePage(page - 1)}
to="{url}?page={page - 1}"
class="pagination-previous"
aria-label="Previous">Previous</Link
on:click={handlePage(1)}
to="{url}page={1}"
class="pagination-link"
aria-label="Goto page 1">1</Link
>
{/if}
{#if page < totalPages}
<Link
on:click={handlePage(page + 1)}
to="{url}?page={page + 1}"
class="pagination-next"
aria-label="Next">Next</Link
>
{/if}
<ul class="pagination-list">
{#if page > 3}
</li>
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{/if}
{#each [...Array(5).keys()].map((x) => x + page - 2) as i}
{#if i >= 1 && i <= totalPages}
{#if i == page}
<li>
<Link
on:click={handlePage(1)}
to="{url}?page={1}"
class="pagination-link"
aria-label="Goto page 1">1</Link
on:click={handlePage(i)}
to="{url}page={i}"
class="pagination-link is-current"
aria-label="Goto page {i}">{i}</Link
>
</li>
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{/if}
{#each [...Array(5).keys()].map((x) => x + page - 2) as i}
{#if i >= 1 && i <= totalPages}
{#if i == page}
<li>
<Link
on:click={handlePage(i)}
to="{url}?page={i}"
class="pagination-link is-current"
aria-label="Goto page {i}">{i}</Link
>
</li>
{:else}
<li>
<Link
on:click={handlePage(i)}
to="{url}?page={i}"
class="pagination-link"
aria-label="Goto page {i}">{i}</Link
>
</li>
{/if}
{/if}
{/each}
{#if totalPages - page > 2}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
{:else}
<li>
<Link
on:click={handlePage(totalPages)}
to="{url}?page={totalPages}"
on:click={handlePage(i)}
to="{url}page={i}"
class="pagination-link"
aria-label="Goto page {totalPages}"
>{totalPages}</Link
aria-label="Goto page {i}">{i}</Link
>
</li>
{/if}
</ul>
</nav>
<div class="columns is-multiline">
{#each posts as post (post.id)}
<div class="column is-one-quarter card">
{/if}
{/each}
{#if totalPages - page > 2}
<li>
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li>
<Link
on:click={handlePage(totalPages)}
to="{url}page={totalPages}"
class="pagination-link"
aria-label="Goto page {totalPages}">{totalPages}</Link
>
</li>
{/if}
</ul>
</nav>
<div class="tile is-multiline is-ancestor">
{#each postChunks as postChunk}
<div class="tile is-parent is-vertical is-3">
{#each postChunk as post, i (post.id)}
<div class="tile is-child is-vertical card">
<div class="card-image">
<figure class="image">
<Link to="/post/{post.id}">
@ -91,16 +104,16 @@
</figure>
</div>
<div class="card-content">
<div class="content">
{#if post.tags}
{#each post.tags as tag (tag)}
<p>
<Link to="/tag/{tag}">{tag}</Link>
</p>
<TagLink {tag} />
{/each}
</div>
{:else}
None
{/if}
</div>
</div>
{/each}
</div>
</div>
</section>
{/each}
</div>

View File

@ -0,0 +1,10 @@
<script>
import { Link } from "svelte-routing";
export let tag;
let tagType = tag.split(":")[0] ?? "";
let tagName = tag.split(":")[1] ?? "";
</script>
<Link class="button is-rounded is-primary is-small m-1" to="/posts?tags={tagName}">{tagName}</Link>

View File

@ -27,10 +27,16 @@ export async function getPosts({ page }) {
return response.data;
}
export async function getPostsTag({ page, tag }) {
const endpoint = url + "/api/post/tag/" + tag + "?page=" + page;
const response = await axios(endpoint);
return response.data;
export async function getPostSearchTag({ page, q }) {
if (q) {
const endpoint = url + "/api/post?tags=" + q + "&page=" + page;
const response = await axios(endpoint);
return response.data;
} else {
const endpoint = url + "/api/post?page=" + page;
const response = await axios(endpoint);
return response.data;
}
}
export async function getPost({ id }) {
@ -75,4 +81,20 @@ export async function postCreate({ blob_id, source_url, tags }) {
}
})
return response.data;
}
export async function postUpdate(id, { source_url, tags }) {
const endpoint = url + "/api/post/"+id;
const response = await axios({
url: endpoint,
method: "POST",
headers: {
'Authorization': 'Bearer ' + current_token,
},
withCredentials: true,
data: {
source_url, tags
}
})
return response.data;
}

View File

@ -1,9 +1,13 @@
@import '../node_modules/bulma/sass/utilities/_all';
@import '../node_modules/bulma/sass/base/_all';
@import '../node_modules/bulma/sass/elements/_all';
@import '../node_modules/bulma/sass/form/_all';
@import '../node_modules/bulma/sass/components/_all';
@import '../node_modules/bulma/sass/grid/_all';
@import '../node_modules/bulma/sass/helpers/_all';
@import '../node_modules/bulma/sass/layout/_all';
@import "../node_modules/bulma/sass/utilities/_all";
@import "../node_modules/bulma/sass/base/_all";
@import "../node_modules/bulma/sass/elements/_all";
@import "../node_modules/bulma/sass/form/_all";
@import "../node_modules/bulma/sass/components/_all";
@import "../node_modules/bulma/sass/grid/_all";
@import "../node_modules/bulma/sass/helpers/_all";
@import "../node_modules/bulma/sass/layout/_all";
.tile.is-multiline {
flex-wrap: wrap;
}

View File

@ -0,0 +1,90 @@
<script>
import { getPost, postUpdate } from "../api.js";
import { navigate } from "svelte-routing";
import Tags from "svelte-tags-input";
import { onMount } from "svelte";
import { Link } from "svelte-routing";
export let id;
let image_path = "";
let form = {
source_url: "",
tags: [],
};
const getData = async () => {
const data = await getPost({ id });
form.source_url = data.source_url;
form.tags = data.tags;
image_path = data.image_path;
};
const onTagChange = (value) => {
form.tags = value.detail.tags;
};
const onSubmit = async () => {
const response = await postUpdate(id, form);
navigate(`/post/${response.id}`);
};
onMount(() => {
getData();
});
</script>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">Edit Post: {id}</p>
</div>
</section>
<div class="container">
<section class="section">
<div class="columns">
<div class="column is-one-third box">
<p>
<Link class="button is-primary" to="/post/{id}"
>Back</Link
>
</p>
<form on:submit|preventDefault={onSubmit}>
<div class="field">
<label for="source" class="label">Source URL</label>
<div class="control">
<input
id="source"
class="input"
type="url"
placeholder="Source URL"
bind:value={form.source_url}
/>
</div>
</div>
<div class="field">
<label for="tags" class="label">Tags</label>
<div class="control" id="tags">
<Tags
tags={form.tags}
addKeys={[9,32]}
on:tags={onTagChange}
/>
</div>
</div>
<div class="control">
<button type="submit" class="button is-primary"
>Submit</button
>
</div>
</form>
</div>
<div class="column">
<figure class="image">
<img alt={id} src={image_path} />
</figure>
</div>
</div>
</section>
</div>

View File

@ -1,59 +1,73 @@
<script>
import { onMount } from "svelte";
import TagLink from "../TagLink.svelte";
import { getPost, postCreate } from "../api.js";
import { Link } from "svelte-routing";
import {getPost } from "../api.js";
export let id;
let post;
const getData = async() => {
const data = await getPost({id});
const getData = async () => {
const data = await getPost({ id });
post = data;
}
};
const trimUrl = (str) => {
if(str.length > 30) {
return str.substring(0,30) + "...";
if (str.length > 30) {
return str.substring(0, 30) + "...";
}
return str;
}
};
onMount(() => {getData()});
onMount(() => {
getData();
});
</script>
<section class="hero is-primary">
<div class="hero-body">
{#if post}
<p class="title">
Post ID: {post.id}
</p>
{/if}
<p class="title">
Post ID: {post.id}
</p>
{/if}
</div>
</section>
{#if post}
<div class="container">
<section class="section">
<div class="columns">
<div class="column is-one-third box">
<p>
Source URL: <a href="{post.source_url}">{trimUrl(post.source_url)}</a>
</p>
<p>
Tags:
{#each post.tags as tag (tag)}
<ul>
<li>
<Link to="/tag/{tag}">{tag}</Link>
</li>
</ul>
{/each}
</p>
<div class="container">
<section class="section">
<div class="columns">
<div class="column is-one-third box">
<div class="content">
<p>
<Link
class="button is-primary"
to="/post/edit/{post.id}">Edit</Link
>
</p>
<p>
Source URL: <a href={post.source_url}
>{trimUrl(post.source_url)}</a
>
</p>
<p>
Tags:<br />
</p>
<p>
{#if post.tags}
{#each post.tags as tag (tag)}
<TagLink {tag} />
{/each}
{:else}
None
{/if}
</p>
</div>
</div>
<div class="column">
<figure class="image">
<img alt={post.id} src={post.image_path} />
</figure>
</div>
</div>
<div class="column">
<figure class="image">
<img alt="{post.id}" src="{post.image_path}">
</figure>
</div>
</div>
</section>
</div>
{/if}
</section>
</div>
{/if}

View File

@ -1,30 +1,45 @@
<script>
import { onMount } from "svelte";
import { getPosts } from "../api.js";
import { Link } from "svelte-routing";
import queryString from "query-string";
import { getPostSearchTag } from "../api.js";
import { navigate } from "svelte-routing";
import PostPaginator from "../PostPaginator.svelte";
import queryString from "query-string";
import Tags from "svelte-tags-input";
export let location;
let searchTerms = [];
let page = 1;
let totalPages = 1;
let posts = [];
const getData = async () => {
const data = await getPosts({ page });
const data = await getPostSearchTag({ page, q: searchTerms.join("+") });
if (Array.isArray(data.posts)) {
posts = data.posts;
totalPages = data.totalPage;
}
};
onMount(() => {
let queryParams;
let queryParams;
const onTagChange = (value) => {
searchTerms = value.detail.tags;
};
$: {
queryParams = queryString.parse(location.search);
console.log(queryParams);
if (queryParams.page) {
page = parseInt(queryParams.page);
}
if (queryParams.tags) {
searchTerms = queryParams.tags.split(" ");
} else {
searchTerms = [];
}
getData();
});
}
const handlePage = (i) => {
return () => {
@ -32,6 +47,14 @@
getData();
};
};
const onSearch = (i) => {
if (searchTerms.length > 0) {
navigate(`/posts?tags=${searchTerms.join("+")}`);
} else {
navigate(`/posts`);
}
};
</script>
<section class="hero is-primary">
@ -40,4 +63,36 @@
</div>
</section>
<PostPaginator url="/posts" posts={posts} page={page} totalPages={totalPages} handlePage={handlePage} />
<section class="section">
<div class="container">
<div class="block">
<form on:submit|preventDefault={onSearch}>
<div class="field has-addons">
<div class="control is-expanded">
<div class="control" id="tags">
<Tags
tags={searchTerms}
addKeys={[9,32]}
on:tags={onTagChange}
/>
</div>
</div>
<div class="control">
<button type="submit" class="button is-primary">
Search
</button>
</div>
</div>
</form>
</div>
<div class="block">
<PostPaginator
url="/posts?tags={searchTerms.join('+')}&"
{posts}
{page}
{totalPages}
{handlePage}
/>
</div>
</div>
</section>

View File

@ -1,47 +0,0 @@
<script>
import { onMount } from "svelte";
import { getPostsTag } from "../api.js";
import PostPaginator from "../PostPaginator.svelte";
import queryString from "query-string";
export let location;
export let id;
let page = 1;
let totalPages = 1;
let posts = [];
const getData = async () => {
const data = await getPostsTag({ page, tag: id });
if (Array.isArray(data.posts)) {
posts = data.posts;
totalPages = data.totalPage;
}
};
onMount(() => {
let queryParams;
queryParams = queryString.parse(location.search);
if (queryParams.page) {
page = parseInt(queryParams.page);
}
getData();
});
const handlePage = (i) => {
return () => {
page = i;
getData();
};
};
</script>
<section class="hero is-primary">
<div class="hero-body">
<p class="title">
{id}
</p>
<p class="subtitle">Tag</p>
</div>
</section>
<PostPaginator url="/tag/{id}" posts={posts} page={page} totalPages={totalPages} handlePage={handlePage} />

View File

@ -88,7 +88,7 @@
<div class="field">
<label for="tags" class="label">Tags</label>
<div class="control" id="tags">
<Tags on:tags={onTagChange} />
<Tags addKeys={[9,32]} on:tags={onTagChange} />
</div>
</div>
<div class="control">

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long