Add bio management feature with UI and backend support

This commit is contained in:
2025-05-17 21:34:16 +08:00
parent 864632e682
commit 38999d844d
8 changed files with 136 additions and 8 deletions

View File

@@ -148,6 +148,7 @@ export const i18nData = {
"Create Tag": "Create Tag", "Create Tag": "Create Tag",
"Search Tags": "Search Tags", "Search Tags": "Search Tags",
"Edit Resource": "Edit Resource", "Edit Resource": "Edit Resource",
"Change Bio": "Change Bio",
} }
}, },
"zh-CN": { "zh-CN": {
@@ -299,6 +300,7 @@ export const i18nData = {
"Create Tag": "创建标签", "Create Tag": "创建标签",
"Search Tags": "搜索标签", "Search Tags": "搜索标签",
"Edit Resource": "编辑资源", "Edit Resource": "编辑资源",
"Change Bio": "更改个人简介",
} }
}, },
"zh-TW": { "zh-TW": {
@@ -450,6 +452,7 @@ export const i18nData = {
"Create Tag": "創建標籤", "Create Tag": "創建標籤",
"Search Tags": "搜尋標籤", "Search Tags": "搜尋標籤",
"Edit Resource": "編輯資源", "Edit Resource": "編輯資源",
"Change Bio": "更改個人簡介",
} }
} }
} }

View File

@@ -7,6 +7,7 @@ export interface User {
can_upload: boolean; can_upload: boolean;
uploads_count: number; uploads_count: number;
comments_count: number; comments_count: number;
bio: string;
} }
export interface UserWithToken extends User { export interface UserWithToken extends User {

View File

@@ -172,6 +172,21 @@ class Network {
} }
} }
async changeBio(bio: string): Promise<Response<User>> {
try {
const response = await axios.postForm(`${this.apiBaseUrl}/user/bio`, {
bio
})
return response.data
} catch (e: any) {
console.error(e)
return {
success: false,
message: e.toString(),
}
}
}
getUserAvatar(user: User): string { getUserAvatar(user: User): string {
return this.baseUrl + user.avatar_path return this.baseUrl + user.avatar_path
} }

View File

@@ -7,6 +7,7 @@ import { MdOutlineAccountCircle, MdLockOutline, MdOutlineEditNote } from "react-
import Button from "../components/button"; import Button from "../components/button";
import showToast from "../components/toast"; import showToast from "../components/toast";
import { useNavigator } from "../components/navigator"; import { useNavigator } from "../components/navigator";
import Input from "../components/input.tsx";
export function ManageMePage() { export function ManageMePage() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -19,6 +20,7 @@ export function ManageMePage() {
<ChangeAvatarDialog /> <ChangeAvatarDialog />
<ChangeUsernameDialog /> <ChangeUsernameDialog />
<ChangePasswordDialog /> <ChangePasswordDialog />
<ChangeBioDialog />
</div>; </div>;
} }
@@ -92,7 +94,7 @@ function ChangeAvatarDialog() {
<div className="h-48 flex items-center justify-center"> <div className="h-48 flex items-center justify-center">
<div className="avatar"> <div className="avatar">
<div className="w-28 rounded-full cursor-pointer" onClick={selectAvatar}> <div className="w-28 rounded-full cursor-pointer" onClick={selectAvatar}>
<img src={avatar ? URL.createObjectURL(avatar) : network.getUserAvatar(app.user!)} /> <img src={avatar ? URL.createObjectURL(avatar) : network.getUserAvatar(app.user!)} alt={"avatar"} />
</div> </div>
</div> </div>
</div> </div>
@@ -299,4 +301,69 @@ function ChangePasswordDialog() {
</div> </div>
</dialog> </dialog>
</>; </>;
}
function ChangeBioDialog() {
const [bio, setBio] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
const handleSubmit = async () => {
if (!bio.trim()) {
setError(t("Bio cannot be empty"));
return;
} else if (bio.length > 200) {
setError(t("Bio cannot be longer than 200 characters"));
return;
}
setIsLoading(true);
const res = await network.changeBio(bio);
setIsLoading(false);
if (!res.success) {
setError(res.message);
} else {
app.user = res.data!;
showToast({
message: t("Bio changed successfully"),
type: "success",
});
const dialog = document.getElementById("change_bio_dialog") as HTMLDialogElement;
if (dialog) {
dialog.close();
}
setBio("");
setError(null);
}
};
return <>
<ListTile icon={<MdOutlineEditNote />} title={t("Change Bio")} onClick={() => {
const dialog = document.getElementById("change_bio_dialog") as HTMLDialogElement;
if (dialog) {
dialog.showModal();
}
}} />
<dialog id="change_bio_dialog" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg">{t("Change Bio")}</h3>
<Input value={bio} onChange={(e) => setBio(e.target.value)} label={"bio"} />
{error && <ErrorAlert message={error} className={"mt-4"} />}
<div className="modal-action">
<form method="dialog">
<Button>{t("Close")}</Button>
</form>
<Button
className="btn-primary"
onClick={handleSubmit}
isLoading={isLoading}
disabled={!bio.trim()}
>
{t("Save")}
</Button>
</div>
</div>
</dialog>
</>;
} }

View File

@@ -63,13 +63,16 @@ function UserCard({ user }: { user: User }) {
<div> <div>
<h1 className="text-2xl font-bold">{user.username}</h1> <h1 className="text-2xl font-bold">{user.username}</h1>
<div className="h-4"></div> <div className="h-4"></div>
<p> {user.bio.trim() !== ""
<span className="text-sm font-bold mr-1"> {user.uploads_count}</span> ? <p className="text-sm text-base-content/80">{user.bio.trim()}</p>
<span className="text-sm">Resources</span> : <p>
<span className="mx-2"></span> <span className="text-sm font-bold mr-1"> {user.uploads_count}</span>
<span className="text-sm font-bold mr-1"> {user.comments_count}</span> <span className="text-sm">Resources</span>
<span className="text-base-content text-sm">Comments</span> <span className="mx-2"></span>
</p> <span className="text-sm font-bold mr-1"> {user.comments_count}</span>
<span className="text-base-content text-sm">Comments</span>
</p>
}
</div> </div>
</div> </div>
} }

View File

@@ -298,6 +298,26 @@ func handleChangeUsername(c fiber.Ctx) error {
}) })
} }
func handleSetUserBio(c fiber.Ctx) error {
uid, ok := c.Locals("uid").(uint)
if !ok {
return model.NewUnAuthorizedError("Unauthorized")
}
bio := c.FormValue("bio")
if bio == "" {
return model.NewRequestError("Bio is required")
}
user, err := service.SetUserBio(uid, bio)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserView]{
Success: true,
Data: user,
Message: "Bio updated successfully",
})
}
func AddUserRoutes(r fiber.Router) { func AddUserRoutes(r fiber.Router) {
u := r.Group("user") u := r.Group("user")
u.Post("/register", handleUserRegister) u.Post("/register", handleUserRegister)
@@ -312,4 +332,5 @@ func AddUserRoutes(r fiber.Router) {
u.Post("/delete", handleDeleteUser) u.Post("/delete", handleDeleteUser)
u.Get("/info", handleGetUserInfo) u.Get("/info", handleGetUserInfo)
u.Post("/username", handleChangeUsername) u.Post("/username", handleChangeUsername)
u.Post("/bio", handleSetUserBio)
} }

View File

@@ -16,6 +16,7 @@ type User struct {
UploadsCount int UploadsCount int
CommentsCount int CommentsCount int
Resources []Resource `gorm:"foreignKey:UserID"` Resources []Resource `gorm:"foreignKey:UserID"`
Bio string
} }
type UserView struct { type UserView struct {
@@ -27,6 +28,7 @@ type UserView struct {
CanUpload bool `json:"can_upload"` CanUpload bool `json:"can_upload"`
UploadsCount int `json:"uploads_count"` UploadsCount int `json:"uploads_count"`
CommentsCount int `json:"comments_count"` CommentsCount int `json:"comments_count"`
Bio string `json:"bio"`
} }
type UserViewWithToken struct { type UserViewWithToken struct {
@@ -44,6 +46,7 @@ func (u User) ToView() UserView {
CanUpload: u.CanUpload || u.IsAdmin, CanUpload: u.CanUpload || u.IsAdmin,
UploadsCount: u.UploadsCount, UploadsCount: u.UploadsCount,
CommentsCount: u.CommentsCount, CommentsCount: u.CommentsCount,
Bio: u.Bio,
} }
} }

View File

@@ -292,3 +292,18 @@ func ChangeUsername(uid uint, newUsername string) (model.UserView, error) {
} }
return user.ToView(), nil return user.ToView(), nil
} }
func SetUserBio(uid uint, bio string) (model.UserView, error) {
if len(bio) > 200 {
return model.UserView{}, model.NewRequestError("Bio must be less than 200 characters")
}
user, err := dao.GetUserByID(uid)
if err != nil {
return model.UserView{}, err
}
user.Bio = bio
if err := dao.UpdateUser(user); err != nil {
return model.UserView{}, err
}
return user.ToView(), nil
}