feat: revamp with tag editor

This commit is contained in:
Damillora 2022-04-16 02:20:14 +07:00
parent 31763528bf
commit 19e7aea06d
26 changed files with 585 additions and 127 deletions

View File

@ -152,6 +152,15 @@ func postCreate(c *gin.Context) {
}
func postUpdate(c *gin.Context) {
_, ok := c.Get("user")
if !ok {
c.JSON(http.StatusForbidden, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: "User don't exist",
})
c.Abort()
}
id := c.Param("id")
var model models.PostUpdateModel
@ -194,6 +203,15 @@ func postUpdate(c *gin.Context) {
}
func postDelete(c *gin.Context) {
_, ok := c.Get("user")
if !ok {
c.JSON(http.StatusForbidden, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: "User don't exist",
})
c.Abort()
}
id := c.Param("id")
err := services.DeletePost(id)

View File

@ -3,6 +3,8 @@ package app
import (
"net/http"
"github.com/Damillora/Shioriko/pkg/middleware"
"github.com/Damillora/Shioriko/pkg/models"
"github.com/Damillora/Shioriko/pkg/services"
"github.com/gin-gonic/gin"
)
@ -11,8 +13,14 @@ func InitializeTagRoutes(g *gin.Engine) {
unprotected := g.Group("/api/tag")
{
unprotected.GET("/", tagGet)
unprotected.GET("/:tag", tagGetOne)
unprotected.GET("/autocomplete", tagAutocomplete)
}
protected := g.Group("/api/tag").Use(middleware.AuthMiddleware())
{
protected.PUT("/:tag/note", tagUpdateNote)
protected.PUT("/:tag", tagUpdate)
}
}
func tagGet(c *gin.Context) {
@ -20,7 +28,94 @@ func tagGet(c *gin.Context) {
c.JSON(http.StatusOK, tags)
}
func tagGetOne(c *gin.Context) {
tag := c.Param("tag")
tagObj, err := services.GetTag(tag)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
}
c.JSON(http.StatusOK, models.TagReadModel{
TagID: tagObj.TagID,
TagName: tagObj.TagName,
TagType: tagObj.TagType,
TagNote: tagObj.TagNote,
PostCount: tagObj.PostCount,
})
}
func tagAutocomplete(c *gin.Context) {
tags := services.GetTagAutocomplete()
c.JSON(http.StatusOK, tags)
}
func tagUpdateNote(c *gin.Context) {
_, ok := c.Get("user")
if !ok {
c.JSON(http.StatusForbidden, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: "User don't exist",
})
c.Abort()
}
var model models.TagNoteUpdateModel
err := c.ShouldBindJSON(&model)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
c.Abort()
return
}
tag := c.Param("tag")
err = services.UpdateTagNotes(tag, model.Note)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
c.Abort()
}
c.JSON(http.StatusOK, nil)
}
func tagUpdate(c *gin.Context) {
_, ok := c.Get("user")
if !ok {
c.JSON(http.StatusForbidden, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: "User don't exist",
})
c.Abort()
}
var model models.TagUpdateModel
err := c.ShouldBindJSON(&model)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
c.Abort()
return
}
tag := c.Param("tag")
err = services.UpdateTag(tag, model)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
c.Abort()
}
c.JSON(http.StatusOK, nil)
}

View File

@ -12,10 +12,12 @@ import (
)
func InitializeTagTypeRoutes(g *gin.Engine) {
unprotected := g.Group(("/api/tagtype"))
{
unprotected.GET("/", tagTypeGet)
}
protected := g.Group("/api/tagtype").Use(middleware.AuthMiddleware())
{
protected.GET("/", tagTypeGet)
protected.POST("/create", tagTypeCreate)
protected.DELETE("/:id", tagTypeDelete)
}

View File

@ -11,5 +11,6 @@ type Tag struct {
UpdatedAt time.Time
TagTypeID uint
TagType TagType
Note string `gorm:"type:text"`
Posts []Post `gorm:"many2many:post_tags"`
}

View File

@ -7,9 +7,10 @@ type UserCreateModel struct {
}
type UserUpdateModel struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required"`
Password string `json:"password"`
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required"`
OldPassword string `json:"oldPassword"`
NewPassword string `json:"newPassword"`
}
type TagTypeCreateModel struct {
@ -26,6 +27,9 @@ type TagUpdateModel struct {
TagTypeID uint `json:"tagTypeId" validate:"required"`
}
type TagNoteUpdateModel struct {
Note string `json:"note" validate:"required"`
}
type PostCreateModel struct {
BlobID string `json:"blob_id" validate:"required"`
SourceURL string `json:"source_url"`

View File

@ -10,3 +10,11 @@ type PostReadModel struct {
Height int `json:"height"`
Uploader string `json:"uploader"`
}
type TagReadModel struct {
TagID string `json:"tagId"`
TagName string `json:"tagName"`
TagType string `json:"tagType"`
TagNote string `json:"tagNote"`
PostCount int `json:"postCount"`
}

View File

@ -42,6 +42,24 @@ func GetTagFilter(tagObjs []database.Tag) []models.TagListItem {
Find(&tags, tagIds)
return tags
}
func GetTag(tagString string) (*models.TagReadModel, error) {
tagObj, err := FindTag(tagString)
if err != nil {
return nil, err
}
var tagModel models.TagReadModel
database.DB.Model(&tagObj).
Joins("join tag_types on tag_types.id = tags.tag_type_id").
Joins("left join post_tags on post_tags.tag_id = tags.id").
Select("tags.id as tag_id, tags.name as tag_name, tag_types.name as tag_type, tags.note as tag_note, count(post_tags.post_id) as post_count").
Group("tags.id, tags.name, tag_types.name, tags.note").
First(&tagModel, "tags.id = ? ", tagObj.ID)
return &tagModel, nil
}
func GetTagAutocomplete() []string {
var tags []string
result := database.DB.Model(&database.Tag{}).
@ -134,6 +152,7 @@ func CreateOrUpdateTag(tagSyntax string) (*database.Tag, error) {
return nil, errors.New("Malformed tag syntax")
}
}
func FindTag(tagSyntax string) (*database.Tag, error) {
tagFields := strings.Split(tagSyntax, ":")
var tagName string
@ -173,3 +192,36 @@ func ParseReadTags(tags []string) ([]database.Tag, error) {
}
return result, nil
}
func UpdateTagNotes(tagString string, notes string) error {
tagObj, err := FindTag(tagString)
if err != nil {
return err
}
tagObj.Note = notes
result := database.DB.Save(&tagObj)
if result.Error != nil {
return result.Error
}
return nil
}
func UpdateTag(tagString string, model models.TagUpdateModel) error {
tagObj, err := FindTag(tagString)
if err != nil {
return err
}
tagObj.TagTypeID = model.TagTypeID
tagObj.Name = model.Name
result := database.DB.Save(&tagObj)
if result.Error != nil {
return result.Error
}
return nil
}

View File

@ -50,7 +50,12 @@ func UpdateUser(id string, model models.UserUpdateModel) (*database.User, error)
user.Username = model.Username
if user.Password != "" {
passwd, err := bcrypt.GenerateFromPassword([]byte(model.Password), bcrypt.DefaultCost)
verifyErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(model.OldPassword))
if verifyErr != nil {
return nil, verifyErr
}
passwd, err := bcrypt.GenerateFromPassword([]byte(model.NewPassword), bcrypt.DefaultCost)
if err != nil {
return nil, err
}

View File

@ -3,15 +3,21 @@
import Navbar from "./Navbar.svelte";
import Home from "./routes/Home.svelte";
import Posts from "./routes/Posts.svelte";
import Post from "./routes/Post.svelte";
import Login from "./routes/Login.svelte";
import Logout from "./routes/Logout.svelte";
import Upload from "./routes/Upload.svelte";
import Edit from "./routes/Edit.svelte";
import Tags from "./routes/Tags.svelte";
import Register from "./routes/Register.svelte";
import Login from "./routes/Auth/Login.svelte";
import Logout from "./routes/Auth/Logout.svelte";
import Register from "./routes/Auth/Register.svelte";
import Posts from "./routes/Post/Posts.svelte";
import Post from "./routes/Post/Post.svelte";
import Upload from "./routes/Post/Upload.svelte";
import Tags from "./routes/Tags/Tags.svelte";
import Tag from "./routes/Tags/Tag.svelte";
import Profile from "./routes/User/Profile.svelte";
export let url = "";
let baseURL = window.BASE_URL;
@ -23,12 +29,13 @@
<Route path="/" component={Home} />
<Route path="/posts" component={Posts} />
<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} />
<Route path="/tags" component={Tags} />
<Route path="/tags/:tag" component={Tag} />
<Route path="/auth/register" component={Register} />
<Route path="/user/profile" component={Profile} />
</div>
</Router>

View File

@ -46,6 +46,9 @@
{#if loggedIn}
<div class="navbar-item">
<div class="buttons">
<Link to="/user/profile" class="button is-primary">
Profile
</Link>
<Link to="/auth/logout" class="button is-light">
Log out
</Link>

View File

@ -41,6 +41,13 @@ export async function getTags() {
const response = await axios.get(endpoint);
return response.data;
}
export async function getTag({ tag }) {
const endpoint = url + "/api/tag/" + tag;
const response = await axios.get(endpoint);
return response.data;
}
export async function getTagAutocomplete() {
const endpoint = url + "/api/tag/autocomplete";
const response = await axios.get(endpoint);
@ -123,6 +130,7 @@ export async function postUpdate(id, { source_url, tags }) {
})
return response.data;
}
export async function postDelete({ id }) {
const endpoint = url + "/api/post/" + id;
const response = await axios({
@ -134,4 +142,57 @@ export async function postDelete({ id }) {
withCredentials: true,
})
return response.status == 200;
}
export async function getUserProfile() {
const endpoint = url + "/api/user/profile";
const response = await axios({
url: endpoint,
method: "GET",
headers: {
'Authorization': 'Bearer ' + current_token,
},
withCredentials: true,
});
console.log(response.data);
return response.data;
}
export async function updateTagNotes(id, { note }) {
const endpoint = url + "/api/tag/" + id + "/note";
const response = await axios({
url: endpoint,
method: "PUT",
headers: {
'Authorization': 'Bearer ' + current_token,
},
withCredentials: true,
data: {
note
}
})
return response.data;
}
export async function updateTag(id, { name, tagTypeId }) {
const endpoint = url + "/api/tag/" + id;
const response = await axios({
url: endpoint,
method: "PUT",
headers: {
'Authorization': 'Bearer ' + current_token,
},
withCredentials: true,
data: {
name, tagTypeId
}
})
return response.data;
}
export async function getTagTypes() {
const endpoint = url + "/api/tagtype";
const response = await axios.get(endpoint);
return response.data;
}

View File

@ -0,0 +1,94 @@
<script>
import { onMount } from "svelte";
import { Link, navigate } from "svelte-routing";
import { getTagTypes, updateTag } from "../../api";
export let tag;
export let data;
export let toggleRenameMenu;
export let onSubmit;
let tagTypes = [];
let form = {
name: "",
tagTypeId: 1,
};
const getData = async () => {
tagTypes = await getTagTypes();
form.name = data.tagName;
let tagType = tagTypes.filter((x) => x.name == data.tagType);
form.tagTypeId = tagType[0].id;
};
const onFormSubmit = async () => {
await updateTag(tag, form);
navigate("/tags/"+form.name);
toggleRenameMenu();
onSubmit(form.name);
};
onMount(() => {
getData();
});
</script>
<form on:submit|preventDefault={onFormSubmit}>
<div class="panel is-warning">
<p class="panel-heading">Edit Tag</p>
<div class="panel-block column">
<div class="row">
<strong>Name:</strong>
</div>
<div class="row">
<div class="field">
<div class="control">
<input
class="input"
type="text"
bind:value={form.name}
/>
</div>
</div>
</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Category:</strong>
</div>
<div class="row">
<div class="field">
<div class="select">
<select bind:value={form.tagTypeId}>
{#each tagTypes as tagType}
<option
value={tagType.id}
selected={form.tagTypeId === tagType.id}
>
{tagType.name}
</option>
{/each}
</select>
</div>
</div>
</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Posts:</strong>
</div>
<div class="row">
{data.postCount} (<Link to="/posts?tags={tag}">Browse</Link>)
</div>
</div>
<div class="panel-block column">
<button class="button is-primary" type="submit">Submit</button>
<button on:click|preventDefault={toggleRenameMenu} class="button"
>Cancel</button
>
</div>
</div>
</form>

View File

@ -0,0 +1,38 @@
<script>
import { Link } from "svelte-routing";
export let tag;
export let data;
export let toggleRenameMenu;
</script>
<div class="panel is-primary">
<p class="panel-heading">Tag</p>
<div class="panel-block column">
<div class="row">
<strong>Name:</strong>
</div>
<div class="row">{data.tagName}</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Category:</strong>
</div>
<div class="row">{data.tagType}</div>
</div>
<div class="panel-block column">
<div class="row">
<strong>Posts:</strong>
</div>
<div class="row">
{data.postCount} (<Link to="/posts?tags={tag}">Browse</Link>)
</div>
</div>
<div class="panel-block column">
<button
on:click|preventDefault={toggleRenameMenu}
class="button is-primary">Rename</button
>
</div>
</div>

View File

@ -0,0 +1,47 @@
<script>
import { onMount } from "svelte";
import { updateTagNotes } from "../../api";
export let tag;
export let data;
export let toggleEditMenu;
export let onSubmit;
let form = {
note: "",
};
const getData = async () => {
form.note = data.tagNote;
};
const onFormSubmit = async () => {
await updateTagNotes(tag, form);
toggleEditMenu();
onSubmit();
};
onMount(() => {
getData();
});
</script>
<form on:submit|preventDefault={onFormSubmit}>
<div class="panel is-warning">
<p class="panel-heading">Edit Notes</p>
<div class="panel-block column">
<textarea
bind:value={form.note}
class="textarea has-fixed-size"
/>
<div class="content" />
</div>
<div class="panel-block column">
<button type="submit" class="button is-primary">Save</button>
<button on:click|preventDefault={toggleEditMenu} class="button"
>Cancel</button
>
</div>
</div>
</form>

View File

@ -0,0 +1,25 @@
<script>
import { Link } from "svelte-routing";
export let data;
export let toggleEditMenu;
</script>
<div class="panel is-info">
<p class="panel-heading">Notes</p>
<div class="panel-block column">
<div class="content pre-line">
{data.tagNote}
</div>
</div>
<div class="panel-block column">
<button on:click|preventDefault={toggleEditMenu} class="button is-primary">Edit</button>
</div>
</div>
<style>
.pre-line {
white-space: pre-line;
}
</style>

View File

@ -6,6 +6,7 @@
@import '../node_modules/bulma/sass/grid/_all';
@import '../node_modules/bulma/sass/helpers/_all';
@import '../node_modules/bulma/sass/layout/_all';
@import '../node_modules/bytemd/dist/index.css';
.tile.is-multiline {
flex-wrap: wrap;

View File

@ -1,5 +1,5 @@
<script>
import { login } from "../api.js";
import { login } from "../../api.js";
import { navigate } from "svelte-routing";
let username = "";

View File

@ -1,5 +1,5 @@
<script>
import { token } from "../stores.js";
import { token } from "../../stores.js";
import { navigate } from "svelte-routing";
import { onMount } from "svelte";

View File

@ -1,5 +1,5 @@
<script>
import { register } from "../api.js";
import { register } from "../../api.js";
import { navigate } from "svelte-routing";
let username = "";

View File

@ -1,97 +0,0 @@
<script>
import { getPost, postUpdate, getTagAutocomplete } from "../api.js";
import { navigate } from "svelte-routing";
import Tags from "svelte-tags-input";
import { onMount } from "svelte";
import { Link } from "svelte-routing";
import AuthRequired from "../AuthRequired.svelte";
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 onAutocomplete = async () => {
const list = await getTagAutocomplete();
return list;
};
const onSubmit = async () => {
const response = await postUpdate(id, form);
navigate(`/post/${response.id}`);
};
onMount(() => {
getData();
});
</script>
<AuthRequired />
<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}
autoComplete={onAutocomplete}
/>
</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,9 +1,9 @@
<script>
import { onMount } from "svelte";
import { getPost, postDelete } from "../api.js";
import { getPost, postDelete } from "../../api.js";
import { navigate } from "svelte-routing";
import EditPostPanel from "../EditPostPanel.svelte";
import ViewPostPanel from "../ViewPostPanel.svelte";
import EditPostPanel from "../../EditPostPanel.svelte";
import ViewPostPanel from "../../ViewPostPanel.svelte";
export let id;
let post;
const getData = async () => {

View File

@ -1,12 +1,10 @@
<script>
import { onMount } from "svelte";
import { getPostSearchTag, getTagAutocomplete } from "../api.js";
import { getPostSearchTag, getTagAutocomplete } from "../../api.js";
import { Link, navigate } from "svelte-routing";
import TagLinkNumbered from "../TagLinkNumbered.svelte";
import TagLinkNumbered from "../../TagLinkNumbered.svelte";
import queryString from "query-string";
import Tags from "svelte-tags-input";
import { add_attribute } from "svelte/internal";
import { paginate } from "../simple-pagination.js";
import { paginate } from "../../simple-pagination.js";
export let location;

View File

@ -1,8 +1,8 @@
<script>
import { uploadBlob, postCreate, getTagAutocomplete } from "../api.js";
import { uploadBlob, postCreate, getTagAutocomplete } from "../../api.js";
import { navigate, Link } from "svelte-routing";
import Tags from "svelte-tags-input";
import AuthRequired from "../AuthRequired.svelte";
import AuthRequired from "../../AuthRequired.svelte";
let currentProgress = 0;

View File

@ -0,0 +1,70 @@
<script>
import { onMount } from "svelte";
import { getTag } from "../../api";
import EditTagNotesPanel from "../../components/TagNotes/EditTagNotesPanel.svelte";
import ViewTagNotesPanel from "../../components/TagNotes/ViewTagNotesPanel.svelte";
import ViewTagPanel from "../../components/Tag/ViewTagPanel.svelte";
import EditTagPanel from "../../components/Tag/EditTagPanel.svelte";
export let tag;
let data;
const getData = async () => {
if (tag) {
data = await getTag({ tag });
}
};
let renameMenuShown = false;
const toggleRenameMenu = () => {
renameMenuShown = !renameMenuShown;
};
let editMenuShown = false;
const toggleEditMenu = () => {
editMenuShown = !editMenuShown;
};
const onTagSubmit = (newName) => {
tag = newName;
getData();
};
onMount(() => {
getData();
});
</script>
<section class="section">
<div class="container">
{#if data}
<div class="columns">
<div class="column is-one-third">
{#if renameMenuShown}
<EditTagPanel
{tag}
{data}
{toggleRenameMenu}
onSubmit={onTagSubmit}
/>
{:else}
<ViewTagPanel {tag} {data} {toggleRenameMenu} />
{/if}
</div>
<div class="column is-two-thirds">
{#if editMenuShown}
<EditTagNotesPanel
{tag}
{data}
{toggleEditMenu}
onSubmit={getData}
/>
{:else}
<ViewTagNotesPanel {data} {toggleEditMenu} />
{/if}
</div>
</div>
{/if}
</div>
</section>

View File

@ -1,5 +1,5 @@
<script>
import { getTags } from "../api";
import { getTags } from "../../api";
import { Link } from "svelte-routing";
let tags = [];
@ -29,7 +29,7 @@
{#each tags as tag}
<tr>
<td>
<Link to="/posts?tags={tag.tagType}:{tag.tagName}">{tag.tagName}</Link>
<Link to="/tags/{tag.tagName}">{tag.tagName}</Link>
</td>
<td>{tag.tagType}</td>
<td>{tag.postCount}</td>

View File

@ -0,0 +1,26 @@
<script>
import { onMount } from "svelte";
import { getUserProfile } from "../../api.js";
import AuthRequired from "../../AuthRequired.svelte";
let user;
const getData = async () => {
user = await getUserProfile();
};
onMount(() => {
getData();
});
</script>
<AuthRequired />
<section class="section">
<div class="container">
{#if user}
<h1 class="title">Welcome, {user.username}</h1>
<p>Email: {user.email}</p>
<p>Username: {user.username}</p>
{/if}
</div>
</section>