mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-27 19:46:24 +00:00
feat: enhance post management with pagination, search, and order functionality
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 11s
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 11s
- Added search input for filtering posts by keywords. - Implemented pagination controls for navigating through posts. - Introduced order selector for sorting posts based on various criteria. - Enhanced post item display with additional metrics (view count, like count, comment count). - Added dropdown menu for post actions (edit, view, toggle privacy, delete). - Integrated double confirmation for delete action. - Updated user profile to support background image upload. - Improved user security settings with better layout and validation. - Refactored auth context to use useCallback for logout function. - Added command palette component for improved command execution. - Introduced popover component for better UI interactions. - Implemented debounce hooks for optimized state updates. - Updated localization files with new keys for improved internationalization. - Added tailwind configuration for styling.
This commit is contained in:
@ -32,9 +32,11 @@ export function UserProfilePage() {
|
||||
const [username, setUsername] = useState(user?.username || '')
|
||||
const [avatarFile, setAvatarFile] = useState<File | null>(null)
|
||||
const [avatarFileUrl, setAvatarFileUrl] = useState<string | null>(null) // 这部分交由useEffect控制,监听 avatarFile 变化
|
||||
const [backgroundFile, setBackgroundFile] = useState<File | null>(null)
|
||||
const [backgroundFileUrl, setBackgroundFileUrl] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [gender, setGender] = useState(user?.gender || '')
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
@ -50,6 +52,20 @@ export function UserProfilePage() {
|
||||
};
|
||||
}, [avatarFile, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
if (!backgroundFile) {
|
||||
setBackgroundFileUrl(null);
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(backgroundFile);
|
||||
setBackgroundFileUrl(url);
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
setBackgroundFileUrl(null);
|
||||
};
|
||||
}, [backgroundFile, user]);
|
||||
|
||||
const handlePictureSelected = (e: PictureInputChangeEvent): void => {
|
||||
const file: File | null = e.target.files?.[0] ?? null;
|
||||
if (!file) {
|
||||
@ -67,12 +83,35 @@ export function UserProfilePage() {
|
||||
}
|
||||
if (file.size > constraints.maxSize) {
|
||||
setAvatarFile(null);
|
||||
toast.error(t("picture_size_cannot_exceed", {"size": "5MiB"}));
|
||||
toast.error(t("picture_size_cannot_exceed", { "size": "5MiB" }));
|
||||
return;
|
||||
}
|
||||
setAvatarFile(file);
|
||||
}
|
||||
|
||||
const handleBackgroundSelected = (e: PictureInputChangeEvent): void => {
|
||||
const file: File | null = e.target.files?.[0] ?? null;
|
||||
if (!file) {
|
||||
setBackgroundFile(null);
|
||||
return;
|
||||
}
|
||||
const constraints: UploadConstraints = {
|
||||
allowedTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
|
||||
maxSize: 5 * 1024 * 1024, // 5 MB
|
||||
};
|
||||
if (!file.type || !file.type.startsWith('image/') || !constraints.allowedTypes.includes(file.type)) {
|
||||
setBackgroundFile(null);
|
||||
toast.error(t("only_allow_picture"));
|
||||
return;
|
||||
}
|
||||
if (file.size > constraints.maxSize) {
|
||||
setBackgroundFile(null);
|
||||
toast.error(t("picture_size_cannot_exceed", { "size": "5MiB" }));
|
||||
return;
|
||||
}
|
||||
setBackgroundFile(file);
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!user) return;
|
||||
if (
|
||||
@ -87,7 +126,7 @@ export function UserProfilePage() {
|
||||
(username.length < 1 || username.length > 20) ||
|
||||
(nickname.length < 1 || nickname.length > 20)
|
||||
) {
|
||||
toast.error(t("nickname_and_username_must_be_between", {"min": 1, "max": 20}))
|
||||
toast.error(t("nickname_and_username_must_be_between", { "min": 1, "max": 20 }))
|
||||
return
|
||||
}
|
||||
|
||||
@ -95,13 +134,15 @@ export function UserProfilePage() {
|
||||
username === user.username &&
|
||||
nickname === user.nickname &&
|
||||
gender === user.gender &&
|
||||
avatarFile === null
|
||||
avatarFile === null &&
|
||||
backgroundFile === null
|
||||
) {
|
||||
toast.warning(t("no_changes_made"))
|
||||
return
|
||||
}
|
||||
|
||||
let avatarUrl = user.avatarUrl;
|
||||
let backgroundUrl = user.backgroundUrl;
|
||||
setSubmitting(true);
|
||||
(async () => {
|
||||
if (avatarFile) {
|
||||
@ -114,8 +155,18 @@ export function UserProfilePage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (backgroundFile) {
|
||||
try {
|
||||
const resp = await uploadFile({ file: backgroundFile });
|
||||
backgroundUrl = getFileUri(resp.data.id);
|
||||
} catch (error: unknown) {
|
||||
toast.error(`${t("failed_to_upload_background")}: ${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUser({ nickname, username, avatarUrl, gender, id: user.id });
|
||||
await updateUser({ nickname, username, avatarUrl, backgroundUrl, gender, id: user.id });
|
||||
window.location.reload();
|
||||
} catch (error: unknown) {
|
||||
toast.error(`${t("failed_to_update_profile")}: ${error}`);
|
||||
@ -123,7 +174,7 @@ export function UserProfilePage() {
|
||||
setSubmitting(false);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
}
|
||||
|
||||
const handleCropped = (blob: Blob) => {
|
||||
@ -139,28 +190,40 @@ export function UserProfilePage() {
|
||||
{t("public_profile")}
|
||||
</h1>
|
||||
<Separator className="my-2" />
|
||||
<div className="grid w-full max-w-sm items-center gap-3">
|
||||
<Label htmlFor="picture">{t("picture")}</Label>
|
||||
<Avatar className="h-40 w-40 rounded-xl border-2">
|
||||
{avatarFileUrl ?
|
||||
<AvatarImage src={avatarFileUrl} alt={nickname || username} /> :
|
||||
<AvatarImage src={getGravatarFromUser({ user })} alt={nickname || username} />}
|
||||
<AvatarFallback>{getFallbackAvatarFromUsername(nickname || username)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex gap-3"><Input
|
||||
id="picture"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif,image/*"
|
||||
onChange={handlePictureSelected}
|
||||
/>
|
||||
<ImageCropper image={avatarFile} onCropped={handleCropped} />
|
||||
<div className="grid w-full max-w-sm items-center gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="picture">{t("picture")}</Label>
|
||||
<Avatar className="h-40 w-40 rounded-xl border-2">
|
||||
{avatarFileUrl ?
|
||||
<AvatarImage src={avatarFileUrl} alt={nickname || username} /> :
|
||||
<AvatarImage src={getGravatarFromUser({ user })} alt={nickname || username} />}
|
||||
<AvatarFallback>{getFallbackAvatarFromUsername(nickname || username)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex gap-2"><Input
|
||||
id="picture"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif,image/*"
|
||||
onChange={handlePictureSelected}
|
||||
/>
|
||||
<ImageCropper image={avatarFile} onCropped={handleCropped} />
|
||||
</div>
|
||||
</div>
|
||||
<Label htmlFor="nickname">{t("nickname")}</Label>
|
||||
<Input type="nickname" id="nickname" value={nickname} onChange={(e) => setNickname(e.target.value)} />
|
||||
<Label htmlFor="username">{t("username")}</Label>
|
||||
<Input type="username" id="username" value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
<Label htmlFor="gender">{t("gender")}</Label>
|
||||
<Input type="gender" id="gender" value={gender} onChange={(e) => setGender(e.target.value)}/>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="nickname">{t("nickname")}</Label>
|
||||
<Input type="nickname" id="nickname" value={nickname} onChange={(e) => setNickname(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">{t("username")}</Label>
|
||||
<Input type="username" id="username" value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
</div >
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="gender">{t("gender")}</Label>
|
||||
<Input type="gender" id="gender" value={gender} onChange={(e) => setGender(e.target.value)} />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<Button className="max-w-1/3" onClick={handleSubmit} disabled={submitting}>{t("update_profile")}{submitting && '...'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user