mirror of
https://github.com/Floriansylvain/RenewCMS.git
synced 2026-08-19 19:53:21 +02:00
Merge pull request #20 from Floriansylvain/dev
Multiple refactors and routes improvement
This commit is contained in:
@@ -1,23 +1,43 @@
|
|||||||
# GohCMS
|
# GohCMS
|
||||||
|
|
||||||
## 🚧 This project is under development !
|
## 🚧 This project is under development !
|
||||||
|
|
||||||
The features are not complete, not fully tested and subject to many changes.
|
The features are not complete, not fully tested and subject to many changes.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
Don't forget to setup the [Environment variables](#environment-variables)!
|
||||||
|
|
||||||
### Use with Docker
|
### Use with Docker
|
||||||
|
|
||||||
To **run** the app, run the command
|
To **run** the app, run the command
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker-compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build and run it yourself
|
### Build and run it yourself
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
## Environment variables
|
|
||||||
- ./env
|
### Environment variables
|
||||||
- APP_BASE_API_PATH
|
|
||||||
- APP_BASE_FRONT_PATH
|
- ./env:
|
||||||
- APP_API_ADDRESS
|
|
||||||
- APP_FRONT_ADDRESS
|
```
|
||||||
- APP_API_PORT
|
APP_API_ADDRESS=http://example.com
|
||||||
- APP_FRONT_PORT
|
APP_FRONT_ADDRESS=http://example.com
|
||||||
- APP_JWT_SECRET
|
APP_BASE_API_PATH=/example
|
||||||
|
APP_BASE_FRONT_PATH=/example
|
||||||
|
APP_API_PORT=1234
|
||||||
|
APP_FRONT_PORT=1234
|
||||||
|
APP_JWT_SECRET=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Usage
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|||||||
+5
-5
@@ -63,11 +63,11 @@ func initArticlesRoutes(r *gin.RouterGroup) {
|
|||||||
articlesRouter := r.Group("/articles")
|
articlesRouter := r.Group("/articles")
|
||||||
articlesRouter.Use(corsMiddleware, api.AuthMiddleware.MiddlewareFunc())
|
articlesRouter.Use(corsMiddleware, api.AuthMiddleware.MiddlewareFunc())
|
||||||
|
|
||||||
articlesRouter.GET("/", articles.GetArticleHandler)
|
articlesRouter.GET("/", articles.Get)
|
||||||
articlesRouter.GET("/:id", articles.GetArticleHandler)
|
articlesRouter.GET("/:id", articles.GetUnique)
|
||||||
articlesRouter.POST("/:id", articles.AddArticleHandler)
|
articlesRouter.POST("/:id", articles.Add)
|
||||||
articlesRouter.PATCH("/:id", articles.EditArticleHandler)
|
articlesRouter.PUT("/:id", articles.Edit)
|
||||||
articlesRouter.DELETE("/:id", articles.DeleteArticleHandler)
|
articlesRouter.DELETE("/:id", articles.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGin() {
|
func initGin() {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func SendBadRequest(c *gin.Context, message string) {
|
func SendBadRequest(c *gin.Context, message string) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"message": message})
|
c.JSON(http.StatusBadRequest, gin.H{"message": message, "code": 400})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendOk(c *gin.Context, message string) {
|
func SendOk(c *gin.Context, message string) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": message})
|
c.JSON(http.StatusOK, gin.H{"message": message, "code": 200})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendForbidden(c *gin.Context, message string) {
|
func SendForbidden(c *gin.Context, message string) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"message": message})
|
c.JSON(http.StatusForbidden, gin.H{"message": message, "code": 403})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package articles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/api"
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Add(c *gin.Context) {
|
||||||
|
var article Article
|
||||||
|
article.TitleID = c.Params.ByName("id")
|
||||||
|
if c.BindJSON(&article) != nil {
|
||||||
|
api.SendBadRequest(c, "Could not correctly parse the article.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document, err := bson.Marshal(article)
|
||||||
|
if err != nil {
|
||||||
|
api.SendBadRequest(c, "Could not correctly marshal the article.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
documents, _ := database.GetDocuments(articlesLocation, map[string]any{})
|
||||||
|
if IsArticleIdAlreadyUsed(article.TitleID, documents) {
|
||||||
|
api.SendBadRequest(c, fmt.Sprintf("Article ID '%v' already used.", article.TitleID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.PushDocument(articlesLocation, document)
|
||||||
|
if err != nil {
|
||||||
|
api.SendBadRequest(c, fmt.Sprintf(`Could not insert document(s) into DB: %v`, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.SendOk(c, "Article successfully added!")
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package articles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/api"
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Delete(c *gin.Context) {
|
||||||
|
id := c.Params.ByName("id")
|
||||||
|
|
||||||
|
deleteCount, err := database.DeleteDocument(articlesLocation, map[string]any{"titleID": id})
|
||||||
|
if err != nil {
|
||||||
|
api.SendBadRequest(c, fmt.Sprintf(`Could not delete document(s) from DB: %v`, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleteCount != 0 {
|
||||||
|
api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully deleted!", deleteCount))
|
||||||
|
} else {
|
||||||
|
api.SendOk(c, "No articles were deleted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package articles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/api"
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Edit(c *gin.Context) {
|
||||||
|
id := c.Params.ByName("id")
|
||||||
|
|
||||||
|
var articleUpdate database.DocumentUpdate
|
||||||
|
articleUpdate.Filter = map[string]any{"titleID": id}
|
||||||
|
c.BindJSON(&articleUpdate.Update)
|
||||||
|
|
||||||
|
editCount, err := database.EditDocument(articlesLocation, articleUpdate)
|
||||||
|
if err != nil {
|
||||||
|
api.SendBadRequest(c, fmt.Sprintf(`Could not edit document(s) from DB: %v`, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if editCount != 0 {
|
||||||
|
api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully edited!", editCount))
|
||||||
|
} else {
|
||||||
|
api.SendOk(c, "No articles were edited.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package articles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/api"
|
||||||
|
"github.com/Floriansylvain/GohCMS/internal/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getApiFullUrl() string {
|
||||||
|
return fmt.Sprintf("%v%v", os.Getenv("APP_API_ADDRESS"), os.Getenv("APP_BASE_API_PATH"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArticleSkipTakeFullUrl(skip uint64, take uint64) string {
|
||||||
|
return fmt.Sprintf("%varticles?skip=%v&take=%v", getApiFullUrl(), skip+take, take)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuiltGetResponse(articles []Article, skip uint64, take uint64) map[string]any {
|
||||||
|
articlesCap := uint64(len(articles))
|
||||||
|
|
||||||
|
var normTake uint64
|
||||||
|
if skip+take > articlesCap {
|
||||||
|
normTake = articlesCap
|
||||||
|
} else {
|
||||||
|
normTake = skip + take
|
||||||
|
}
|
||||||
|
|
||||||
|
slicedArticles := articles[skip:normTake]
|
||||||
|
total := len(slicedArticles)
|
||||||
|
return map[string]any{
|
||||||
|
"content": slicedArticles,
|
||||||
|
"total": total,
|
||||||
|
"pagination": map[string]any{
|
||||||
|
"skip": skip,
|
||||||
|
"take": take,
|
||||||
|
"links": map[string]any{
|
||||||
|
"next": getArticleSkipTakeFullUrl(skip+take, take),
|
||||||
|
"previous": getArticleSkipTakeFullUrl(skip-take, take),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"last_page": math.Ceil(float64(articlesCap) / float64(take)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUintQueryParam(c *gin.Context, param string, defaultValue uint64) (uint64, error) {
|
||||||
|
if c.Query(param) == "" {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(c.Query(param), 10, 0)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArticleIdFilter(titleID string) map[string]any {
|
||||||
|
filter := map[string]any{}
|
||||||
|
if titleID != "" {
|
||||||
|
filter["titleID"] = titleID
|
||||||
|
}
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(c *gin.Context) {
|
||||||
|
skip, skipErr := parseUintQueryParam(c, "skip", 0)
|
||||||
|
take, takeErr := parseUintQueryParam(c, "take", 10)
|
||||||
|
if skipErr != nil || takeErr != nil {
|
||||||
|
api.SendBadRequest(c, "Take and Skip query parameters must be positive numbers.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
documents, _ := database.GetDocuments(articlesLocation, map[string]any{})
|
||||||
|
articlesArray := ParseArticlesFromBytesToArray(documents)
|
||||||
|
articlesArrayLength := uint64(len(articlesArray))
|
||||||
|
if skip >= articlesArrayLength || take == 0 {
|
||||||
|
c.JSON(http.StatusOK, map[string]any{
|
||||||
|
"content": []string{},
|
||||||
|
"total": "0",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, getBuiltGetResponse(articlesArray, skip, take))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUnique(c *gin.Context) {
|
||||||
|
titleID := c.Params.ByName("id")
|
||||||
|
|
||||||
|
article, err := database.GetUniqueDocument(articlesLocation, getArticleIdFilter(titleID))
|
||||||
|
if err != nil {
|
||||||
|
api.SendBadRequest(c, fmt.Sprintf("The ID '%v' doesn't match any article.", titleID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedArticle Article
|
||||||
|
bson.Unmarshal(article, &parsedArticle)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, parsedArticle)
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package articles
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/Floriansylvain/GohCMS/internal/api"
|
|
||||||
"github.com/Floriansylvain/GohCMS/internal/database"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetArticleHandler(c *gin.Context) {
|
|
||||||
articleID := c.Params.ByName("id")
|
|
||||||
|
|
||||||
filter := gin.H{}
|
|
||||||
if articleID != "" {
|
|
||||||
filter["titleID"] = articleID
|
|
||||||
}
|
|
||||||
|
|
||||||
articles, err := database.GetDocuments(articlesLocation, filter)
|
|
||||||
if err != nil {
|
|
||||||
api.SendBadRequest(c, fmt.Sprintf("The ID '%v' doesn't match any article.", articleID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, ParseArticlesFromBytesToArray(articles))
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddArticleHandler(c *gin.Context) {
|
|
||||||
var article Article
|
|
||||||
article.TitleID = c.Params.ByName("id")
|
|
||||||
if c.BindJSON(&article) != nil {
|
|
||||||
api.SendBadRequest(c, "Could not correctly parse the article.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
document, err := bson.Marshal(article)
|
|
||||||
if err != nil {
|
|
||||||
api.SendBadRequest(c, "Could not correctly marshal the article.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
documents, _ := database.GetDocuments(articlesLocation, gin.H{})
|
|
||||||
if IsArticleIdAlreadyUsed(article.TitleID, documents) {
|
|
||||||
api.SendBadRequest(c, fmt.Sprintf("Article ID '%v' already used.", article.TitleID))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = database.PushDocument(articlesLocation, document)
|
|
||||||
if err != nil {
|
|
||||||
api.SendBadRequest(c, fmt.Sprintf(`Could not insert document(s) into DB: %v`, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.SendOk(c, "Article successfully added!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteArticleHandler(c *gin.Context) {
|
|
||||||
id := c.Params.ByName("id")
|
|
||||||
|
|
||||||
deleteCount, err := database.DeleteDocument(articlesLocation, gin.H{"titleID": id})
|
|
||||||
if err != nil {
|
|
||||||
api.SendBadRequest(c, fmt.Sprintf(`Could not delete document(s) from DB: %v`, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if deleteCount != 0 {
|
|
||||||
api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully deleted!", deleteCount))
|
|
||||||
} else {
|
|
||||||
api.SendOk(c, "No articles were deleted.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func EditArticleHandler(c *gin.Context) {
|
|
||||||
id := c.Params.ByName("id")
|
|
||||||
|
|
||||||
var articleUpdate database.DocumentUpdate
|
|
||||||
articleUpdate.Filter = gin.H{"titleID": id}
|
|
||||||
c.BindJSON(&articleUpdate.Update)
|
|
||||||
|
|
||||||
editCount, err := database.EditDocument(articlesLocation, articleUpdate)
|
|
||||||
if err != nil {
|
|
||||||
api.SendBadRequest(c, fmt.Sprintf(`Could not edit document(s) from DB: %v`, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if editCount != 0 {
|
|
||||||
api.SendOk(c, fmt.Sprintf("%d article(s) was/were successfully edited!", editCount))
|
|
||||||
} else {
|
|
||||||
api.SendOk(c, "No articles were edited.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,7 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
)
|
)
|
||||||
@@ -14,11 +12,6 @@ type Location struct {
|
|||||||
Collection string
|
Collection string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DocumentUpdate struct {
|
|
||||||
Filter gin.H `json:"filter"`
|
|
||||||
Update gin.H `json:"update"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetNewClient() *mongo.Client {
|
func GetNewClient() *mongo.Client {
|
||||||
client, err := mongo.Connect(
|
client, err := mongo.Connect(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
@@ -28,73 +21,3 @@ func GetNewClient() *mongo.Client {
|
|||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushDocument(location Location, document interface{}) error {
|
|
||||||
client := GetNewClient()
|
|
||||||
collection := client.Database(location.Database).Collection(location.Collection)
|
|
||||||
defer client.Disconnect(context.TODO())
|
|
||||||
|
|
||||||
_, err := collection.InsertOne(context.TODO(), document)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDocuments(location Location, filter interface{}) ([][]byte, error) {
|
|
||||||
client := GetNewClient()
|
|
||||||
collection := client.Database(location.Database).Collection(location.Collection)
|
|
||||||
defer client.Disconnect(context.TODO())
|
|
||||||
|
|
||||||
var results [][]byte
|
|
||||||
|
|
||||||
cursor, err := collection.Find(context.TODO(), filter)
|
|
||||||
if err != nil {
|
|
||||||
return results, errors.New("something is wrong with filter to find the document.")
|
|
||||||
}
|
|
||||||
for cursor.TryNext(context.TODO()) {
|
|
||||||
results = append(results, cursor.Current)
|
|
||||||
}
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUniqueDocument(location Location, filter interface{}) ([]byte, error) {
|
|
||||||
client := GetNewClient()
|
|
||||||
collection := client.Database(location.Database).Collection(location.Collection)
|
|
||||||
defer client.Disconnect(context.TODO())
|
|
||||||
|
|
||||||
singleResult := collection.FindOne(context.TODO(), filter)
|
|
||||||
if singleResult.Err() != nil {
|
|
||||||
return nil, errors.New("no document was found, check the filter or the location.")
|
|
||||||
}
|
|
||||||
return singleResult.DecodeBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteDocument(location Location, filter interface{}) (int64, error) {
|
|
||||||
client := GetNewClient()
|
|
||||||
collection := client.Database(location.Database).Collection(location.Collection)
|
|
||||||
defer client.Disconnect(context.TODO())
|
|
||||||
|
|
||||||
result, err := collection.DeleteOne(context.TODO(), filter)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.New("something is wrong with filter to delete the document.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.DeletedCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EditDocument(location Location, jsons DocumentUpdate) (int64, error) {
|
|
||||||
client := GetNewClient()
|
|
||||||
collection := client.Database(location.Database).Collection(location.Collection)
|
|
||||||
defer client.Disconnect(context.TODO())
|
|
||||||
|
|
||||||
result, err := collection.UpdateOne(context.TODO(), jsons.Filter, gin.H{"$set": jsons.Update})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if result.MatchedCount == 0 {
|
|
||||||
return 0, errors.New("cannot find document matching filter.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ModifiedCount, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteDocument(location Location, filter interface{}) (int64, error) {
|
||||||
|
client := GetNewClient()
|
||||||
|
collection := client.Database(location.Database).Collection(location.Collection)
|
||||||
|
defer client.Disconnect(context.TODO())
|
||||||
|
|
||||||
|
result, err := collection.DeleteOne(context.TODO(), filter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("something is wrong with filter to delete the document.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.DeletedCount, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentUpdate struct {
|
||||||
|
Filter gin.H `json:"filter"`
|
||||||
|
Update gin.H `json:"update"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditDocument(location Location, jsons DocumentUpdate) (int64, error) {
|
||||||
|
client := GetNewClient()
|
||||||
|
collection := client.Database(location.Database).Collection(location.Collection)
|
||||||
|
defer client.Disconnect(context.TODO())
|
||||||
|
|
||||||
|
result, err := collection.UpdateOne(context.TODO(), jsons.Filter, gin.H{"$set": jsons.Update})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if result.MatchedCount == 0 {
|
||||||
|
return 0, errors.New("cannot find document matching filter.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ModifiedCount, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDocuments(location Location, filter interface{}) ([][]byte, error) {
|
||||||
|
client := GetNewClient()
|
||||||
|
collection := client.Database(location.Database).Collection(location.Collection)
|
||||||
|
defer client.Disconnect(context.TODO())
|
||||||
|
|
||||||
|
var results [][]byte
|
||||||
|
|
||||||
|
cursor, err := collection.Find(context.TODO(), filter)
|
||||||
|
if err != nil {
|
||||||
|
return results, errors.New("something is wrong with filter to find the document.")
|
||||||
|
}
|
||||||
|
for cursor.TryNext(context.TODO()) {
|
||||||
|
results = append(results, cursor.Current)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUniqueDocument(location Location, filter interface{}) ([]byte, error) {
|
||||||
|
client := GetNewClient()
|
||||||
|
collection := client.Database(location.Database).Collection(location.Collection)
|
||||||
|
defer client.Disconnect(context.TODO())
|
||||||
|
|
||||||
|
singleResult := collection.FindOne(context.TODO(), filter)
|
||||||
|
if singleResult.Err() != nil {
|
||||||
|
return nil, errors.New("no document was found, check the filter or the location.")
|
||||||
|
}
|
||||||
|
return singleResult.DecodeBytes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
func PushDocument(location Location, document interface{}) error {
|
||||||
|
client := GetNewClient()
|
||||||
|
collection := client.Database(location.Database).Collection(location.Collection)
|
||||||
|
defer client.Disconnect(context.TODO())
|
||||||
|
|
||||||
|
_, err := collection.InsertOne(context.TODO(), document)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -103,6 +103,12 @@ th,
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-input input:focus,
|
||||||
|
.label-input-error input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: solid 3px var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -181,6 +187,11 @@ th,
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-primary:focus,
|
||||||
|
.button-secondary:focus {
|
||||||
|
outline: solid 3px var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
.tox-tinymce {
|
.tox-tinymce {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
|||||||
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"fr-FR": {
|
||||||
|
"pagination": {
|
||||||
|
"first": "Premier",
|
||||||
|
"first_title": "Première Page",
|
||||||
|
"last": "Dernier",
|
||||||
|
"last_title": "Dernière Page",
|
||||||
|
"prev": "Précédent",
|
||||||
|
"prev_title": "Page Précédente",
|
||||||
|
"next": "Suivant",
|
||||||
|
"next_title": "Page Suivante",
|
||||||
|
"all": "Toute",
|
||||||
|
"page_size": "Nombre d'éléments"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const emits = defineEmits(['close'])
|
||||||
|
const props = defineProps<{
|
||||||
|
title?: string,
|
||||||
|
description: string,
|
||||||
|
type: 'error' | 'success'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal">
|
||||||
|
<header class="error-bg" v-if="type === 'error'">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
|
stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
<h2>{{ props.title ?? 'Échec !' }}</h2>
|
||||||
|
</header>
|
||||||
|
<header class="success-bg" v-else>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
|
stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<h2>{{ props.title ?? 'Succès !' }}</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<p>{{ props.description }}</p>
|
||||||
|
<button class="button-primary" @click="$emit('close')">Fermer</button>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
|
||||||
|
box-shadow: #0002 0 0 15px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
width: 500px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-bg {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-bg {
|
||||||
|
background-color: var(--secondary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
color: var(--neutral-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
main p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main button {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 50px;
|
||||||
|
color: var(--neutral-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -78,6 +78,10 @@ header nav a:hover {
|
|||||||
background-color: var(--neutral-verylight);
|
background-color: var(--neutral-verylight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header nav a:focus {
|
||||||
|
outline: solid 3px var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
.disconnect-link {
|
.disconnect-link {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,25 +11,61 @@ export interface Article {
|
|||||||
online: boolean
|
online: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getArticles(id: string) : Promise<Array<Article>> {
|
export interface GetArticle {
|
||||||
return await fetch(`${baseApiUrl}/articles/${id}`, {
|
content: Article[],
|
||||||
credentials: 'include',
|
total: number
|
||||||
method: 'GET',
|
pagination: {
|
||||||
})
|
skip: number,
|
||||||
.then(result => result.json())
|
take: number
|
||||||
.catch(error => {
|
links: {
|
||||||
console.error(error)
|
next: string,
|
||||||
})
|
previous: string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postArticle(article: Article) : Promise<object> {
|
function fetchArticle(id: string): Promise<Article | GetArticle> {
|
||||||
return await fetch(`${baseApiUrl}/articles/${article.titleID}`, {
|
return new Promise((resolve, reject) => {
|
||||||
credentials: 'include',
|
fetch(`${baseApiUrl}/articles/${id}`, {
|
||||||
method: 'POST',
|
credentials: 'include',
|
||||||
body: JSON.stringify(article)
|
method: 'GET',
|
||||||
})
|
|
||||||
.then(result => result.json())
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(article => resolve(article))
|
||||||
|
.catch(error => reject(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUniqueArticle(id: string): Promise<Article> {
|
||||||
|
return fetchArticle(id) as Promise<Article>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAllArticle(): Promise<GetArticle> {
|
||||||
|
return fetchArticle("") as Promise<GetArticle>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteArticle(id: string): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch(`${baseApiUrl}/articles/${id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(article => resolve(article))
|
||||||
|
.catch(error => reject(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendArticleWithMethod(article: Article, method: 'POST' | 'PUT'): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch(`${baseApiUrl}/articles/${article.titleID}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method,
|
||||||
|
body: JSON.stringify(article)
|
||||||
|
})
|
||||||
|
.then(result => result.json())
|
||||||
|
.then(article => resolve(article))
|
||||||
|
.catch(error => reject(error))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
export function reloadPage() {
|
||||||
|
router.go(0)
|
||||||
|
}
|
||||||
@@ -1,20 +1,81 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getArticles, type Article } from '@/utils/database';
|
import tabulator_langs from '@/assets/tabulator_langs.json'
|
||||||
import { onMounted, ref, type Ref } from 'vue';
|
import { baseApiUrl } from '@/utils/api'
|
||||||
|
import { deleteArticle } from '@/utils/database'
|
||||||
|
import { reloadPage } from '@/utils/router'
|
||||||
|
import { TabulatorFull as Tabulator, type CellComponent } from 'tabulator-tables'
|
||||||
|
import { onMounted, ref, type Ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { TabulatorFull as Tabulator } from 'tabulator-tables'
|
|
||||||
|
|
||||||
const articles: Ref<Array<Article>> = ref([])
|
interface BasicTableButton {
|
||||||
|
classes: string[],
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
const table = ref<HTMLInputElement | string>('')
|
const table: Ref<HTMLInputElement | string> = ref('')
|
||||||
const tabulator: Ref<Tabulator | undefined> = ref(undefined)
|
const tabulator: Ref<Tabulator | undefined> = ref(undefined)
|
||||||
|
|
||||||
onMounted(async () => {
|
const basicEditButton = getBasicTableButton({ classes: ['button-secondary', 'table-action-button'], text: '✏️' })
|
||||||
articles.value = await getArticles('')
|
const basicDeleteButton = getBasicTableButton({ classes: ['button-secondary', 'table-action-button'], text: '❌' })
|
||||||
|
|
||||||
|
function getBasicTableButton(basicButton: BasicTableButton): HTMLAnchorElement {
|
||||||
|
const button = document.createElement('a')
|
||||||
|
button.classList.add(...basicButton.classes)
|
||||||
|
button.textContent = basicButton.text
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTablePage(): void {
|
||||||
|
const currentPage = tabulator.value?.getPage()
|
||||||
|
|
||||||
|
if (isNaN(currentPage as number)) {
|
||||||
|
reloadPage()
|
||||||
|
} else {
|
||||||
|
tabulator.value?.setPage(currentPage as number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowEditButton(cell: CellComponent): Node {
|
||||||
|
const editButton = basicEditButton.cloneNode(true) as HTMLAnchorElement
|
||||||
|
editButton.href = `/articles/edit/${cell.getValue()}`
|
||||||
|
return editButton
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowDeleteButton(cell: CellComponent): Node {
|
||||||
|
const deleteButton = basicDeleteButton.cloneNode(true) as HTMLAnchorElement
|
||||||
|
|
||||||
|
deleteButton.onclick = async () => {
|
||||||
|
await deleteArticle(cell.getValue())
|
||||||
|
updateTablePage()
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteButton
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonsCell(cell: CellComponent): HTMLDivElement {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.append(getRowEditButton(cell), getRowDeleteButton(cell))
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableAjaxUrlPage(url: string, config: any, params: any) {
|
||||||
|
config.credentials = 'include'
|
||||||
|
return url + `?skip=${params.size * (params.page - 1)}&take=${params.size}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTabulatorTable(): void {
|
||||||
tabulator.value = new Tabulator(table.value, {
|
tabulator.value = new Tabulator(table.value, {
|
||||||
data: articles.value,
|
|
||||||
reactiveData: true,
|
|
||||||
layout: 'fitColumns',
|
layout: 'fitColumns',
|
||||||
|
locale: 'fr-FR',
|
||||||
|
reactiveData: true,
|
||||||
|
selectable: false,
|
||||||
|
pagination: true,
|
||||||
|
paginationSizeSelector: true,
|
||||||
|
paginationMode: 'remote',
|
||||||
|
paginationSize: 10,
|
||||||
|
ajaxURL: `${baseApiUrl}/articles`,
|
||||||
|
ajaxURLGenerator: getTableAjaxUrlPage,
|
||||||
|
dataReceiveParams: { data: "content" },
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: 'Titre',
|
title: 'Titre',
|
||||||
@@ -46,25 +107,16 @@ onMounted(async () => {
|
|||||||
{
|
{
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
field: 'titleID',
|
field: 'titleID',
|
||||||
formatter: function (cell) {
|
formatter: getButtonsCell,
|
||||||
const container = document.createElement('div')
|
|
||||||
|
|
||||||
const editButton = document.createElement('a')
|
|
||||||
const deleteButton = document.createElement('a')
|
|
||||||
editButton.classList.add('button-secondary')
|
|
||||||
deleteButton.classList.add('button-secondary')
|
|
||||||
editButton.textContent = '✏️'
|
|
||||||
deleteButton.textContent = '🗑️'
|
|
||||||
|
|
||||||
editButton.href = `/articles/edit/${cell.getValue()}`
|
|
||||||
|
|
||||||
container.append(editButton, deleteButton)
|
|
||||||
return container
|
|
||||||
},
|
|
||||||
headerSort: false
|
headerSort: false
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
langs: tabulator_langs
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
initTabulatorTable()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -75,12 +127,20 @@ onMounted(async () => {
|
|||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-action-button {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,19 +150,10 @@ main>a {
|
|||||||
|
|
||||||
#table {
|
#table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
margin: auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons>* {
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,50 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getArticles, type Article } from '@/utils/database';
|
import Modal from '@/components/ModalSuccessError.vue';
|
||||||
|
import { fetchUniqueArticle, sendArticleWithMethod, type Article } from '@/utils/database';
|
||||||
import Editor from '@tinymce/tinymce-vue';
|
import Editor from '@tinymce/tinymce-vue';
|
||||||
import { onMounted, ref, type Ref } from 'vue';
|
import { onMounted, ref, type Ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const article: Ref<Article | void> = ref()
|
const article: Ref<Article | void> = ref()
|
||||||
const editorData: Ref<string> = ref('')
|
const editorData: Ref<string> = ref('')
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const successModalShow = ref(false)
|
||||||
|
const errorModalShow = ref(false)
|
||||||
|
const errorModalDescription: Ref<string> = ref("Quelque chose s'est mal passé...")
|
||||||
|
|
||||||
|
function displayErrorMessage(message: string): void {
|
||||||
|
errorModalDescription.value = message
|
||||||
|
errorModalShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getArticle(): Promise<Article | undefined> {
|
||||||
|
try {
|
||||||
|
return await fetchUniqueArticle(route.params.articleID as string)
|
||||||
|
} catch {
|
||||||
|
displayErrorMessage("Impossible de récupérer l'article. Vérifiez l'URL. Tentez-vous d'accéder au mode édition directement depuis un lien ?")
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const articleFetch = await getArticles(useRoute().params.articleID as string)
|
article.value = await getArticle()
|
||||||
article.value = articleFetch[0]
|
editorData.value = article.value?.content.html ?? ""
|
||||||
editorData.value = article.value.content.html
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function abort() {
|
function abort() {
|
||||||
|
router.push('/articles')
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveContent() {
|
function saveContent() {
|
||||||
console.log(editorData.value)
|
if (article.value == undefined) return;
|
||||||
|
|
||||||
|
article.value.content.html = editorData.value
|
||||||
|
|
||||||
|
sendArticleWithMethod(article.value, 'PUT')
|
||||||
|
.then(() => successModalShow.value = true)
|
||||||
|
.catch(error => displayErrorMessage(`Impossible de sauvegarder l'article. (${error.toString()})`))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -33,9 +60,14 @@ function saveContent() {
|
|||||||
</div>
|
</div>
|
||||||
<aside class="buttons">
|
<aside class="buttons">
|
||||||
<button @click="saveContent()" class="button-primary">Enregistrer</button>
|
<button @click="saveContent()" class="button-primary">Enregistrer</button>
|
||||||
<button @click="abort()" class="button-secondary">Annuler</button>
|
<button @click="abort()" class="button-secondary">Retour</button>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal v-if="errorModalShow" :description="errorModalDescription" @close="errorModalShow = false" type="error">
|
||||||
|
</Modal>
|
||||||
|
<Modal v-if="successModalShow" description="Le contenu a bien été sauvegardé." @close="successModalShow = false"
|
||||||
|
type="success">
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { postArticle, type Article } from '@/utils/database';
|
import { sendArticleWithMethod, type Article } from '@/utils/database';
|
||||||
import { ref, type Ref } from 'vue';
|
import { ref, type Ref } from 'vue';
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import { RouterLink, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ function isFormEmpty(): boolean {
|
|||||||
return title.value === '' || rawTags.value === ''
|
return title.value === '' || rawTags.value === ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function createArticle() {
|
async function createArticle(): Promise<Article> {
|
||||||
const article: Article = {
|
const article: Article = {
|
||||||
titleID: generateTitleID(title.value),
|
titleID: generateTitleID(title.value),
|
||||||
title: title.value,
|
title: title.value,
|
||||||
@@ -40,7 +40,12 @@ function createArticle() {
|
|||||||
online: false,
|
online: false,
|
||||||
tags: tags.value
|
tags: tags.value
|
||||||
}
|
}
|
||||||
postArticle(article)
|
await sendArticleWithMethod(article, 'POST')
|
||||||
|
return article
|
||||||
|
}
|
||||||
|
|
||||||
|
async function formSubmitHandler(): Promise<void> {
|
||||||
|
const article = await createArticle()
|
||||||
router.push(`/articles/edit/${article.titleID}`)
|
router.push(`/articles/edit/${article.titleID}`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -48,7 +53,7 @@ function createArticle() {
|
|||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<h2>Créer un nouvel article</h2>
|
<h2>Créer un nouvel article</h2>
|
||||||
<form @submit.prevent="createArticle()">
|
<form @submit.prevent="formSubmitHandler()">
|
||||||
<div class="inputs-group">
|
<div class="inputs-group">
|
||||||
<div class="label-input">
|
<div class="label-input">
|
||||||
<label for="title">Titre de l'article</label>
|
<label for="title">Titre de l'article</label>
|
||||||
|
|||||||
@@ -2,9 +2,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p>home page</p>
|
<main>
|
||||||
|
<h1>Bienvenue.</h1>
|
||||||
|
<h2>Vous êtes sur la page d'accueil de GohCMS.</h2>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
main {
|
||||||
|
padding: 64px;
|
||||||
|
color: var(--neutral-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
main h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { fileURLToPath, URL } from "node:url"
|
import { fileURLToPath, URL } from "node:url"
|
||||||
import { defineConfig, loadEnv } from "vite"
|
import { defineConfig, loadEnv, type UserConfig } from "vite"
|
||||||
import vue from "@vitejs/plugin-vue"
|
import vue from "@vitejs/plugin-vue"
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }): UserConfig => {
|
||||||
const env1 = loadEnv(mode, "./../../", "")
|
const env1 = loadEnv(mode, "./../../", "")
|
||||||
const env2 = loadEnv(mode, process.cwd(), "")
|
const env2 = loadEnv(mode, process.cwd(), "")
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: env1.APP_FRONT_PORT,
|
port: parseInt(env1.APP_FRONT_PORT),
|
||||||
},
|
},
|
||||||
base: env1.APP_BASE_FRONT_PATH ?? "/"
|
base: env1.APP_BASE_FRONT_PATH ?? "/"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user