Started to work the shortcodes
This commit is contained in:
@@ -3,6 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -137,14 +138,43 @@ type WebsiteSettings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Media struct {
|
type Media struct {
|
||||||
ID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"id"`
|
bun.BaseModel `bun:"table:media"`
|
||||||
AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"`
|
ID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"id"`
|
||||||
Author *User `bun:"rel:belongs-to,join:author_id=user_id" json:"author,omitempty"`
|
AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"`
|
||||||
Type string `bun:"media_type" json:"type"` // Image, Video, GIF etc. Add support for PDFs?
|
Author *User `bun:"rel:belongs-to,join:author_id=user_id" json:"author,omitempty"`
|
||||||
Alt string `bun:"media_alt" json:"alt"`
|
Type string `bun:"media_type" json:"type"` // Image, Video, GIF etc. Add support for PDFs?
|
||||||
Path string `bun:"media_path" json:"path"`
|
Alt string `bun:"media_alt" json:"alt"`
|
||||||
Size int64 `bun:"media_size" json:"size"`
|
Path string `bun:"media_path" json:"path"`
|
||||||
URL string `bun:"-" json:"url"`
|
Size int64 `bun:"media_size" json:"size"`
|
||||||
|
URL string `bun:"-" json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcodeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShortcodeMedia ShortcodeType = "media"
|
||||||
|
ShortcodeValue ShortcodeType = "value"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Shortcode struct {
|
||||||
|
bun.BaseModel `bun:"table:shortcodes,alias:sc"`
|
||||||
|
|
||||||
|
ID int64 `bun:"id,pk,autoincrement" json:"id"` // Primary key
|
||||||
|
Code string `bun:"code,notnull,unique" json:"code"` // The shortcode value
|
||||||
|
Type ShortcodeType `bun:"shortcode_type,notnull" json:"type"`
|
||||||
|
Value *string `bun:"value" json:"value,omitempty"`
|
||||||
|
MediaID *uuid.UUID `bun:"media_id,type:uuid" json:"media_id,omitempty"` // Nullable reference to another table's ID
|
||||||
|
Media *Media `bun:"rel:belongs-to,join:media_id=id" json:"media,omitempty"` // Relation to Media
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortcode) Validate() error {
|
||||||
|
if s.Value != nil && s.MediaID != nil {
|
||||||
|
return errors.New("both value and media_id cannot be set at the same time")
|
||||||
|
}
|
||||||
|
if s.Value == nil && s.MediaID == nil {
|
||||||
|
return errors.New("either value or media_id must be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitDatabase(dsn DSN) (*bun.DB, error) {
|
func InitDatabase(dsn DSN) (*bun.DB, error) {
|
||||||
@@ -164,6 +194,7 @@ func InitDatabase(dsn DSN) (*bun.DB, error) {
|
|||||||
_, err = db.NewCreateTable().Model((*Blog)(nil)).IfNotExists().Exec(ctx)
|
_, err = db.NewCreateTable().Model((*Blog)(nil)).IfNotExists().Exec(ctx)
|
||||||
_, err = db.NewCreateTable().Model((*WebsiteSettings)(nil)).IfNotExists().Exec(ctx)
|
_, err = db.NewCreateTable().Model((*WebsiteSettings)(nil)).IfNotExists().Exec(ctx)
|
||||||
_, err = db.NewCreateTable().Model((*Media)(nil)).IfNotExists().Exec(ctx)
|
_, err = db.NewCreateTable().Model((*Media)(nil)).IfNotExists().Exec(ctx)
|
||||||
|
_, err = db.NewCreateTable().Model((*Shortcode)(nil)).IfNotExists().Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
28
backend/api/delete_shortcode.go
Normal file
28
backend/api/delete_shortcode.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
core "fr.latosa-escrima/api/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleDeleteShortcode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code := r.PathValue("shortcode")
|
||||||
|
_, err := core.DB.NewDelete().
|
||||||
|
Model((*core.Shortcode)(nil)).
|
||||||
|
Where("code = ?", code).
|
||||||
|
Exec(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.JSONSuccess{
|
||||||
|
Status: core.Success,
|
||||||
|
Message: "Shortcode deleted",
|
||||||
|
}.Respond(w, http.StatusOK)
|
||||||
|
}
|
||||||
31
backend/api/get_shortcode.go
Normal file
31
backend/api/get_shortcode.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"fr.latosa-escrima/api/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleGetShortcode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code := r.PathValue("shortcode")
|
||||||
|
var shortcode core.Shortcode
|
||||||
|
err := core.DB.NewSelect().
|
||||||
|
Model(&shortcode).
|
||||||
|
Where("code = ?", code).
|
||||||
|
Limit(1).
|
||||||
|
Scan(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.JSONSuccess{
|
||||||
|
Status: core.Success,
|
||||||
|
Message: "Shortcode found",
|
||||||
|
Data: shortcode,
|
||||||
|
}.Respond(w, http.StatusOK)
|
||||||
|
}
|
||||||
26
backend/api/get_shortcodes.go
Normal file
26
backend/api/get_shortcodes.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"fr.latosa-escrima/api/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleGetShortcodes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var shortcodes []core.Shortcode
|
||||||
|
err := core.DB.NewSelect().Model(&shortcodes).Scan(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.JSONSuccess{
|
||||||
|
Status: core.Success,
|
||||||
|
Message: "Shortcodes retrieved.",
|
||||||
|
Data: shortcodes,
|
||||||
|
}.Respond(w, http.StatusOK)
|
||||||
|
}
|
||||||
44
backend/api/new_shortcode.go
Normal file
44
backend/api/new_shortcode.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"fr.latosa-escrima/api/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleCreateShortcode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var shortcode core.Shortcode
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&shortcode)
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = shortcode.Validate()
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = core.DB.NewInsert().Model(&shortcode).Exec(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.JSONSuccess{
|
||||||
|
Status: core.Success,
|
||||||
|
Message: "Shortcode inserted.",
|
||||||
|
}.Respond(w, http.StatusCreated)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -11,26 +10,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleCreateUser(w http.ResponseWriter, r *http.Request) {
|
func HandleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
core.JSONError{
|
|
||||||
Status: core.Error,
|
|
||||||
Message: "The body of your message is invalid.",
|
|
||||||
}.Respond(w, http.StatusNotAcceptable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("body : ", body )
|
|
||||||
var user core.User
|
var user core.User
|
||||||
err = json.Unmarshal(body, &user)
|
err := json.NewDecoder(r.Body).Decode(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.JSONError{
|
core.JSONError{
|
||||||
Status: core.Error,
|
Status: core.Error,
|
||||||
Message: "It seems your body in invalid JSON.",
|
Message: err.Error(),
|
||||||
}.Respond(w, http.StatusNotAcceptable)
|
}.Respond(w, http.StatusNotAcceptable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("User : ", user)
|
log.Println("User : ", user)
|
||||||
|
|
||||||
res, err := user.Insert(context.Background())
|
res, err := user.Insert(context.Background())
|
||||||
|
|||||||
71
backend/api/update_shortcode.go
Normal file
71
backend/api/update_shortcode.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
core "fr.latosa-escrima/api/core"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateShortcodeArgs struct {
|
||||||
|
Code *string `json:"code"` // The shortcode value
|
||||||
|
Type *core.ShortcodeType `json:"type"`
|
||||||
|
Value *string `json:"value"`
|
||||||
|
MediaID *uuid.UUID `json:"media_id"` // Nullable reference to another table's ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleUpdateShortcode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var updateArgs UpdateShortcodeArgs
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&updateArgs)
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortcode core.Shortcode
|
||||||
|
updateQuery := core.DB.NewUpdate().Model(&shortcode)
|
||||||
|
val := reflect.ValueOf(updateArgs)
|
||||||
|
typ := reflect.TypeOf(updateArgs)
|
||||||
|
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := val.Field(i)
|
||||||
|
|
||||||
|
tag := typ.Field(i).Tag.Get("bun")
|
||||||
|
if tag == "" {
|
||||||
|
tag = typ.Field(i).Tag.Get("json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add fields that are non-nil and non-zero
|
||||||
|
if field.IsValid() && !field.IsNil() && !field.IsZero() {
|
||||||
|
updateQuery.Set(fmt.Sprintf("%s = ?", strings.Split(tag, ",")[0]), field.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.PathValue("shortcode")
|
||||||
|
_, err = updateQuery.
|
||||||
|
Where("code = ?", code).
|
||||||
|
Returning("*").
|
||||||
|
Exec(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
core.JSONError{
|
||||||
|
Status: core.Error,
|
||||||
|
Message: err.Error(),
|
||||||
|
}.Respond(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.JSONSuccess{
|
||||||
|
Status: core.Success,
|
||||||
|
Message: "Shortcode updated.",
|
||||||
|
Data: shortcode,
|
||||||
|
}.Respond(w, http.StatusOK)
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,15 +23,7 @@ type UpdateUserArgs struct {
|
|||||||
|
|
||||||
func HandleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
func HandleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
var updateArgs UpdateUserArgs
|
var updateArgs UpdateUserArgs
|
||||||
body, err := io.ReadAll(r.Body)
|
err := json.NewDecoder(r.Body).Decode(&updateArgs)
|
||||||
if err != nil {
|
|
||||||
core.JSONError{
|
|
||||||
Status: core.Error,
|
|
||||||
Message: err.Error(),
|
|
||||||
}.Respond(w, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(body, &updateArgs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
core.JSONError{
|
core.JSONError{
|
||||||
Status: core.Error,
|
Status: core.Error,
|
||||||
|
|||||||
@@ -143,6 +143,26 @@ func main() {
|
|||||||
Handler: api.HandleDeleteMedia,
|
Handler: api.HandleDeleteMedia,
|
||||||
Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
|
Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
|
||||||
},
|
},
|
||||||
|
"/shortcodes/new": {
|
||||||
|
Handler: api.HandleCreateShortcode,
|
||||||
|
Middlewares: []core.Middleware{api.Methods("POST"), api.AuthJWT},
|
||||||
|
},
|
||||||
|
"/shortcodes/": {
|
||||||
|
Handler: api.HandleGetShortcodes,
|
||||||
|
Middlewares: []core.Middleware{api.Methods("GET"), api.AuthJWT},
|
||||||
|
},
|
||||||
|
"/shortcodes/{shortcode}": {
|
||||||
|
Handler: api.HandleGetShortcode,
|
||||||
|
Middlewares: []core.Middleware{api.Methods("GET")},
|
||||||
|
},
|
||||||
|
"/shortcodes/{shortcode}/delete": {
|
||||||
|
Handler: api.HandleDeleteShortcode,
|
||||||
|
Middlewares: []core.Middleware{api.Methods("DELETE"), api.AuthJWT},
|
||||||
|
},
|
||||||
|
"/shortcodes/{shortcode}/update": {
|
||||||
|
Handler: api.HandleUpdateShortcode,
|
||||||
|
Middlewares: []core.Middleware{api.Methods("PATCH"), api.AuthJWT},
|
||||||
|
},
|
||||||
"/contact": {
|
"/contact": {
|
||||||
Handler: api.HandleContact,
|
Handler: api.HandleContact,
|
||||||
Middlewares: []core.Middleware{api.Methods("POST"), CSRFMiddleware},
|
Middlewares: []core.Middleware{api.Methods("POST"), CSRFMiddleware},
|
||||||
|
|||||||
65
frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx
Normal file
65
frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShortcodeTable } from "@/components/shortcodes-table";
|
||||||
|
import type IShortcode from "@/interfaces/IShortcode";
|
||||||
|
import { request, useApi } from "@/hooks/use-api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
export default function ShortcodesPage() {
|
||||||
|
const {
|
||||||
|
data: shortcodes,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
mutate,
|
||||||
|
success,
|
||||||
|
} = useApi<IShortcode[]>("/shortcodes", undefined, true);
|
||||||
|
|
||||||
|
const handleUpdate = async (updatedShortcode: IShortcode) => {
|
||||||
|
const res = await request<IShortcode>(
|
||||||
|
`/shortcodes/${updatedShortcode.code}/update`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
requiresAuth: true,
|
||||||
|
body: updatedShortcode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
mutate();
|
||||||
|
// Implement update logic here
|
||||||
|
console.log("Update shortcode:", updatedShortcode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (code: string) => {
|
||||||
|
const res = await request<undefined>(`/shortcodes/${code}/delete`, {
|
||||||
|
requiresAuth: true,
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = async (newShortcode: Omit<IShortcode, "id">) => {
|
||||||
|
const res = await request<IShortcode>(`/shortcodes/new`, {
|
||||||
|
body: newShortcode,
|
||||||
|
method: "POST",
|
||||||
|
requiresAuth: true,
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-10">
|
||||||
|
<h1 className="text-2xl font-bold mb-5">Shortcodes</h1>
|
||||||
|
{isLoading && (
|
||||||
|
<Loader2 className="flex w-full min-w-0 flex-col gap-1 justify-center animate-spin" />
|
||||||
|
)}
|
||||||
|
{error && <p>{error}</p>}
|
||||||
|
<ShortcodeTable
|
||||||
|
shortcodes={shortcodes ?? []}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onAdd={handleAdd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -95,7 +95,12 @@ const data = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Media",
|
title: "Media",
|
||||||
url: "/dashboard/media",
|
url: "/dashboard/settings/media",
|
||||||
|
icon: Camera,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Shortcodes",
|
||||||
|
url: "/dashboard/settings/shortcodes",
|
||||||
icon: Camera,
|
icon: Camera,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
"use client";
|
||||||
import { Book, Menu, Sunset, Trees, Zap } from "lucide-react";
|
import { Book, Menu, Sunset, Trees, Zap } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -18,6 +19,8 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { deleteCookie, getCookie } from "cookies-next";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const subMenuItemsOne = [
|
const subMenuItemsOne = [
|
||||||
{
|
{
|
||||||
@@ -67,6 +70,11 @@ const subMenuItemsTwo = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
|
const [cookie, setCookie] = useState<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const _cookie = getCookie("auth_token");
|
||||||
|
setCookie(_cookie?.toString() ?? null);
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<section className="sticky top-0 z-50 bg-background p-4">
|
<section className="sticky top-0 z-50 bg-background p-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -133,11 +141,26 @@ const Navbar = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 animate-in ease-in-out">
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Link href="/login">Se connecter</Link>
|
{cookie ? (
|
||||||
|
<Link href="/dashboard">Compte</Link>
|
||||||
|
) : (
|
||||||
|
<Link href="/login">Se connecter</Link>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button>Sign up</Button>
|
{cookie ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteCookie("auth_token");
|
||||||
|
setCookie(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Se déconnecter
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>Créer un compte</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="block lg:hidden">
|
<div className="block lg:hidden">
|
||||||
|
|||||||
120
frontend/components/shortcode-dialogue.tsx
Normal file
120
frontend/components/shortcode-dialogue.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import type IShortcode from "@/interfaces/IShortcode";
|
||||||
|
|
||||||
|
interface ShortcodeDialogProps {
|
||||||
|
onSave: (shortcode: Omit<IShortcode, "id">) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShortcodeDialog({ onSave }: ShortcodeDialogProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const [type, setType] = useState<"value" | "media">("value");
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const [mediaId, setMediaId] = useState("");
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave({ code, type, value, media_id: mediaId });
|
||||||
|
setOpen(false);
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setCode("");
|
||||||
|
setType("value");
|
||||||
|
setValue("");
|
||||||
|
setMediaId("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Add New Shortcode</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Shortcode</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new shortcode here. Click save when you're
|
||||||
|
done.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="code" className="text-right">
|
||||||
|
Code
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="code"
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tabs
|
||||||
|
defaultValue={type}
|
||||||
|
onValueChange={(v) => setType(v as "value" | "media")}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="value">Value</TabsTrigger>
|
||||||
|
<TabsTrigger value="media">Media</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="value">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="value" className="text-right">
|
||||||
|
Value
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="value"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="media">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="mediaId" className="text-right">
|
||||||
|
Media ID
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="mediaId"
|
||||||
|
value={mediaId}
|
||||||
|
onChange={(e) => setMediaId(e.target.value)}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" onClick={handleSave}>
|
||||||
|
Save shortcode
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
frontend/components/shortcodes-table.tsx
Normal file
102
frontend/components/shortcodes-table.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { MoreHorizontal } from "lucide-react";
|
||||||
|
import type IShortcode from "@/interfaces/IShortcode";
|
||||||
|
import ShortcodeDialog from "@/components/shortcode-dialogue";
|
||||||
|
|
||||||
|
interface ShortcodeTableProps {
|
||||||
|
shortcodes: IShortcode[];
|
||||||
|
onUpdate: (shortcode: IShortcode) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onAdd: (shortcode: Omit<IShortcode, "id">) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShortcodeTable({
|
||||||
|
shortcodes,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
onAdd,
|
||||||
|
}: ShortcodeTableProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<ShortcodeDialog onSave={onAdd} />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Code</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Value</TableHead>
|
||||||
|
<TableHead>Media ID</TableHead>
|
||||||
|
<TableHead className="w-[80px]">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{shortcodes.map((shortcode) => (
|
||||||
|
<TableRow key={shortcode.id}>
|
||||||
|
<TableCell>{shortcode.id}</TableCell>
|
||||||
|
<TableCell>{shortcode.code}</TableCell>
|
||||||
|
<TableCell>{shortcode.type}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{shortcode.value || "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{shortcode.media_id || "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
Open menu
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
onUpdate(shortcode)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
onDelete(shortcode.code)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
frontend/components/ui/table.tsx
Normal file
120
frontend/components/ui/table.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
));
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ export interface ApiResponse<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function request<T>(
|
export async function request<T>(
|
||||||
url: string,
|
endpoint: string,
|
||||||
options: {
|
options: {
|
||||||
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
||||||
body?: any;
|
body?: any;
|
||||||
@@ -39,7 +39,7 @@ export async function request<T>(
|
|||||||
headers.Authorization = `Bearer ${authToken}`;
|
headers.Authorization = `Bearer ${authToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}${url}`, {
|
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
|||||||
10
frontend/interfaces/IShortcode.ts
Normal file
10
frontend/interfaces/IShortcode.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Media from "./Media";
|
||||||
|
|
||||||
|
export default interface IShortcode {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
type: string;
|
||||||
|
value?: string;
|
||||||
|
media_id?: string;
|
||||||
|
media?: Media;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user