внедрил систему статусов (BIO)
This commit is contained in:
@@ -9,4 +9,6 @@ public static class ProfilesErrors
|
||||
public static Error AvatarNotFound => new("Profiles.AvatarNotFound", "Avatar not found");
|
||||
public static Error InvalidAvatarFormat => new("Profiles.InvalidAvatarFormat", "Invalid avatar format");
|
||||
public static Error AvatarUploadFailed => new("Profiles.AvatarUploadFailed", "Avatar upload failed");
|
||||
public static Error BioTooLong => new("Profiles.BioTooLong", "Bio must be 200 characters or less");
|
||||
public static Error StatusTextTooLong => new("Profiles.StatusTextTooLong", "Status text must be 50 characters or less");
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ public class UserProfileDto
|
||||
public DateTime? LastSeen { get; set; }
|
||||
public DateTime? Birthday { get; set; }
|
||||
public bool IsPremium { get; set; }
|
||||
public string? StatusText { get; set; }
|
||||
public string? StatusEmoji { get; set; }
|
||||
public DateTime? StatusExpiresAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,5 +2,11 @@ using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Application.Profiles.DTOs;
|
||||
|
||||
public record UpdateProfileRequest(string? DisplayName, string? Bio, DateTime? Birthday);
|
||||
public record UpdateProfileRequest(
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
DateTime? Birthday,
|
||||
string? StatusText,
|
||||
string? StatusEmoji,
|
||||
DateTime? StatusExpiresAt);
|
||||
public record UpdateSettingsRequest(bool? HideStoryViews);
|
||||
|
||||
@@ -11,7 +11,10 @@ public sealed record UpdateProfileCommand(
|
||||
Guid UserId,
|
||||
string? DisplayName,
|
||||
string? Bio,
|
||||
DateTime? Birthday) : ICommand<UserProfileDto>;
|
||||
DateTime? Birthday,
|
||||
string? StatusText,
|
||||
string? StatusEmoji,
|
||||
DateTime? StatusExpiresAt) : ICommand<UserProfileDto>;
|
||||
|
||||
internal sealed class UpdateProfileCommandHandler : ICommandHandler<UpdateProfileCommand, UserProfileDto>
|
||||
{
|
||||
@@ -28,9 +31,18 @@ internal sealed class UpdateProfileCommandHandler : ICommandHandler<UpdateProfil
|
||||
if (profile is null)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.ProfileNotFound);
|
||||
|
||||
if ((request.Bio?.Length ?? 0) > 200)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.BioTooLong);
|
||||
|
||||
if ((request.StatusText?.Length ?? 0) > 50)
|
||||
return Result.Failure<UserProfileDto>(ProfilesErrors.StatusTextTooLong);
|
||||
|
||||
profile.DisplayName = request.DisplayName ?? profile.DisplayName;
|
||||
profile.About = request.Bio;
|
||||
profile.Birthday = request.Birthday;
|
||||
profile.StatusText = request.StatusText;
|
||||
profile.StatusEmoji = request.StatusEmoji;
|
||||
profile.StatusExpiresAt = request.StatusExpiresAt;
|
||||
|
||||
var result = await _repository.UpdateAsync(profile, cancellationToken);
|
||||
if (result.IsFailure)
|
||||
|
||||
@@ -19,6 +19,12 @@ public sealed class ProfileDocument
|
||||
|
||||
public string? Bio { get; private set; }
|
||||
|
||||
public string? StatusText { get; private set; }
|
||||
|
||||
public string? StatusEmoji { get; private set; }
|
||||
|
||||
public DateTime? StatusExpiresAt { get; private set; }
|
||||
|
||||
public string? AvatarUrl { get; private set; }
|
||||
|
||||
public DateTime? Birthday { get; private set; }
|
||||
@@ -65,6 +71,13 @@ public sealed class ProfileDocument
|
||||
public void UpdateSettings(bool hideStoryViews)
|
||||
=> HideStoryViews = hideStoryViews;
|
||||
|
||||
public void UpdateStatus(string? statusText, string? statusEmoji, DateTime? statusExpiresAt)
|
||||
{
|
||||
StatusText = statusText;
|
||||
StatusEmoji = statusEmoji;
|
||||
StatusExpiresAt = statusExpiresAt;
|
||||
}
|
||||
|
||||
public void UpdateStatus(bool isBanned, bool isDeleted)
|
||||
{
|
||||
IsBanned = isBanned;
|
||||
|
||||
@@ -80,6 +80,7 @@ internal class ProfileRepository : IProfileRepository
|
||||
dto.Birthday);
|
||||
|
||||
document.UpdateAvatar(dto.Avatar);
|
||||
document.UpdateStatus(dto.StatusText, dto.StatusEmoji, dto.StatusExpiresAt);
|
||||
|
||||
await _profiles.ReplaceOneAsync(p => p.Id == dto.UserId, document, cancellationToken: ct);
|
||||
return Result.Success(document.ToDto());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Knot.Contracts.Profiles.Application.DTOs;
|
||||
using Knot.Modules.Profiles.Domain;
|
||||
using System;
|
||||
|
||||
namespace Knot.Modules.Profiles.Infrastructure.Mappings;
|
||||
|
||||
@@ -7,6 +8,8 @@ public static class ProfileMappings
|
||||
{
|
||||
public static UserProfileDto ToDto(this ProfileDocument document)
|
||||
{
|
||||
var hasActiveStatus = !document.StatusExpiresAt.HasValue || document.StatusExpiresAt.Value > DateTime.UtcNow;
|
||||
|
||||
return new UserProfileDto
|
||||
{
|
||||
UserId = document.Id,
|
||||
@@ -18,6 +21,9 @@ public static class ProfileMappings
|
||||
LastSeen = null,
|
||||
Birthday = document.Birthday,
|
||||
IsPremium = false,
|
||||
StatusText = hasActiveStatus ? document.StatusText : null,
|
||||
StatusEmoji = hasActiveStatus ? document.StatusEmoji : null,
|
||||
StatusExpiresAt = hasActiveStatus ? document.StatusExpiresAt : null,
|
||||
CreatedAt = document.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ public static class ProfilesEndpoints
|
||||
public static void MapProfilesEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("api/profiles").RequireAuthorization();
|
||||
var userGroup = app.MapGroup("api/user").RequireAuthorization();
|
||||
var usersGroup = app.MapGroup("api/users").RequireAuthorization();
|
||||
|
||||
group.MapGet("search", async ([FromQuery] string q, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
{
|
||||
@@ -67,16 +69,39 @@ public static class ProfilesEndpoints
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
||||
});
|
||||
|
||||
group.MapPut("profile", async ([FromBody] Knot.Modules.Profiles.Application.Profiles.DTOs.UpdateProfileRequest request, ISender sender, IUserContext userContext, CancellationToken ct) =>
|
||||
async Task<IResult> UpdateProfileHandler(
|
||||
[FromBody] Knot.Modules.Profiles.Application.Profiles.DTOs.UpdateProfileRequest request,
|
||||
ISender sender,
|
||||
IUserContext userContext,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await sender.Send(new UpdateProfileCommand(userContext.UserId, request.DisplayName, request.Bio, request.Birthday), ct);
|
||||
var result = await sender.Send(
|
||||
new UpdateProfileCommand(
|
||||
userContext.UserId,
|
||||
request.DisplayName,
|
||||
request.Bio,
|
||||
request.Birthday,
|
||||
request.StatusText,
|
||||
request.StatusEmoji,
|
||||
request.StatusExpiresAt),
|
||||
ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
||||
});
|
||||
}
|
||||
|
||||
group.MapPut("profile", UpdateProfileHandler);
|
||||
group.MapPatch("profile", UpdateProfileHandler);
|
||||
userGroup.MapPatch("profile", UpdateProfileHandler);
|
||||
|
||||
group.MapGet("{id:guid}", async (Guid id, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetProfileQuery(id), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
||||
});
|
||||
|
||||
usersGroup.MapGet("{id:guid}", async (Guid id, ISender sender, CancellationToken ct) =>
|
||||
{
|
||||
var result = await sender.Send(new GetProfileQuery(id), ct);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(result.Error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ export interface UserPresence extends UserBasic {
|
||||
export interface User extends UserPresence {
|
||||
bio: string | null;
|
||||
birthday: string | null;
|
||||
statusText?: string | null;
|
||||
statusEmoji?: string | null;
|
||||
statusExpiresAt?: string | null;
|
||||
createdAt: string;
|
||||
hideStoryViews?: boolean;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@ export function normalizeUser(user: any): any {
|
||||
result.about = result.bio;
|
||||
}
|
||||
|
||||
// 4. Приведение status (snake_case -> camelCase)
|
||||
if (!result.statusText && result.status_text) {
|
||||
result.statusText = result.status_text;
|
||||
}
|
||||
if (!result.statusEmoji && result.status_emoji) {
|
||||
result.statusEmoji = result.status_emoji;
|
||||
}
|
||||
if (!result.statusExpiresAt && result.status_expires_at) {
|
||||
result.statusExpiresAt = result.status_expires_at;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import { ChatApi } from '../../infrastructure/chatApi';
|
||||
import ConfirmModal from '../../../../core/presentation/components/ui/ConfirmModal';
|
||||
import Avatar from '../../../../core/presentation/components/ui/Avatar';
|
||||
import type { Chat } from '../../../../core/domain/types';
|
||||
import { UserApi } from '../../../users/infrastructure/userApi';
|
||||
|
||||
const userStatusCache = new Map<string, { emoji?: string | null; text?: string | null }>();
|
||||
|
||||
interface ChatListItemProps {
|
||||
chat: Chat;
|
||||
@@ -24,6 +27,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
|
||||
|
||||
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [otherUserStatus, setOtherUserStatus] = useState<{ emoji?: string | null; text?: string | null } | null>(null);
|
||||
const ctxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const myMember = chat.members.find((m) => m.user.id === user?.id);
|
||||
@@ -87,6 +91,38 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
|
||||
const [showAttachmentConfirm, setShowAttachmentConfirm] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState<{ processed: number, total: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (chat.type !== 'personal' || !otherMember?.user?.id) {
|
||||
setOtherUserStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const otherId = otherMember.user.id;
|
||||
const cached = userStatusCache.get(otherId);
|
||||
if (cached) {
|
||||
setOtherUserStatus(cached);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
UserApi.getUser(otherId)
|
||||
.then((profile) => {
|
||||
if (cancelled) return;
|
||||
const status = { emoji: profile.statusEmoji, text: profile.statusText };
|
||||
userStatusCache.set(otherId, status);
|
||||
setOtherUserStatus(status);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setOtherUserStatus(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [chat.type, otherMember?.user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat.isImporting || !chat.importJobId) {
|
||||
setImportStatus(null);
|
||||
@@ -210,6 +246,14 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
{isPinned && <span className="material-symbols-outlined text-[14px] text-primary rotate-45">push_pin</span>}
|
||||
{chat.type === 'personal' && otherUserStatus?.emoji && (
|
||||
<span
|
||||
className="text-sm leading-none"
|
||||
title={otherUserStatus.text || undefined}
|
||||
>
|
||||
{otherUserStatus.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-sm font-bold tracking-tight truncate ${isActive ? 'text-primary' : 'text-on-surface'}`}>{chatName}</span>
|
||||
</div>
|
||||
{timeStr && <span className="text-[10px] uppercase font-bold tracking-wider text-on-surface-variant/50 flex-shrink-0 ml-2">{timeStr}</span>}
|
||||
|
||||
@@ -8,18 +8,25 @@ export class UserApi {
|
||||
}
|
||||
|
||||
static async getUser(id: string) {
|
||||
// Конечная точка в новом бэкенде: GET /api/profiles/{id}
|
||||
return httpClient.request<User>(`/profiles/${id}`);
|
||||
// Основной путь по ТЗ: GET /api/users/{id}; старый профильный путь оставлен на бэкенде.
|
||||
return httpClient.request<User>(`/users/${id}`);
|
||||
}
|
||||
|
||||
static async updateProfile(data: { displayName?: string; bio?: string; birthday?: string | null }) {
|
||||
// Конечная точка в новом бэкенде: PUT /api/profiles/profile
|
||||
static async updateProfile(data: {
|
||||
displayName?: string;
|
||||
bio?: string;
|
||||
birthday?: string | null;
|
||||
statusText?: string | null;
|
||||
statusEmoji?: string | null;
|
||||
statusExpiresAt?: string | null;
|
||||
}) {
|
||||
// Основной путь по ТЗ: PATCH /api/user/profile
|
||||
const payload = {
|
||||
...data,
|
||||
about: data.bio // Дублируем для совместимости
|
||||
};
|
||||
return httpClient.request<User>('/profiles/profile', {
|
||||
method: 'PUT',
|
||||
return httpClient.request<User>('/user/profile', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
User, Camera, Edit3, Calendar, Info,
|
||||
MapPin, AtSign, Check, Loader2, LogOut,
|
||||
ChevronRight, ArrowLeft, Languages, Palette, Trash2,
|
||||
Bell, Shield, Eye
|
||||
Bell, Shield, Eye, Smile
|
||||
} from 'lucide-react';
|
||||
import { useAuthStore } from '../../../auth/application/authStore';
|
||||
import { useLang } from '../../../../core/infrastructure/i18n';
|
||||
@@ -14,6 +14,16 @@ import DatePicker from '../../../../core/presentation/components/ui/DatePicker';
|
||||
import AvatarCropModal from './AvatarCropModal';
|
||||
import { Area } from 'react-easy-crop';
|
||||
import { getCroppedImg } from '../../../../core/utils/cropImage';
|
||||
import EmojiOnlyPicker from '../../../stories/presentation/components/EmojiOnlyPicker';
|
||||
|
||||
const BIO_MAX_LENGTH = 200;
|
||||
const STATUS_MAX_LENGTH = 50;
|
||||
const STATUS_PRESETS = [
|
||||
{ emoji: '📞', textRu: 'На созвоне', textEn: 'On a call' },
|
||||
{ emoji: '🍔', textRu: 'Обед', textEn: 'Lunch' },
|
||||
{ emoji: '💻', textRu: 'Работаю', textEn: 'Working' },
|
||||
{ emoji: '💤', textRu: 'Сплю', textEn: 'Sleeping' },
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, updateUser, logout } = useAuthStore();
|
||||
@@ -23,9 +33,13 @@ export default function SettingsPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
displayName: user?.displayName || '',
|
||||
bio: user?.bio || '',
|
||||
birthday: user?.birthday || ''
|
||||
birthday: user?.birthday || '',
|
||||
statusText: user?.statusText || '',
|
||||
statusEmoji: user?.statusEmoji || '💬',
|
||||
statusDuration: 'none' as 'none' | '1h' | '1d'
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showStatusPicker, setShowStatusPicker] = useState(false);
|
||||
|
||||
// Avatar cropping state
|
||||
const [cropModal, setCropModal] = useState<{
|
||||
@@ -41,7 +55,10 @@ export default function SettingsPage() {
|
||||
setFormData({
|
||||
displayName: user.displayName || '',
|
||||
bio: user.bio || '',
|
||||
birthday: user.birthday || ''
|
||||
birthday: user.birthday || '',
|
||||
statusText: user.statusText || '',
|
||||
statusEmoji: user.statusEmoji || '💬',
|
||||
statusDuration: 'none'
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
@@ -51,6 +68,15 @@ export default function SettingsPage() {
|
||||
try {
|
||||
const payload = {
|
||||
...formData,
|
||||
bio: formData.bio.slice(0, BIO_MAX_LENGTH),
|
||||
statusText: formData.statusText.trim() ? formData.statusText.slice(0, STATUS_MAX_LENGTH) : null,
|
||||
statusEmoji: formData.statusText.trim() ? formData.statusEmoji : null,
|
||||
statusExpiresAt:
|
||||
formData.statusText.trim() && formData.statusDuration !== 'none'
|
||||
? new Date(
|
||||
Date.now() + (formData.statusDuration === '1h' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000)
|
||||
).toISOString()
|
||||
: null,
|
||||
birthday: formData.birthday || null
|
||||
};
|
||||
const updatedUser = await UserApi.updateProfile(payload);
|
||||
@@ -237,7 +263,7 @@ export default function SettingsPage() {
|
||||
{editing ? (
|
||||
<textarea
|
||||
value={formData.bio}
|
||||
onChange={(e) => setFormData(p => ({ ...p, bio: e.target.value }))}
|
||||
onChange={(e) => setFormData(p => ({ ...p, bio: e.target.value.slice(0, BIO_MAX_LENGTH) }))}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-3 text-sm font-medium text-white/80 focus:border-primary/50 transition-all outline-none resize-none h-32"
|
||||
placeholder={t('tellAboutBio')}
|
||||
/>
|
||||
@@ -246,6 +272,11 @@ export default function SettingsPage() {
|
||||
{user?.bio || t('noBio')}
|
||||
</p>
|
||||
)}
|
||||
{editing && (
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-white/35 text-right">
|
||||
{formData.bio.length}/{BIO_MAX_LENGTH}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 p-8 rounded-[2.5rem] bg-white/[0.02] border border-white/5 transition-colors hover:bg-white/[0.04]">
|
||||
@@ -266,6 +297,87 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 p-8 rounded-[2.5rem] bg-white/[0.02] border border-white/5 transition-colors hover:bg-white/[0.04]">
|
||||
<div className="flex items-center gap-3 text-primary mb-1">
|
||||
<Smile size={16} />
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em]">
|
||||
{lang === 'ru' ? 'Статус' : 'Status'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{STATUS_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={`${preset.emoji}-${preset.textRu}`}
|
||||
onClick={() => setFormData((p) => ({
|
||||
...p,
|
||||
statusEmoji: preset.emoji,
|
||||
statusText: lang === 'ru' ? preset.textRu : preset.textEn,
|
||||
}))}
|
||||
className="px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-xs font-bold text-white/80 hover:border-primary/40"
|
||||
>
|
||||
{preset.emoji} {lang === 'ru' ? preset.textRu : preset.textEn}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowStatusPicker(true)}
|
||||
className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-white text-sm font-bold"
|
||||
>
|
||||
{formData.statusEmoji}
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.statusText}
|
||||
onChange={(e) => setFormData((p) => ({ ...p, statusText: e.target.value.slice(0, STATUS_MAX_LENGTH) }))}
|
||||
className="flex-1 bg-white/5 border border-white/10 rounded-2xl px-4 py-2.5 text-sm font-medium text-white/90 focus:border-primary/50 transition-all outline-none"
|
||||
placeholder={lang === 'ru' ? 'Что у вас сейчас?' : 'What are you up to?'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ id: 'none', label: lang === 'ru' ? 'Без таймера' : 'No timer' },
|
||||
{ id: '1h', label: lang === 'ru' ? '1 час' : '1 hour' },
|
||||
{ id: '1d', label: lang === 'ru' ? '1 день' : '1 day' },
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
onClick={() => setFormData((p) => ({ ...p, statusDuration: opt.id as 'none' | '1h' | '1d' }))}
|
||||
className={`px-3 py-2 rounded-xl text-xs font-black uppercase tracking-wider border ${
|
||||
formData.statusDuration === opt.id
|
||||
? 'bg-primary/20 border-primary/40 text-primary'
|
||||
: 'bg-white/5 border-white/10 text-white/60'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-black uppercase tracking-widest text-white/35">
|
||||
{formData.statusText.length}/{STATUS_MAX_LENGTH}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setFormData((p) => ({ ...p, statusText: '', statusEmoji: '💬', statusDuration: 'none' }))}
|
||||
className="text-[10px] font-black uppercase tracking-widest text-red-400/80 hover:text-red-400"
|
||||
>
|
||||
{lang === 'ru' ? 'Очистить статус' : 'Clear status'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex px-4 py-2 rounded-full bg-white/5 border border-white/10 text-sm font-bold text-white">
|
||||
{user?.statusEmoji || '💬'} {user?.statusText || (lang === 'ru' ? 'Статус не указан' : 'No status')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* App Settings */}
|
||||
<div className="space-y-6 pt-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
@@ -324,6 +436,17 @@ export default function SettingsPage() {
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>
|
||||
{showStatusPicker && (
|
||||
<EmojiOnlyPicker
|
||||
onSelect={(emoji) => {
|
||||
setFormData((p) => ({ ...p, statusEmoji: emoji }));
|
||||
setShowStatusPicker(false);
|
||||
}}
|
||||
onClose={() => setShowStatusPicker(false)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLang } from '../../../../core/infrastructure/i18n';
|
||||
import { useAuthStore } from '../../../auth/application/authStore';
|
||||
import { useFriendStore } from '../../../friends/application/friendStore';
|
||||
import { ChatApi } from '../../../chats/infrastructure/chatApi';
|
||||
import { httpClient } from '../../../../core/infrastructure/httpClient';
|
||||
import { UserApi } from '../../infrastructure/userApi';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox';
|
||||
import type { FriendWithId, FriendRequest } from '../../../../core/domain/types';
|
||||
@@ -51,7 +51,7 @@ export default function UserProfile({ userId, onClose, onMessage, isSelf: isSelf
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
try {
|
||||
const res = await httpClient.request<any>(`/profiles/${userId}`);
|
||||
const res = await UserApi.getUser(userId);
|
||||
setUser(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -242,6 +242,13 @@ export default function UserProfile({ userId, onClose, onMessage, isSelf: isSelf
|
||||
<div className="px-4 py-1.5 rounded-full bg-primary/15 border border-primary/25">
|
||||
<span className="text-xs font-black text-primary uppercase tracking-widest">@{user.username || 'user_' + userId.slice(0, 5)}</span>
|
||||
</div>
|
||||
{(user.statusEmoji || user.statusText) && (
|
||||
<div className="px-4 py-1.5 rounded-full bg-white/5 border border-white/10">
|
||||
<span className="text-xs font-black text-white tracking-wide">
|
||||
{user.statusEmoji || '💬'} {user.statusText || (lang === 'ru' ? 'Статус' : 'Status')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs text-white/40 font-black uppercase tracking-widest"><Calendar size={14} className="text-primary/50" /><span>{t('joined')} {formatRegistrationDate(user.createdAt)}</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user