| @ -0,0 +1,283 @@ | |||
| package Api | |||
| import ( | |||
| "encoding/json" | |||
| "io/ioutil" | |||
| "log" | |||
| "net/http" | |||
| "net/url" | |||
| "strconv" | |||
| "time" | |||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth" | |||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/JsonSerialization" | |||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" | |||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" | |||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Util" | |||
| ) | |||
| func getPosts(w http.ResponseWriter, r *http.Request) { | |||
| var ( | |||
| posts []Models.Post | |||
| returnJson []byte | |||
| values url.Values | |||
| page, pageSize int | |||
| search string | |||
| err error | |||
| ) | |||
| _, err = Auth.CheckCookie(r) | |||
| if err != nil { | |||
| w.WriteHeader(http.StatusUnauthorized) | |||
| return | |||
| } | |||
| values = r.URL.Query() | |||
| page, err = strconv.Atoi(values.Get("page")) | |||
| if err != nil { | |||
| log.Println("Could not parse page url argument") | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| pageSize, err = strconv.Atoi(values.Get("pageSize")) | |||
| if err != nil { | |||
| log.Println("Could not parse pageSize url argument") | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| search = values.Get("search") | |||
| posts, err = Database.GetPosts(page, pageSize, search) | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| if len(posts) == 0 { | |||
| Util.JsonReturn(w, 404, "No more data") | |||
| return | |||
| } | |||
| returnJson, err = json.MarshalIndent(posts, "", " ") | |||
| if err != nil { | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| // Return updated json | |||
| w.WriteHeader(http.StatusOK) | |||
| w.Write(returnJson) | |||
| } | |||
| func getPost(w http.ResponseWriter, r *http.Request) { | |||
| var ( | |||
| postData Models.Post | |||
| returnJson []byte | |||
| err error | |||
| ) | |||
| _, err = Auth.CheckCookie(r) | |||
| if err != nil { | |||
| w.WriteHeader(http.StatusUnauthorized) | |||
| return | |||
| } | |||
| postData, err = Util.GetPostById(w, r) | |||
| if err != nil { | |||
| return | |||
| } | |||
| returnJson, err = json.MarshalIndent(postData, "", " ") | |||
| if err != nil { | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| // Return updated json | |||
| w.WriteHeader(http.StatusOK) | |||
| w.Write(returnJson) | |||
| } | |||
| func createPost(w http.ResponseWriter, r *http.Request) { | |||
| var ( | |||
| requestBody []byte | |||
| postData Models.Post | |||
| returnJson []byte | |||
| err error | |||
| ) | |||
| _, err = Auth.CheckCookie(r) | |||
| if err != nil { | |||
| w.WriteHeader(http.StatusUnauthorized) | |||
| return | |||
| } | |||
| requestBody, err = ioutil.ReadAll(r.Body) | |||
| if err != nil { | |||
| log.Printf("Error encountered reading POST body: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| postData, err = JsonSerialization.DeserializePost(requestBody, []string{ | |||
| "id", | |||
| "links", | |||
| "images", | |||
| "videos", | |||
| "audios", | |||
| }, false) | |||
| if err != nil { | |||
| log.Printf("Invalid data provided to posts API: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 405, "Invalid data") | |||
| return | |||
| } | |||
| err = Database.CreatePost(&postData) | |||
| if err != nil { | |||
| Util.JsonReturn(w, 405, "Invalid data") | |||
| } | |||
| returnJson, err = json.MarshalIndent(postData, "", " ") | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| // Return updated json | |||
| w.WriteHeader(http.StatusOK) | |||
| w.Write(returnJson) | |||
| } | |||
| func updatePost(w http.ResponseWriter, r *http.Request) { | |||
| var ( | |||
| postData Models.Post | |||
| requestBody []byte | |||
| returnJson []byte | |||
| id string | |||
| err error | |||
| ) | |||
| _, err = Auth.CheckCookie(r) | |||
| if err != nil { | |||
| w.WriteHeader(http.StatusUnauthorized) | |||
| return | |||
| } | |||
| id, err = Util.GetPostId(r) | |||
| if err != nil { | |||
| log.Printf("Error encountered getting id\n") | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| requestBody, err = ioutil.ReadAll(r.Body) | |||
| if err != nil { | |||
| log.Printf("Error encountered reading POST body: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| postData, err = JsonSerialization.DeserializePost(requestBody, []string{}, true) | |||
| if err != nil { | |||
| log.Printf("Invalid data provided to posts API: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 405, "Invalid data") | |||
| return | |||
| } | |||
| postData, err = Database.UpdatePost(id, &postData) | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| returnJson, err = json.MarshalIndent(postData, "", " ") | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| // Return updated json | |||
| w.WriteHeader(http.StatusOK) | |||
| w.Write(returnJson) | |||
| } | |||
| func publishPost(w http.ResponseWriter, r *http.Request) { | |||
| var ( | |||
| now time.Time = time.Now() | |||
| postData Models.Post | |||
| returnJson []byte | |||
| err error | |||
| ) | |||
| _, err = Auth.CheckCookie(r) | |||
| if err != nil { | |||
| w.WriteHeader(http.StatusUnauthorized) | |||
| return | |||
| } | |||
| postData, err = Util.GetPostById(w, r) | |||
| if err != nil { | |||
| log.Printf("Error encountered getting id\n") | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| if postData.PublishedAt == nil { | |||
| postData.PublishedAt = &now | |||
| } else { | |||
| postData.PublishedAt = nil | |||
| } | |||
| postData, err = Database.UpdatePost(postData.ID.String(), &postData) | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| returnJson, err = json.MarshalIndent(postData, "", " ") | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| // Return updated json | |||
| w.WriteHeader(http.StatusOK) | |||
| w.Write(returnJson) | |||
| } | |||
| func deletePost(w http.ResponseWriter, r *http.Request) { | |||
| var ( | |||
| postData Models.Post | |||
| err error | |||
| ) | |||
| _, err = Auth.CheckCookie(r) | |||
| if err != nil { | |||
| w.WriteHeader(http.StatusUnauthorized) | |||
| return | |||
| } | |||
| postData, err = Util.GetPostById(w, r) | |||
| if err != nil { | |||
| return | |||
| } | |||
| err = Database.DeletePost(&postData) | |||
| if err != nil { | |||
| log.Printf("An error occured: %s\n", err.Error()) | |||
| Util.JsonReturn(w, 500, "An error occured") | |||
| return | |||
| } | |||
| // Return updated json | |||
| w.WriteHeader(http.StatusOK) | |||
| } | |||
| @ -0,0 +1,97 @@ | |||
| package Database | |||
| import ( | |||
| "fmt" | |||
| "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" | |||
| "gorm.io/gorm" | |||
| "gorm.io/gorm/clause" | |||
| ) | |||
| func GetPosts(page, pageSize int, search string) ([]Models.Post, error) { | |||
| var ( | |||
| posts []Models.Post | |||
| query *gorm.DB | |||
| offset int | |||
| err error | |||
| ) | |||
| switch { | |||
| case pageSize > 100: | |||
| pageSize = 100 | |||
| case pageSize <= 0: | |||
| pageSize = 10 | |||
| } | |||
| offset = page * pageSize | |||
| search = fmt.Sprintf("%%%s%%", search) | |||
| query = DB.Model(Models.Post{}). | |||
| Preload(clause.Associations). | |||
| Offset(offset). | |||
| Limit(pageSize). | |||
| Order("created_at desc") | |||
| if search != "%%" { | |||
| query = query. | |||
| Where("title LIKE ?", search). | |||
| Or("content LIKE ?", search) | |||
| } | |||
| err = query. | |||
| Find(&posts). | |||
| Error | |||
| return posts, err | |||
| } | |||
| func GetPostById(id string) (Models.Post, error) { | |||
| var ( | |||
| postData Models.Post | |||
| err error | |||
| ) | |||
| err = DB.Preload(clause.Associations). | |||
| First(&postData, "id = ?", id). | |||
| Error | |||
| return postData, err | |||
| } | |||
| func CreatePost(postData *Models.Post) error { | |||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||
| Create(postData). | |||
| Error | |||
| } | |||
| func UpdatePost(id string, postData *Models.Post) (Models.Post, error) { | |||
| var ( | |||
| err error | |||
| ) | |||
| DB.Model(postData). | |||
| Where("id = ?", id). | |||
| Association("PostLinks"). | |||
| Replace(postData.PostLinks) | |||
| err = DB.Model(&Models.Post{}). | |||
| Select("*"). | |||
| Omit("id", "created_at", "updated_at", "deleted_at"). | |||
| Where("id = ?", id). | |||
| Updates(postData). | |||
| Error | |||
| if err != nil { | |||
| return Models.Post{}, err | |||
| } | |||
| return GetPostById(id) | |||
| } | |||
| func DeletePost(postData *Models.Post) error { | |||
| return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||
| Delete(postData). | |||
| Error | |||
| } | |||
| @ -0,0 +1,204 @@ | |||
| <template> | |||
| <div class="form-outline editor-form" v-if="editor"> | |||
| <div class="mb-3"> | |||
| <div class="input-group"> | |||
| <button | |||
| @click="editor.chain().focus().toggleBold().run()" | |||
| :class="editor.isActive('bold') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-bold"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleItalic().run()" | |||
| :class="editor.isActive('italic') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-italic"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleStrike().run()" | |||
| :class="editor.isActive('strike') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-strikethrough"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleCode().run()" | |||
| :class="editor.isActive('code') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-code"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().setParagraph().run()" | |||
| :class="editor.isActive('paragraph') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-paragraph"></i> | |||
| </button> | |||
| </div> | |||
| <div class="input-group"> | |||
| <button | |||
| @click="editor.chain().focus().toggleHeading({level: 1 }).run()" | |||
| :class="editor.isActive('heading', { level: 1 }) ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| H1 | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleHeading({level: 2 }).run()" | |||
| :class="editor.isActive('heading', { level: 2 }) ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| H2 | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleHeading({level: 3 }).run()" | |||
| :class="editor.isActive('heading', { level: 3 }) ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| H3 | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleHeading({level: 4 }).run()" | |||
| :class="editor.isActive('heading', { level: 4 }) ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| H4 | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" | |||
| :class="editor.isActive('heading', { level: 5 }) ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| H5 | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" | |||
| :class="editor.isActive('heading', { level: 6 }) ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| H6 | |||
| </button> | |||
| </div> | |||
| <div class="input-group"> | |||
| <button | |||
| @click="editor.chain().focus().toggleBulletList().run()" | |||
| :class="editor.isActive('bulletList') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-list"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleOrderedList().run()" | |||
| :class="editor.isActive('orderedList') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-list-ol"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().toggleCodeBlock().run()" | |||
| :class="editor.isActive('codeBlock') ? 'btn-dark' : 'btn-outline-dark'" | |||
| type="button" | |||
| class="btn" | |||
| > | |||
| <i class="fa-solid fa-file-code"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().setHorizontalRule().run()" | |||
| type="button" | |||
| class="btn btn-outline-dark" | |||
| > | |||
| <i class="fa-solid fa-ruler-horizontal"></i> | |||
| </button> | |||
| </div> | |||
| <div class="input-group"> | |||
| <button | |||
| @click="editor.chain().focus().undo().run()" | |||
| type="button" | |||
| class="btn btn-outline-dark" | |||
| > | |||
| <i class="fa-solid fa-rotate-left"></i> | |||
| </button> | |||
| <button | |||
| @click="editor.chain().focus().redo().run()" | |||
| type="button" | |||
| class="btn btn-outline-dark" | |||
| > | |||
| <i class="fa-solid fa-rotate-right"></i> | |||
| </button> | |||
| </div> | |||
| </div> | |||
| <editor-content :editor="editor" class="form-control"/> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { Editor, EditorContent } from '@tiptap/vue-3' | |||
| import StarterKit from '@tiptap/starter-kit' | |||
| export default { | |||
| props: { | |||
| modelValue: { | |||
| type: String, | |||
| default: '', | |||
| }, | |||
| }, | |||
| data () { | |||
| return { | |||
| editor: null, | |||
| } | |||
| }, | |||
| components: { | |||
| EditorContent, | |||
| }, | |||
| watch: { | |||
| modelValue(value) { | |||
| // HTML | |||
| const isSame = this.editor.getHTML() === value | |||
| // JSON | |||
| // const isSame = JSON.stringify(this.editor.getJSON()) === JSON.stringify(value) | |||
| if (isSame) { | |||
| return | |||
| } | |||
| this.editor.commands.setContent(value, false) | |||
| }, | |||
| }, | |||
| mounted() { | |||
| this.editor = new Editor({ | |||
| extensions: [ | |||
| StarterKit, | |||
| ], | |||
| content: this.modelValue, | |||
| onUpdate: () => { | |||
| // HTML | |||
| this.$emit('update:modelValue', this.editor.getHTML()) | |||
| // JSON | |||
| // this.$emit('update:modelValue', this.editor.getJSON()) | |||
| }, | |||
| }) | |||
| }, | |||
| beforeUnmount() { | |||
| this.editor.destroy() | |||
| }, | |||
| } | |||
| </script> | |||