mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 20:27:23 +00:00
Add bio management feature with UI and backend support
This commit is contained in:
@@ -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": "更改個人簡介",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
@@ -300,3 +302,68 @@ function ChangePasswordDialog() {
|
|||||||
</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>
|
||||||
|
</>;
|
||||||
|
}
|
@@ -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() !== ""
|
||||||
|
? <p className="text-sm text-base-content/80">{user.bio.trim()}</p>
|
||||||
|
: <p>
|
||||||
<span className="text-sm font-bold mr-1"> {user.uploads_count}</span>
|
<span className="text-sm font-bold mr-1"> {user.uploads_count}</span>
|
||||||
<span className="text-sm">Resources</span>
|
<span className="text-sm">Resources</span>
|
||||||
<span className="mx-2"></span>
|
<span className="mx-2"></span>
|
||||||
<span className="text-sm font-bold mr-1"> {user.comments_count}</span>
|
<span className="text-sm font-bold mr-1"> {user.comments_count}</span>
|
||||||
<span className="text-base-content text-sm">Comments</span>
|
<span className="text-base-content text-sm">Comments</span>
|
||||||
</p>
|
</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user