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