diff --git a/backend/api/core/schemas.go b/backend/api/core/schemas.go index 464bb55..5660a58 100644 --- a/backend/api/core/schemas.go +++ b/backend/api/core/schemas.go @@ -3,6 +3,7 @@ package core import ( "context" "database/sql" + "errors" "fmt" "time" @@ -137,14 +138,43 @@ type WebsiteSettings struct { } type Media struct { - ID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"id"` - AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"` - Author *User `bun:"rel:belongs-to,join:author_id=user_id" json:"author,omitempty"` - Type string `bun:"media_type" json:"type"` // Image, Video, GIF etc. Add support for PDFs? - Alt string `bun:"media_alt" json:"alt"` - Path string `bun:"media_path" json:"path"` - Size int64 `bun:"media_size" json:"size"` - URL string `bun:"-" json:"url"` + bun.BaseModel `bun:"table:media"` + ID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"id"` + AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"` + Author *User `bun:"rel:belongs-to,join:author_id=user_id" json:"author,omitempty"` + Type string `bun:"media_type" json:"type"` // Image, Video, GIF etc. Add support for PDFs? + Alt string `bun:"media_alt" json:"alt"` + Path string `bun:"media_path" json:"path"` + 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) { @@ -164,6 +194,7 @@ func InitDatabase(dsn DSN) (*bun.DB, error) { _, err = db.NewCreateTable().Model((*Blog)(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((*Shortcode)(nil)).IfNotExists().Exec(ctx) if err != nil { return nil, err } diff --git a/backend/api/delete_shortcode.go b/backend/api/delete_shortcode.go new file mode 100644 index 0000000..c6a8346 --- /dev/null +++ b/backend/api/delete_shortcode.go @@ -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) +} diff --git a/backend/api/get_shortcode.go b/backend/api/get_shortcode.go new file mode 100644 index 0000000..1fdcc72 --- /dev/null +++ b/backend/api/get_shortcode.go @@ -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) +} diff --git a/backend/api/get_shortcodes.go b/backend/api/get_shortcodes.go new file mode 100644 index 0000000..2c48c7f --- /dev/null +++ b/backend/api/get_shortcodes.go @@ -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) +} diff --git a/backend/api/new_shortcode.go b/backend/api/new_shortcode.go new file mode 100644 index 0000000..471e8a6 --- /dev/null +++ b/backend/api/new_shortcode.go @@ -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) + +} diff --git a/backend/api/new_user.go b/backend/api/new_user.go index 6a94861..7c35ad0 100644 --- a/backend/api/new_user.go +++ b/backend/api/new_user.go @@ -3,7 +3,6 @@ package api import ( "context" "encoding/json" - "io" "log" "net/http" @@ -11,26 +10,15 @@ import ( ) 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 - err = json.Unmarshal(body, &user) + err := json.NewDecoder(r.Body).Decode(&user) if err != nil { core.JSONError{ Status: core.Error, - Message: "It seems your body in invalid JSON.", + Message: err.Error(), }.Respond(w, http.StatusNotAcceptable) return } - log.Println("User : ", user) res, err := user.Insert(context.Background()) diff --git a/backend/api/update_shortcode.go b/backend/api/update_shortcode.go new file mode 100644 index 0000000..ab699a3 --- /dev/null +++ b/backend/api/update_shortcode.go @@ -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) +} diff --git a/backend/api/update_user.go b/backend/api/update_user.go index 3d7cd84..9d2397e 100644 --- a/backend/api/update_user.go +++ b/backend/api/update_user.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "reflect" "strings" @@ -24,15 +23,7 @@ type UpdateUserArgs struct { func HandleUpdateUser(w http.ResponseWriter, r *http.Request) { var updateArgs UpdateUserArgs - body, err := io.ReadAll(r.Body) - if err != nil { - core.JSONError{ - Status: core.Error, - Message: err.Error(), - }.Respond(w, http.StatusInternalServerError) - return - } - err = json.Unmarshal(body, &updateArgs) + err := json.NewDecoder(r.Body).Decode(&updateArgs) if err != nil { core.JSONError{ Status: core.Error, diff --git a/backend/main.go b/backend/main.go index e27b64e..7cc078e 100644 --- a/backend/main.go +++ b/backend/main.go @@ -143,6 +143,26 @@ func main() { Handler: api.HandleDeleteMedia, 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": { Handler: api.HandleContact, Middlewares: []core.Middleware{api.Methods("POST"), CSRFMiddleware}, diff --git a/frontend/app/(auth)/dashboard/media/old.tsx b/frontend/app/(auth)/dashboard/settings/media/old.tsx similarity index 100% rename from frontend/app/(auth)/dashboard/media/old.tsx rename to frontend/app/(auth)/dashboard/settings/media/old.tsx diff --git a/frontend/app/(auth)/dashboard/media/page.tsx b/frontend/app/(auth)/dashboard/settings/media/page.tsx similarity index 100% rename from frontend/app/(auth)/dashboard/media/page.tsx rename to frontend/app/(auth)/dashboard/settings/media/page.tsx diff --git a/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx b/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx new file mode 100644 index 0000000..03fe4cd --- /dev/null +++ b/frontend/app/(auth)/dashboard/settings/shortcodes/page.tsx @@ -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("/shortcodes", undefined, true); + + const handleUpdate = async (updatedShortcode: IShortcode) => { + const res = await request( + `/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(`/shortcodes/${code}/delete`, { + requiresAuth: true, + method: "DELETE", + }); + mutate(); + }; + + const handleAdd = async (newShortcode: Omit) => { + const res = await request(`/shortcodes/new`, { + body: newShortcode, + method: "POST", + requiresAuth: true, + }); + console.log(res); + mutate(); + }; + + return ( +
+

Shortcodes

+ {isLoading && ( + + )} + {error &&

{error}

} + +
+ ); +} diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index fa9978b..dd6266a 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -95,7 +95,12 @@ const data = { items: [ { title: "Media", - url: "/dashboard/media", + url: "/dashboard/settings/media", + icon: Camera, + }, + { + title: "Shortcodes", + url: "/dashboard/settings/shortcodes", icon: Camera, }, ], diff --git a/frontend/components/nav-bar.tsx b/frontend/components/nav-bar.tsx index 491683c..6fee23e 100644 --- a/frontend/components/nav-bar.tsx +++ b/frontend/components/nav-bar.tsx @@ -1,3 +1,4 @@ +"use client"; import { Book, Menu, Sunset, Trees, Zap } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -18,6 +19,8 @@ import { SheetTrigger, } from "@/components/ui/sheet"; import Link from "next/link"; +import { deleteCookie, getCookie } from "cookies-next"; +import { useEffect, useState } from "react"; const subMenuItemsOne = [ { @@ -67,6 +70,11 @@ const subMenuItemsTwo = [ ]; const Navbar = () => { + const [cookie, setCookie] = useState(null); + useEffect(() => { + const _cookie = getCookie("auth_token"); + setCookie(_cookie?.toString() ?? null); + }, []); return (
@@ -133,11 +141,26 @@ const Navbar = () => {
-
+
- + {cookie ? ( + + ) : ( + + )}
diff --git a/frontend/components/shortcode-dialogue.tsx b/frontend/components/shortcode-dialogue.tsx new file mode 100644 index 0000000..c80a401 --- /dev/null +++ b/frontend/components/shortcode-dialogue.tsx @@ -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) => 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 ( + + + + + + + Add New Shortcode + + Create a new shortcode here. Click save when you're + done. + + +
+
+ + setCode(e.target.value)} + className="col-span-3" + /> +
+ setType(v as "value" | "media")} + className="w-full" + > + + Value + Media + + +
+ + setValue(e.target.value)} + className="col-span-3" + /> +
+
+ +
+ + setMediaId(e.target.value)} + className="col-span-3" + /> +
+
+
+
+ + + +
+
+ ); +} diff --git a/frontend/components/shortcodes-table.tsx b/frontend/components/shortcodes-table.tsx new file mode 100644 index 0000000..ff6a3e9 --- /dev/null +++ b/frontend/components/shortcodes-table.tsx @@ -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) => void; +} + +export function ShortcodeTable({ + shortcodes, + onUpdate, + onDelete, + onAdd, +}: ShortcodeTableProps) { + return ( +
+
+ +
+
+ + + + ID + Code + Type + Value + Media ID + Actions + + + + {shortcodes.map((shortcode) => ( + + {shortcode.id} + {shortcode.code} + {shortcode.type} + + {shortcode.value || "N/A"} + + + {shortcode.media_id || "N/A"} + + + + + + + + + onUpdate(shortcode) + } + > + Update + + + onDelete(shortcode.code) + } + > + Delete + + + + + + ))} + +
+
+
+ ); +} diff --git a/frontend/components/ui/table.tsx b/frontend/components/ui/table.tsx new file mode 100644 index 0000000..38ac746 --- /dev/null +++ b/frontend/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className, + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/frontend/hooks/use-api.tsx b/frontend/hooks/use-api.tsx index a8a3150..e531136 100644 --- a/frontend/hooks/use-api.tsx +++ b/frontend/hooks/use-api.tsx @@ -11,7 +11,7 @@ export interface ApiResponse { } export async function request( - url: string, + endpoint: string, options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: any; @@ -39,7 +39,7 @@ export async function request( headers.Authorization = `Bearer ${authToken}`; } - const response = await fetch(`${API_URL}${url}`, { + const response = await fetch(`${API_URL}${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : undefined, diff --git a/frontend/interfaces/IShortcode.ts b/frontend/interfaces/IShortcode.ts new file mode 100644 index 0000000..c16dfe9 --- /dev/null +++ b/frontend/interfaces/IShortcode.ts @@ -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; +}