Add user avatar URL
This commit is contained in:
@@ -74,5 +74,18 @@ public class UserController : BaseController
|
||||
var updatedUser = await _userService.UpdateAgentName(user, agentName);
|
||||
return Ok(updatedUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the avatar URL for the current user.
|
||||
/// </summary>
|
||||
/// <param name="avatarUrl">The new avatar URL to set.</param>
|
||||
/// <returns>The updated user with the new avatar URL.</returns>
|
||||
[HttpPut("avatar")]
|
||||
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
|
||||
{
|
||||
var user = await GetUser();
|
||||
var updatedUser = await _userService.UpdateAvatarUrl(user, avatarUrl);
|
||||
return Ok(updatedUser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ public interface IUserService
|
||||
Task<User> Authenticate(string name, string address, string message, string signature);
|
||||
Task<User> GetUserByAddressAsync(string address);
|
||||
Task<User> UpdateAgentName(User user, string agentName);
|
||||
Task<User> UpdateAvatarUrl(User user, string avatarUrl);
|
||||
User GetUser(string name);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using System.Text.RegularExpressions;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Common;
|
||||
using Managing.Domain.Accounts;
|
||||
@@ -134,11 +135,34 @@ public class UserService : IUserService
|
||||
var existingUser = await _userRepository.GetUserByAgentNameAsync(agentName);
|
||||
if (existingUser != null)
|
||||
{
|
||||
throw new Exception("Agent name already used");
|
||||
throw new Exception($"Agent name already used by {existingUser.Name}");
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
user.AgentName = agentName;
|
||||
await _userRepository.UpdateUser(user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<User> UpdateAvatarUrl(User user, string avatarUrl)
|
||||
{
|
||||
// Validate URL format and image extension
|
||||
if (!Uri.TryCreate(avatarUrl, UriKind.Absolute, out Uri? uriResult) ||
|
||||
(uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
throw new Exception("Invalid URL format");
|
||||
}
|
||||
|
||||
// Check for valid image extension
|
||||
string pattern = @"\.(jpeg|jpg|png)$";
|
||||
if (!Regex.IsMatch(avatarUrl, pattern, RegexOptions.IgnoreCase))
|
||||
{
|
||||
throw new Exception("URL must point to a JPEG or PNG image");
|
||||
}
|
||||
|
||||
user.AvatarUrl = avatarUrl;
|
||||
await _userRepository.UpdateUser(user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,5 @@ public class User
|
||||
public string Name { get; set; }
|
||||
public List<Account> Accounts { get; set; }
|
||||
public string AgentName { get; set; }
|
||||
public string AvatarUrl { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ public class UserDto : Document
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string AgentName { get; set; }
|
||||
public string AvatarUrl { get; set; }
|
||||
}
|
||||
@@ -532,7 +532,8 @@ public static class MongoMappers
|
||||
return new User
|
||||
{
|
||||
Name = user.Name,
|
||||
AgentName = user.AgentName
|
||||
AgentName = user.AgentName,
|
||||
AvatarUrl = user.AvatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -541,7 +542,8 @@ public static class MongoMappers
|
||||
return new UserDto
|
||||
{
|
||||
Name = user.Name,
|
||||
AgentName = user.AgentName
|
||||
AgentName = user.AgentName,
|
||||
AvatarUrl = user.AvatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
var dto = await _userRepository.FindOneAsync(u => u.Name == user.Name);
|
||||
dto.AgentName = user.AgentName;
|
||||
dto.AvatarUrl = user.AvatarUrl;
|
||||
_userRepository.Update(dto);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -459,8 +459,6 @@ export const closeGmxPositionImpl = async (
|
||||
return position.marketInfo.indexToken.symbol === ticker && position.isLong === (direction === TradeDirection.Long);
|
||||
});
|
||||
|
||||
console.log("positionsInfo", positionsInfo);
|
||||
|
||||
if (!positionKey) {
|
||||
throw new Error(`No open ${direction} position found for ${ticker}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {TradeDirection} from '../../src/generated/ManagingApiTypes'
|
||||
|
||||
test('GMX Position Closing', async (t) => {
|
||||
await t.test('should close a long position for BTC', async () => {
|
||||
const sdk = await getClientForAddress('0x932167388dD9aad41149b3cA23eBD489E2E2DD78')
|
||||
const sdk = await getClientForAddress('0xbBA4eaA534cbD0EcAed5E2fD6036Aec2E7eE309f')
|
||||
|
||||
const result = await closeGmxPositionImpl(
|
||||
sdk,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TradeChart, CardPositionItem } from '..'
|
||||
import { Backtest, MoneyManagement } from '../../../generated/ManagingApi'
|
||||
import {CardPositionItem, TradeChart} from '..'
|
||||
import {Backtest} from '../../../generated/ManagingApi'
|
||||
import {CardPosition, CardText} from '../../mollecules'
|
||||
|
||||
interface IBacktestRowDetailsProps {
|
||||
@@ -21,7 +21,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
strategiesValues,
|
||||
signals,
|
||||
statistics,
|
||||
moneyManagement
|
||||
config
|
||||
} = backtest;
|
||||
|
||||
return (
|
||||
@@ -71,8 +71,8 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
||||
<CardText
|
||||
title="Money Management"
|
||||
content={
|
||||
"SL: " +(moneyManagement?.stopLoss * 100).toFixed(2) + "% TP: " +
|
||||
(moneyManagement?.takeProfit * 100).toFixed(2) + "%"
|
||||
"SL: " +(config.moneyManagement?.stopLoss * 100).toFixed(2) + "% TP: " +
|
||||
(config.moneyManagement?.takeProfit * 100).toFixed(2) + "%"
|
||||
}
|
||||
></CardText>
|
||||
<CardText
|
||||
|
||||
@@ -2323,6 +2323,45 @@ export class UserClient extends AuthorizedApiBase {
|
||||
}
|
||||
return Promise.resolve<User>(null as any);
|
||||
}
|
||||
|
||||
user_UpdateAvatarUrl(avatarUrl: string): Promise<User> {
|
||||
let url_ = this.baseUrl + "/User/avatar";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(avatarUrl);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processUser_UpdateAvatarUrl(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processUser_UpdateAvatarUrl(response: Response): Promise<User> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as User;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<User>(null as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkflowClient extends AuthorizedApiBase {
|
||||
@@ -2519,6 +2558,7 @@ export interface User {
|
||||
name?: string | null;
|
||||
accounts?: Account[] | null;
|
||||
agentName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
|
||||
@@ -10,8 +10,13 @@ type UpdateAgentNameForm = {
|
||||
agentName: string
|
||||
}
|
||||
|
||||
type UpdateAvatarForm = {
|
||||
avatarUrl: string
|
||||
}
|
||||
|
||||
function UserInfoSettings() {
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false)
|
||||
const [showAvatarModal, setShowAvatarModal] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const api = new UserClient({}, apiUrl)
|
||||
@@ -22,12 +27,18 @@ function UserInfoSettings() {
|
||||
})
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
register: registerAgentName,
|
||||
handleSubmit: handleSubmitAgentName,
|
||||
formState: { errors: agentNameErrors },
|
||||
} = useForm<UpdateAgentNameForm>()
|
||||
|
||||
const onSubmit = async (data: UpdateAgentNameForm) => {
|
||||
const {
|
||||
register: registerAvatar,
|
||||
handleSubmit: handleSubmitAvatar,
|
||||
formState: { errors: avatarErrors },
|
||||
} = useForm<UpdateAvatarForm>()
|
||||
|
||||
const onSubmitAgentName = async (data: UpdateAgentNameForm) => {
|
||||
const toast = new Toast('Updating agent name')
|
||||
try {
|
||||
await api.user_UpdateAgentName(data.agentName)
|
||||
@@ -40,6 +51,19 @@ function UserInfoSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmitAvatar = async (data: UpdateAvatarForm) => {
|
||||
const toast = new Toast('Updating avatar')
|
||||
try {
|
||||
await api.user_UpdateAvatarUrl(data.avatarUrl)
|
||||
queryClient.invalidateQueries({ queryKey: ['user'] })
|
||||
setShowAvatarModal(false)
|
||||
toast.update('success', 'Avatar updated successfully')
|
||||
} catch (error) {
|
||||
console.error('Error updating avatar:', error)
|
||||
toast.update('error', 'Failed to update avatar')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="bg-base-200 rounded-lg p-6 shadow-lg">
|
||||
@@ -61,13 +85,36 @@ function UserInfoSettings() {
|
||||
Update Agent Name
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold">Avatar:</label>
|
||||
<div className="mt-2 flex items-center space-x-4">
|
||||
{user?.avatarUrl ? (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt="User avatar"
|
||||
className="w-16 h-16 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full bg-base-300 flex items-center justify-center">
|
||||
<span className="text-2xl">{user?.name?.[0]?.toUpperCase() || '?'}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setShowAvatarModal(true)}
|
||||
>
|
||||
Update Avatar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
showModal={showUpdateModal}
|
||||
onClose={() => setShowUpdateModal(false)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
onSubmit={handleSubmitAgentName(onSubmitAgentName)}
|
||||
titleHeader="Update Agent Name"
|
||||
>
|
||||
<div className="form-control w-full">
|
||||
@@ -77,13 +124,50 @@ function UserInfoSettings() {
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
{...register('agentName', { required: 'Agent name is required' })}
|
||||
{...registerAgentName('agentName', { required: 'Agent name is required' })}
|
||||
defaultValue={user?.agentName || ''}
|
||||
/>
|
||||
{errors.agentName && (
|
||||
{agentNameErrors.agentName && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">
|
||||
{errors.agentName.message}
|
||||
{agentNameErrors.agentName.message}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button type="submit" className="btn">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
showModal={showAvatarModal}
|
||||
onClose={() => setShowAvatarModal(false)}
|
||||
onSubmit={handleSubmitAvatar(onSubmitAvatar)}
|
||||
titleHeader="Update Avatar"
|
||||
>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text">Avatar URL (JPEG or PNG)</span>
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
className="input input-bordered w-full"
|
||||
{...registerAvatar('avatarUrl', {
|
||||
required: 'Avatar URL is required',
|
||||
pattern: {
|
||||
value: /^https?:\/\/.+\.(jpeg|jpg|png)$/i,
|
||||
message: 'URL must be a valid image URL ending in .jpeg, .jpg, or .png'
|
||||
}
|
||||
})}
|
||||
defaultValue={user?.avatarUrl || ''}
|
||||
/>
|
||||
{avatarErrors.avatarUrl && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">
|
||||
{avatarErrors.avatarUrl.message}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user