feat: add avatar
This commit is contained in:
@@ -49,6 +49,7 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
|
|||||||
"id": user.id,
|
"id": user.id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
|
"avatar": user.avatar,
|
||||||
"is_admin": user.is_admin
|
"is_admin": user.is_admin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +74,7 @@ def register_user(user: UserCreate, background_tasks: BackgroundTasks, db: Sessi
|
|||||||
db_user = User(
|
db_user = User(
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email,
|
email=user.email,
|
||||||
|
avatar=user.avatar,
|
||||||
hashed_password=hashed_password,
|
hashed_password=hashed_password,
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
is_admin=is_admin
|
is_admin=is_admin
|
||||||
@@ -178,6 +180,7 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
|||||||
db_user = User(
|
db_user = User(
|
||||||
username=user.username,
|
username=user.username,
|
||||||
email=user.email,
|
email=user.email,
|
||||||
|
avatar=user.avatar,
|
||||||
hashed_password=get_password_hash(user.password),
|
hashed_password=get_password_hash(user.password),
|
||||||
is_active=user.is_active,
|
is_active=user.is_active,
|
||||||
is_admin=user.is_admin
|
is_admin=user.is_admin
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class User(Base):
|
|||||||
username = Column(String, unique=True, index=True, nullable=False)
|
username = Column(String, unique=True, index=True, nullable=False)
|
||||||
email = Column(String, unique=True, index=True, nullable=False)
|
email = Column(String, unique=True, index=True, nullable=False)
|
||||||
hashed_password = Column(String, nullable=False)
|
hashed_password = Column(String, nullable=False)
|
||||||
|
avatar = Column(String, nullable=True) # Store avatar identifier or URL
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
is_admin = Column(Boolean, default=False)
|
is_admin = Column(Boolean, default=False)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from datetime import datetime
|
|||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: str
|
email: str
|
||||||
|
avatar: Optional[str] = None
|
||||||
is_active: Optional[bool] = True
|
is_active: Optional[bool] = True
|
||||||
is_admin: Optional[bool] = False
|
is_admin: Optional[bool] = False
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ class UserCreate(UserBase):
|
|||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
email: Optional[str] = None
|
email: Optional[str] = None
|
||||||
|
avatar: Optional[str] = None
|
||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
is_admin: Optional[bool] = None
|
is_admin: Optional[bool] = None
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
|
|||||||
@@ -885,8 +885,12 @@ function SidebarBody() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 border border-indigo-200 shadow-sm">
|
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 border border-indigo-200 shadow-sm overflow-hidden">
|
||||||
|
{user?.avatar ? (
|
||||||
|
<img src={user.avatar} alt="avatar" className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
<User className="h-4.5 w-4.5" />
|
<User className="h-4.5 w-4.5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-medium truncate max-w-[100px] text-left">
|
<div className="text-sm font-medium truncate max-w-[100px] text-left">
|
||||||
{user?.username || t('defaultUser')}
|
{user?.username || t('defaultUser')}
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||||
import { Save, Loader2 } from "lucide-react";
|
import { Save, Loader2, Check } from "lucide-react";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useAuthStore } from "@/store/authStore";
|
import { useAuthStore } from "@/store/authStore";
|
||||||
|
|
||||||
|
const BUILTIN_AVATARS = [
|
||||||
|
"https://api.dicebear.com/7.x/bottts/svg?seed=Felix",
|
||||||
|
"https://api.dicebear.com/7.x/bottts/svg?seed=Aneka",
|
||||||
|
"https://api.dicebear.com/7.x/bottts/svg?seed=Tinkerbell",
|
||||||
|
"https://api.dicebear.com/7.x/bottts/svg?seed=Bella",
|
||||||
|
"https://api.dicebear.com/7.x/bottts/svg?seed=Buster",
|
||||||
|
"https://api.dicebear.com/7.x/bottts/svg?seed=Max",
|
||||||
|
"https://api.dicebear.com/7.x/adventurer/svg?seed=Leo",
|
||||||
|
"https://api.dicebear.com/7.x/adventurer/svg?seed=Oliver",
|
||||||
|
"https://api.dicebear.com/7.x/adventurer/svg?seed=Mia",
|
||||||
|
"https://api.dicebear.com/7.x/adventurer/svg?seed=Lily",
|
||||||
|
"https://api.dicebear.com/7.x/adventurer/svg?seed=Chloe",
|
||||||
|
"https://api.dicebear.com/7.x/adventurer/svg?seed=Simba"
|
||||||
|
];
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user, updateUser } = useAuthStore();
|
const { user, updateUser } = useAuthStore();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
const [avatar, setAvatar] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -21,6 +37,7 @@ export function Settings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
setEmail(user.email || '');
|
setEmail(user.email || '');
|
||||||
|
setAvatar(user.avatar || '');
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
@@ -38,7 +55,8 @@ export function Settings() {
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
email: email
|
email: email,
|
||||||
|
avatar: avatar || null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
@@ -55,8 +73,8 @@ export function Settings() {
|
|||||||
setPassword('');
|
setPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
|
|
||||||
// Update global state with new email
|
// Update global state with new email and avatar
|
||||||
updateUser({ email: response.email });
|
updateUser({ email: response.email, avatar: response.avatar });
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Failed to save settings", error);
|
console.error("Failed to save settings", error);
|
||||||
@@ -87,6 +105,28 @@ export function Settings() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<Label>{t('avatar', 'Avatar')}</Label>
|
||||||
|
<div className="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-12 gap-2 mt-2">
|
||||||
|
{BUILTIN_AVATARS.map((url) => (
|
||||||
|
<div
|
||||||
|
key={url}
|
||||||
|
className={`relative cursor-pointer rounded-full overflow-hidden border-2 transition-all ${
|
||||||
|
avatar === url ? 'border-indigo-500 scale-110 shadow-md' : 'border-transparent hover:border-indigo-200'
|
||||||
|
}`}
|
||||||
|
onClick={() => setAvatar(url)}
|
||||||
|
>
|
||||||
|
<img src={url} alt="avatar" className="w-full h-auto bg-muted/30" />
|
||||||
|
{avatar === url && (
|
||||||
|
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
|
||||||
|
<Check className="h-4 w-4 text-indigo-600 drop-shadow-sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 pt-2 border-t border-border">
|
||||||
<Label htmlFor="username">{t('username')}</Label>
|
<Label htmlFor="username">{t('username')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="username"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface User {
|
|||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
avatar?: string | null;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user