refactor(client): remove avatarType, add auth effects, simplify UserAvatar

This commit is contained in:
Kirill
2026-05-22 12:08:41 +05:00
parent 6bedf0b28a
commit be65f2330e
12 changed files with 40 additions and 65 deletions
@@ -56,7 +56,6 @@ export function OrderChat({ messages, isPending, onSend }: Props) {
<UserAvatar <UserAvatar
userId="admin" userId="admin"
avatarUrl={adminAv?.avatar} avatarUrl={adminAv?.avatar}
avatarType={adminAv?.avatarType}
avatarStyle={adminAv?.avatarStyle} avatarStyle={adminAv?.avatarStyle}
size={24} size={24}
/> />
@@ -64,7 +63,6 @@ export function OrderChat({ messages, isPending, onSend }: Props) {
<UserAvatar <UserAvatar
userId={currentUser.id} userId={currentUser.id}
avatarUrl={currentUser.avatar} avatarUrl={currentUser.avatar}
avatarType={currentUser.avatarType}
avatarStyle={currentUser.avatarStyle} avatarStyle={currentUser.avatarStyle}
size={24} size={24}
/> />
@@ -175,7 +175,6 @@ export function OrderDetailContent({ detail, orderId }: { detail: AdminOrderDeta
<UserAvatar <UserAvatar
userId={currentUser.id} userId={currentUser.id}
avatarUrl={currentUser.avatar} avatarUrl={currentUser.avatar}
avatarType={currentUser.avatarType}
avatarStyle={currentUser.avatarStyle} avatarStyle={currentUser.avatarStyle}
size={24} size={24}
/> />
@@ -184,7 +183,6 @@ export function OrderDetailContent({ detail, orderId }: { detail: AdminOrderDeta
<UserAvatar <UserAvatar
userId={detail.user.id} userId={detail.user.id}
avatarUrl={detail.user.avatar} avatarUrl={detail.user.avatar}
avatarType={detail.user.avatarType}
avatarStyle={detail.user.avatarStyle} avatarStyle={detail.user.avatarStyle}
size={24} size={24}
/> />
@@ -22,7 +22,6 @@ function ReviewItem({ rv }: { rv: PublicProductReviewItem }) {
<UserAvatar <UserAvatar
userId={rv.authorDisplay} userId={rv.authorDisplay}
avatarUrl={rv.authorAvatar} avatarUrl={rv.authorAvatar}
avatarType={rv.authorAvatarType}
avatarStyle={rv.authorAvatarStyle} avatarStyle={rv.authorAvatarStyle}
size={32} size={32}
/> />
@@ -46,7 +46,6 @@ export function UserMenu({ user, isAdmin = false, onNavigate, onLogout }: Props)
<UserAvatar <UserAvatar
userId={user.id} userId={user.id}
avatarUrl={user.avatar} avatarUrl={user.avatar}
avatarType={user.avatarType}
avatarStyle={user.avatarStyle} avatarStyle={user.avatarStyle}
size={28} size={28}
/> />
@@ -56,9 +56,7 @@ export function AdminSettingsPage() {
mode: 'onChange', mode: 'onChange',
}) })
const hasOAuthAvatar = Boolean(user?.avatar && user.avatarType !== 'generated') const hasAvatar = Boolean(user?.avatar)
const useOAuth = user?.avatarType === 'oauth'
const useGenerated = user?.avatarType === 'generated'
const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID) const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID)
const [previewSrc, setPreviewSrc] = useState<string | null>(null) const [previewSrc, setPreviewSrc] = useState<string | null>(null)
@@ -70,14 +68,12 @@ export function AdminSettingsPage() {
mutationFn: (params: { mutationFn: (params: {
displayName: string | null displayName: string | null
avatar?: string | null avatar?: string | null
avatarType?: string | null
avatarStyle?: string | null avatarStyle?: string | null
}) => apiClient.patch('admin/profile', params), }) => apiClient.patch('admin/profile', params),
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
const p: UpdateProfileParams = { displayName: variables.displayName ?? null } const p: UpdateProfileParams = { displayName: variables.displayName ?? null }
if (variables.avatar !== undefined) { if (variables.avatar !== undefined) {
p.avatar = variables.avatar p.avatar = variables.avatar
p.avatarType = variables.avatarType ?? null
p.avatarStyle = variables.avatarStyle ?? null p.avatarStyle = variables.avatarStyle ?? null
} }
updateProfileFx(p) updateProfileFx(p)
@@ -144,7 +140,6 @@ export function AdminSettingsPage() {
<UserAvatar <UserAvatar
userId={String(user.id)} userId={String(user.id)}
avatarUrl={hasUnsavedPreview ? previewSrc : user.avatar} avatarUrl={hasUnsavedPreview ? previewSrc : user.avatar}
avatarType={hasUnsavedPreview ? 'generated' : user.avatarType}
avatarStyle={hasUnsavedPreview ? previewStyle : user.avatarStyle} avatarStyle={hasUnsavedPreview ? previewStyle : user.avatarStyle}
size={80} size={80}
sx={{ sx={{
@@ -153,7 +148,7 @@ export function AdminSettingsPage() {
}} }}
/> />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}> <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
{hasUnsavedPreview ? 'Предпросмотр' : useOAuth ? 'Сохранён' : useGenerated ? 'Сохранён' : 'Авто'} {hasUnsavedPreview ? 'Предпросмотр' : hasAvatar ? 'Сохранён' : 'Авто'}
</Typography> </Typography>
</Box> </Box>
{hasUnsavedPreview && ( {hasUnsavedPreview && (
@@ -161,7 +156,6 @@ export function AdminSettingsPage() {
<UserAvatar <UserAvatar
userId={String(user.id)} userId={String(user.id)}
avatarUrl={user.avatar} avatarUrl={user.avatar}
avatarType={user.avatarType}
avatarStyle={user.avatarStyle} avatarStyle={user.avatarStyle}
size={80} size={80}
sx={{ border: 2, borderColor: 'divider', opacity: 0.6 }} sx={{ border: 2, borderColor: 'divider', opacity: 0.6 }}
@@ -209,7 +203,6 @@ export function AdminSettingsPage() {
profileSaveMut.mutate({ profileSaveMut.mutate({
displayName: name.length ? name : null, displayName: name.length ? name : null,
avatar: previewSrc, avatar: previewSrc,
avatarType: 'generated',
avatarStyle: previewStyle, avatarStyle: previewStyle,
}) })
setPreviewSrc(null) setPreviewSrc(null)
@@ -222,24 +215,6 @@ export function AdminSettingsPage() {
</Button> </Button>
</Stack> </Stack>
)} )}
{hasOAuthAvatar && !hasUnsavedPreview && (
<Button
variant="outlined"
disabled={pendingProfile || profileSaveMut.isPending || useOAuth}
onClick={() => {
const raw = profileForm.getValues('displayName')
const name = raw.trim()
profileSaveMut.mutate({
displayName: name.length ? name : null,
avatarType: 'oauth',
})
}}
sx={{ mt: 0.5 }}
>
Использовать OAuth
</Button>
)}
</Box> </Box>
</Stack> </Stack>
</Box> </Box>
@@ -195,7 +195,6 @@ export function AdminUsersPage() {
<UserAvatar <UserAvatar
userId={u.id} userId={u.id}
avatarUrl={u.avatar} avatarUrl={u.avatar}
avatarType={u.avatarType}
avatarStyle={u.avatarStyle} avatarStyle={u.avatarStyle}
size={28} size={28}
/> />
@@ -181,7 +181,6 @@ export function MessagesPage() {
<UserAvatar <UserAvatar
userId="admin" userId="admin"
avatarUrl={adminAv?.avatar} avatarUrl={adminAv?.avatar}
avatarType={adminAv?.avatarType}
avatarStyle={adminAv?.avatarStyle} avatarStyle={adminAv?.avatarStyle}
size={24} size={24}
/> />
@@ -189,7 +188,6 @@ export function MessagesPage() {
<UserAvatar <UserAvatar
userId={currentUser.id} userId={currentUser.id}
avatarUrl={currentUser.avatar} avatarUrl={currentUser.avatar}
avatarType={currentUser.avatarType}
avatarStyle={currentUser.avatarStyle} avatarStyle={currentUser.avatarStyle}
size={24} size={24}
/> />
@@ -56,9 +56,7 @@ export function SettingsPage() {
const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify) const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify)
const profileErrorMsg = getApiErrorMessage(errorProfile) const profileErrorMsg = getApiErrorMessage(errorProfile)
const hasOAuthAvatar = Boolean(user?.avatar && user.avatarType !== 'generated') const hasAvatar = Boolean(user?.avatar)
const useOAuth = user?.avatarType === 'oauth'
const useGenerated = user?.avatarType === 'generated'
const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID) const [selectedStyle, setSelectedStyle] = useState(user?.avatarStyle || DEFAULT_STYLE_ID)
const [previewSrc, setPreviewSrc] = useState<string | null>(null) const [previewSrc, setPreviewSrc] = useState<string | null>(null)
@@ -128,7 +126,6 @@ export function SettingsPage() {
<UserAvatar <UserAvatar
userId={user.id} userId={user.id}
avatarUrl={hasUnsavedPreview ? previewSrc : user.avatar} avatarUrl={hasUnsavedPreview ? previewSrc : user.avatar}
avatarType={hasUnsavedPreview ? 'generated' : user.avatarType}
avatarStyle={hasUnsavedPreview ? previewStyle : user.avatarStyle} avatarStyle={hasUnsavedPreview ? previewStyle : user.avatarStyle}
size={80} size={80}
sx={{ sx={{
@@ -137,7 +134,7 @@ export function SettingsPage() {
}} }}
/> />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}> <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
{hasUnsavedPreview ? 'Предпросмотр' : useOAuth ? 'Сохранён' : useGenerated ? 'Сохранён' : 'Авто'} {hasUnsavedPreview ? 'Предпросмотр' : hasAvatar ? 'Сохранён' : 'Авто'}
</Typography> </Typography>
</Box> </Box>
{hasUnsavedPreview && ( {hasUnsavedPreview && (
@@ -145,7 +142,6 @@ export function SettingsPage() {
<UserAvatar <UserAvatar
userId={user.id} userId={user.id}
avatarUrl={user.avatar} avatarUrl={user.avatar}
avatarType={user.avatarType}
avatarStyle={user.avatarStyle} avatarStyle={user.avatarStyle}
size={80} size={80}
sx={{ border: 2, borderColor: 'divider', opacity: 0.6 }} sx={{ border: 2, borderColor: 'divider', opacity: 0.6 }}
@@ -191,7 +187,6 @@ export function SettingsPage() {
updateProfileFx({ updateProfileFx({
displayName: user.displayName?.trim() || null, displayName: user.displayName?.trim() || null,
avatar: previewSrc, avatar: previewSrc,
avatarType: 'generated',
avatarStyle: previewStyle, avatarStyle: previewStyle,
}) })
setPreviewSrc(null) setPreviewSrc(null)
@@ -204,22 +199,6 @@ export function SettingsPage() {
</Button> </Button>
</Stack> </Stack>
)} )}
{hasOAuthAvatar && !hasUnsavedPreview && (
<Button
variant="outlined"
disabled={pendingProfile || useOAuth}
onClick={() => {
updateProfileFx({
displayName: user.displayName?.trim() || null,
avatarType: 'oauth',
})
}}
sx={{ mt: 0.5 }}
>
Использовать OAuth
</Button>
)}
</Box> </Box>
<Divider /> <Divider />
+34 -2
View File
@@ -11,11 +11,15 @@ export type AuthUser = {
lastName?: string | null lastName?: string | null
gender?: string | null gender?: string | null
avatar?: string | null avatar?: string | null
avatarType?: string | null
avatarStyle?: string | null avatarStyle?: string | null
isAdmin?: boolean isAdmin?: boolean
} }
export type AuthMethod = {
type: 'password' | 'vk' | 'yandex'
active: boolean
}
export const tokenSet = createEvent<string | null>() export const tokenSet = createEvent<string | null>()
export const logout = createEvent() export const logout = createEvent()
@@ -72,7 +76,6 @@ export const verifyEmailChangeFx = createEffect(async (params: { newEmail: strin
export type UpdateProfileParams = { export type UpdateProfileParams = {
displayName: string | null displayName: string | null
avatar?: string | null avatar?: string | null
avatarType?: string | null
avatarStyle?: string | null avatarStyle?: string | null
} }
@@ -81,6 +84,35 @@ export const updateProfileFx = createEffect(async (params: UpdateProfileParams)
return data.user return data.user
}) })
// ----- Auth effects -----
export const loginFx = createEffect(async (params: { email: string; password: string }) => {
const { data } = await apiClient.post<{ token: string; user: AuthUser }>('auth/login', params)
tokenSet(data.token)
return data.user
})
export const registerFx = createEffect(
async (params: { email: string; password: string; displayName?: string }) => {
const { data } = await apiClient.post<{ token: string; user: AuthUser }>('auth/register', params)
tokenSet(data.token)
return data.user
},
)
export const fetchAuthMethodsFx = createEffect(async () => {
const { data } = await apiClient.get<{ methods: AuthMethod[] }>('me/auth-methods')
return data.methods
})
export const setPasswordFx = createEffect(async (password: string) => {
await apiClient.post('me/password', { password })
})
export const unlinkOAuthFx = createEffect(async (provider: 'vk' | 'yandex') => {
await apiClient.delete(`me/oauth/${provider}`)
})
// ----- Error stores ----- // ----- Error stores -----
export const $requestEmailChangeCodeError = createErrorStore(requestEmailChangeCodeFx).$error export const $requestEmailChangeCodeError = createErrorStore(requestEmailChangeCodeFx).$error
+2 -3
View File
@@ -7,20 +7,19 @@ import { DEFAULT_STYLE_ID, getStyleById } from '@/shared/lib/avatar-styles'
type UserAvatarProps = { type UserAvatarProps = {
userId: string userId: string
avatarUrl?: string | null avatarUrl?: string | null
avatarType?: string | null
avatarStyle?: string | null avatarStyle?: string | null
size?: number size?: number
sx?: SxProps<Theme> sx?: SxProps<Theme>
} }
export function UserAvatar({ userId, avatarUrl, avatarType, avatarStyle, size = 40, sx }: UserAvatarProps) { export function UserAvatar({ userId, avatarUrl, avatarStyle, size = 40, sx }: UserAvatarProps) {
const generatedSrc = useMemo(() => { const generatedSrc = useMemo(() => {
const styleDef = getStyleById(avatarStyle || DEFAULT_STYLE_ID) const styleDef = getStyleById(avatarStyle || DEFAULT_STYLE_ID)
const avatar = createAvatar(styleDef.style, { seed: userId }) const avatar = createAvatar(styleDef.style, { seed: userId })
return avatar.toDataUri() return avatar.toDataUri()
}, [userId, avatarStyle]) }, [userId, avatarStyle])
const src = avatarType && avatarUrl ? avatarUrl : generatedSrc const src = avatarUrl || generatedSrc
return ( return (
<Avatar src={src} sx={{ width: size, height: size, bgcolor: 'primary.main', ...sx }}> <Avatar src={src} sx={{ width: size, height: size, bgcolor: 'primary.main', ...sx }}>
@@ -104,7 +104,6 @@ export function ReviewsBlock() {
<UserAvatar <UserAvatar
userId={r.authorDisplay} userId={r.authorDisplay}
avatarUrl={r.authorAvatar} avatarUrl={r.authorAvatar}
avatarType={r.authorAvatarType}
avatarStyle={r.authorAvatarStyle} avatarStyle={r.authorAvatarStyle}
size={40} size={40}
/> />
Binary file not shown.