create and get categories with database for user
Build and Test / build-and-test-backend (push) Successful in 31s
Build and Test / build-and-test-frontend (push) Successful in 34s

This commit is contained in:
wt committed 2025-08-09 17:32:18 +07:00
1 parent 2c06975a43
commit 90e5863adc
12 files changed
+189 -79

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
package api
import (
"backend/database"
"backend/model"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func CreateCategory(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
userid := claims["id"].(string)
requestdata := new(model.Category)
fmt.Println(string(c.Body()))
if err := c.BodyParser(requestdata); err != nil {
return c.Status(
fiber.StatusBadRequest,
).JSON(
fiber.Map{
"error": "Cannot parse request body",
},
)
}
fmt.Println(requestdata)
requestdata.UserID = userid
err := database.CreateCategory(requestdata)
if err != nil {
return c.Status(
fiber.StatusInternalServerError,
).JSON(
fiber.Map{
"error": err,
},
)
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
}
+5 -1
View File
@@ -4,10 +4,14 @@ import (
"backend/database" "backend/database"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
) )
func GetAllCategories(c *fiber.Ctx) error { func GetAllCategories(c *fiber.Ctx) error {
categories, err := database.GetAllCategories() user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
userid := claims["id"].(string)
categories, err := database.GetAllCategories(userid)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
+5 -3
View File
@@ -13,7 +13,9 @@ import (
func CreateCategory(category *model.Category) error { func CreateCategory(category *model.Category) error {
e := Db.Where(model.Category{ e := Db.Where(model.Category{
Title: category.Title, Title: category.Title,
UserID: category.UserID,
Type: category.Type,
}).FirstOrCreate(&category).Error }).FirstOrCreate(&category).Error
if e != nil { if e != nil {
return e return e
@@ -75,9 +77,9 @@ func GetCategoriesFromFile(filename string) []model.Category {
return res return res
} }
func GetAllCategories() ([]model.Category, error) { func GetAllCategories(userid string) ([]model.Category, error) {
var categories []model.Category var categories []model.Category
e := Db.Find(&categories).Error e := Db.Where(model.Category{UserID: userid}).Find(&categories).Error
if e != nil { if e != nil {
return nil, e return nil, e
} }
+10 -7
View File
@@ -41,13 +41,16 @@ func InitDatabase() {
allusers := GetUsersFromFile(os.Getenv("USERS_FILE")) allusers := GetUsersFromFile(os.Getenv("USERS_FILE"))
for i := range allusers { for i := range allusers {
CreateUser(&allusers[i]) CreateUser(&allusers[i])
} if os.Getenv("DEFAULT_CATEGORIES_FILE") != "" {
} default_categories := GetCategoriesFromFile(os.Getenv("DEFAULT_CATEGORIES_FILE"))
for j := range default_categories {
if os.Getenv("DEFAULT_CATEGORIES_FILE") != "" { default_categories[j].UserID = allusers[i].ID
default_categories := GetCategoriesFromFile(os.Getenv("DEFAULT_CATEGORIES_FILE")) for k := range default_categories[j].Subcategories {
for i := range default_categories { default_categories[j].Subcategories[k].UserID = allusers[i].ID
CreateCategory(&default_categories[i]) }
CreateCategory(&default_categories[j])
}
}
} }
} }
} }
+29 -29
View File
@@ -1,36 +1,36 @@
package internaljwt package internaljwt
import ( // import (
"backend/model" // "backend/model"
"github.com/gofiber/fiber/v2" // "github.com/gofiber/fiber/v2"
) // )
func JwtFromBody(c *fiber.Ctx) error { // func JwtFromBody(c *fiber.Ctx) error {
req := new(model.TokenRequest) // req := new(model.TokenRequest)
if err := c.BodyParser(req); err != nil { // if err := c.BodyParser(req); err != nil {
return c.Status( // return c.Status(
fiber.StatusBadRequest, // fiber.StatusBadRequest,
).JSON( // ).JSON(
fiber.Map{ // fiber.Map{
"error": "Cannot parse request body", // "error": "Cannot parse request body",
}, // },
) // )
} // }
if req.Token == "" { // if req.Token == "" {
return c.Status( // return c.Status(
fiber.StatusBadRequest, // fiber.StatusBadRequest,
).JSON( // ).JSON(
fiber.Map{ // fiber.Map{
"error": "Token not found in request body", // "error": "Token not found in request body",
}, // },
) // )
} // }
c.Request().Header.Set( // c.Request().Header.Set(
"Authorization", "Bearer "+req.Token, // "Authorization", "Bearer "+req.Token,
) // )
return c.Next() // return c.Next()
} // }
+1 -1
View File
@@ -4,7 +4,7 @@ type Category struct {
BaseModel BaseModel
Icon string `json:"icon"` Icon string `json:"icon"`
Title string `json:"title"` Title string `json:"title"`
Subcategories []Subcategory `json:"subcategories" gorm:"foreignKey:CategoryID"` Subcategories []Subcategory `json:"subcategories,omitempty" gorm:"foreignKey:CategoryID"`
Type string `json:"type"` Type string `json:"type"`
UserID string UserID string
} }
+25 -25
View File
@@ -3,7 +3,6 @@ package server
import ( import (
"backend/api" "backend/api"
"backend/database" "backend/database"
"backend/internal/internaljwt"
"backend/utils" "backend/utils"
"os" "os"
@@ -25,33 +24,33 @@ func Configure(app *fiber.App) {
apiGroup := app.Group("/api") apiGroup := app.Group("/api")
apiGroup.Post("/token", api.Login) apiGroup.Post("/token", api.Login)
app.Use(internaljwt.JwtFromBody, jwtware.New(jwtware.Config{ app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{ SigningKey: jwtware.SigningKey{
Key: []byte(os.Getenv("JWT_SECRET_KEY")), Key: []byte(os.Getenv("JWT_SECRET_KEY")),
}, },
ErrorHandler: func(c *fiber.Ctx, err error) error { // ErrorHandler: func(c *fiber.Ctx, err error) error {
if err.Error() == "Missing or malformed JWT" { // if err.Error() == "Missing or malformed JWT" {
return c.Status( // return c.Status(
fiber.StatusBadRequest, // fiber.StatusBadRequest,
).JSON( // ).JSON(
fiber.Map{ // fiber.Map{
"status": "error", // "status": "error",
"message": "Missing or malformed JWT", // "message": "Missing or malformed JWT",
"data": nil, // "data": nil,
}, // },
) // )
} // }
return c.Status( // return c.Status(
fiber.StatusUnauthorized, // fiber.StatusUnauthorized,
).JSON( // ).JSON(
fiber.Map{ // fiber.Map{
"status": "error", // "status": "error",
"message": "Invalid or expired JWT", // "message": "Invalid or expired JWT",
"data": nil, // "data": nil,
"error": err.Error(), // "error": err.Error(),
}, // },
) // )
}, // },
})) }))
usersGroup := apiGroup.Group("/users") usersGroup := apiGroup.Group("/users")
@@ -63,4 +62,5 @@ func Configure(app *fiber.App) {
categoryGroup := apiGroup.Group("/category") categoryGroup := apiGroup.Group("/category")
categoryGroup.Get("/", api.GetAllCategories) categoryGroup.Get("/", api.GetAllCategories)
categoryGroup.Post("/", api.CreateCategory)
} }
@@ -0,0 +1,20 @@
import routes from "../../routes";
const CreateCategory = async (category) => {
try {
const response = await fetch(routes.categories, {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json",
},
body: JSON.stringify(category),
});
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching categories:", error);
return [];
}
};
export default CreateCategory;
@@ -2,13 +2,34 @@ import { useState } from "react";
import { useTheme } from "../../contexts/ThemeContext"; import { useTheme } from "../../contexts/ThemeContext";
import { Modal, Button, Card, Form } from "react-bootstrap"; import { Modal, Button, Card, Form } from "react-bootstrap";
import EmojiPickerButton from "./components/EmojiPickerButton/EmojiPickerButton"; import EmojiPickerButton from "./components/EmojiPickerButton/EmojiPickerButton";
import CreateCategory from "../../logic/category/CreateCategory";
const AddCategoryModal = ({ variant, ...props }) => { const AddCategoryModal = ({
variant,
type,
showParent,
closeParent,
...props
}) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const handleClose = () => setShow(false); const handleClose = () => setShow(false);
const handleShow = () => setShow(true); const handleShow = () => setShow(true);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [emojivalue, setEmojivate] = useState("😀");
const createCategory = async () => {
closeParent();
console.log({ title: title, icon: emojivalue, type: type });
var result = await CreateCategory({
title: title,
icon: emojivalue,
type: type,
});
if (result.status === "ok") {
setShow(false);
showParent();
}
};
return ( return (
<> <>
<Button variant={theme} onClick={handleShow} className="m-0" {...props}> <Button variant={theme} onClick={handleShow} className="m-0" {...props}>
@@ -34,7 +55,10 @@ const AddCategoryModal = ({ variant, ...props }) => {
</Modal.Header> </Modal.Header>
<Modal.Body className="p-3"> <Modal.Body className="p-3">
<div className="d-flex align-items-center justify-content-center"> <div className="d-flex align-items-center justify-content-center">
<EmojiPickerButton /> <EmojiPickerButton
inputValue={emojivalue}
setInputValue={setEmojivate}
/>
</div> </div>
<Form.Control <Form.Control
type="text" type="text"
@@ -59,7 +83,9 @@ const AddCategoryModal = ({ variant, ...props }) => {
Add subcategory Add subcategory
</Button> </Button>
<spacer /> <spacer />
<Button className="w-100">Add</Button> <Button className="w-100" onClick={() => createCategory()}>
Add
</Button>
</Card.Body> </Card.Body>
</Card> </Card>
</Modal.Body> </Modal.Body>
@@ -3,10 +3,10 @@ import { Button, Overlay, Popover, InputGroup } from "react-bootstrap";
import EmojiPicker from "emoji-picker-react"; import EmojiPicker from "emoji-picker-react";
import { useTheme } from "../../../../contexts/ThemeContext"; import { useTheme } from "../../../../contexts/ThemeContext";
const EmojiPickerButton = () => { const EmojiPickerButton = ({ inputValue, setInputValue }) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [showPicker, setShowPicker] = useState(false); const [showPicker, setShowPicker] = useState(false);
const [inputValue, setInputValue] = useState("😀"); // const [inputValue, setInputValue] = useState("😀");
const target = useRef(null); const target = useRef(null);
const handleEmojiSelect = (emoji) => { const handleEmojiSelect = (emoji) => {
@@ -21,6 +21,7 @@ const CategoriesModal = ({ variant }) => {
const { theme } = useTheme(); const { theme } = useTheme();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [categoriestype, setCategoriestype] = useState("expense");
const handleClose = () => setShow(false); const handleClose = () => setShow(false);
const handleShow = () => setShow(true); const handleShow = () => setShow(true);
useEffect(() => { useEffect(() => {
@@ -33,7 +34,7 @@ const CategoriesModal = ({ variant }) => {
); );
console.log(data); console.log(data);
}); });
}, []); }, [show]);
const filteredExpenseCategories = useMemo(() => { const filteredExpenseCategories = useMemo(() => {
if (!searchTerm) return expenseCategories; if (!searchTerm) return expenseCategories;
const term = searchTerm.toLowerCase(); const term = searchTerm.toLowerCase();
@@ -80,6 +81,9 @@ const CategoriesModal = ({ variant }) => {
<AddCategoryModal <AddCategoryModal
className="w-100" className="w-100"
style={{ paddingLeft: "0" }} style={{ paddingLeft: "0" }}
type={categoriestype}
showParent={handleShow}
closeParent={handleClose}
/> />
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item>Reorder</Dropdown.Item> <Dropdown.Item>Reorder</Dropdown.Item>
@@ -101,18 +105,22 @@ const CategoriesModal = ({ variant }) => {
</Modal.Header> </Modal.Header>
<Modal.Body className="p-0"> <Modal.Body className="p-0">
<Tabs <Tabs
defaultActiveKey="Expenses" defaultActiveKey="expense"
id="categories-tabs" id="categories-tabs"
className="nav nav-tabs" className="nav nav-tabs"
onSelect={(selectedKey) => {
setCategoriestype(selectedKey);
console.log(selectedKey, categoriestype);
}}
> >
<Tab eventKey="Incomes" title="Incomes"> <Tab eventKey="income" title="Incomes">
<div <div
className={styles.scrollableContent} className={styles.scrollableContent}
style={{ padding: "10px" }} style={{ padding: "10px" }}
> >
{filteredIncomeCategories.map((category) => ( {filteredIncomeCategories.map((category) => (
<CategorySign <CategorySign
key={`${category.id}/expense`} key={`${category.id}`}
icon={category.icon} icon={category.icon}
title={category.title} title={category.title}
subcategories={ subcategories={
@@ -122,14 +130,14 @@ const CategoriesModal = ({ variant }) => {
))} ))}
</div> </div>
</Tab> </Tab>
<Tab eventKey="Expenses" title="Expenses"> <Tab eventKey="expense" title="Expenses">
<div <div
className={styles.scrollableContent} className={styles.scrollableContent}
style={{ padding: "10px" }} style={{ padding: "10px" }}
> >
{filteredExpenseCategories.map((category) => ( {filteredExpenseCategories.map((category) => (
<CategorySign <CategorySign
key={`${category.id}/income`} key={`${category.id}`}
icon={category.icon} icon={category.icon}
title={category.title} title={category.title}
subcategories={ subcategories={
@@ -45,11 +45,18 @@ const CategorySign = ({ icon, title, subcategories }) => {
> >
<div>&#10142;</div> <div>&#10142;</div>
</div> </div>
<div className={classNames("d-flex gap-3", styles.hideScrollbar)}> <div
className={classNames("d-flex gap-3", styles.hideScrollbar)}
style={{ overflowX: "auto" }}
>
{subcategories.map((subcategory) => ( {subcategories.map((subcategory) => (
<div <div
className="btn btn-secondary" className="btn btn-secondary"
style={{ borderRadius: "25px", padding: "0 15px" }} style={{
borderRadius: "25px",
padding: "0 15px",
textWrap: "nowrap",
}}
key={subcategory.id} key={subcategory.id}
> >
{subcategory.title} {subcategory.title}