diff --git a/ShoppingAssistantWebClient.Web/Models/Role.cs b/ShoppingAssistantWebClient.Web/Models/Role.cs new file mode 100644 index 0000000..26df7fc --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Models/Role.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ShoppingAssistantWebClient.Web.Models +{ + public class Role + { + public string Id { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/ShoppingAssistantWebClient.Web/Models/User.cs b/ShoppingAssistantWebClient.Web/Models/User.cs new file mode 100644 index 0000000..fcce482 --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Models/User.cs @@ -0,0 +1,17 @@ +using ShoppingAssistantWebClient.Web.Models; + +namespace ShoppingAssistantWebClient.Web.Models +{ + public class User + { + public string Id { get; set; } + + public string GuestId { get; set; } + + public List? Roles { get; set; } = new List(); + + public string Email { get; set; } + + public string Phone { get; set; } + } +} \ No newline at end of file diff --git a/ShoppingAssistantWebClient.Web/Network/ApiClient.cs b/ShoppingAssistantWebClient.Web/Network/ApiClient.cs index a4a6f24..1c8d77f 100644 --- a/ShoppingAssistantWebClient.Web/Network/ApiClient.cs +++ b/ShoppingAssistantWebClient.Web/Network/ApiClient.cs @@ -101,17 +101,24 @@ public class ApiClient await SetAuthenticationAsync(); var count = 0; // var requestUrl = $"{_httpClient.BaseAddress}{url}"; - var response = await _httpClient.PostAsJsonAsync(requestUrl, obj); - using var responseStream = await response.Content.ReadAsStreamAsync(); - using var reader = new StreamReader(responseStream, Encoding.UTF8); + var jsonBody = JsonConvert.SerializeObject(obj); - SearchEventType eventType = SearchEventType.Message; - while (!cancellationToken.IsCancellationRequested) + var body = new StringContent(jsonBody, Encoding.UTF8, "application/json"); + var request = new HttpRequestMessage(HttpMethod.Post, requestUrl) { - var jsonChunk = await reader.ReadLineAsync(cancellationToken); + Content = body + }; + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + + using var httpResponse = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + using var streamReader = new StreamReader(await httpResponse.Content.ReadAsStreamAsync(cancellationToken)); + var eventType = SearchEventType.Message; + while (!streamReader.EndOfStream) + { + var jsonChunk = await streamReader.ReadLineAsync(cancellationToken); count += 1; // if (count >=5 ){ // - break; // + yield break; // }; // if (jsonChunk == null) continue; if (jsonChunk.StartsWith("event: ")) diff --git a/ShoppingAssistantWebClient.Web/Pages/Cards.razor b/ShoppingAssistantWebClient.Web/Pages/Cards.razor index 4bd8442..ec83fce 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Cards.razor +++ b/ShoppingAssistantWebClient.Web/Pages/Cards.razor @@ -96,7 +96,7 @@
- +
} diff --git a/ShoppingAssistantWebClient.Web/Pages/Cards.razor.cs b/ShoppingAssistantWebClient.Web/Pages/Cards.razor.cs index 2a9fd60..1bddd24 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Cards.razor.cs +++ b/ShoppingAssistantWebClient.Web/Pages/Cards.razor.cs @@ -2,8 +2,6 @@ using Microsoft.AspNetCore.Components; using ShoppingAssistantWebClient.Web.Models; using ShoppingAssistantWebClient.Web.Network; using GraphQL; -using Newtonsoft.Json; -using Microsoft.JSInterop; using ShoppingAssistantWebClient.Web.Services; namespace ShoppingAssistantWebClient.Web.Pages; diff --git a/ShoppingAssistantWebClient.Web/Pages/Chat.razor b/ShoppingAssistantWebClient.Web/Pages/Chat.razor index bdef32e..e938d12 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Chat.razor +++ b/ShoppingAssistantWebClient.Web/Pages/Chat.razor @@ -19,6 +19,8 @@
@name
+ +
@@ -33,9 +35,27 @@ if (item.Role != "User") { -
  • -

    @item.Text

    -
  • + if (@item.Text == "Waiting for response") + { + +
    Waiting for response + +
    + +
    + + + + } + else + { + +
  • + @item.Text +
  • + + } + } else @@ -52,34 +72,34 @@ +
    +
    -
    + @if (Suggestion.Count != 0) + { +
    Several possible options
    -
    - - @if (Suggestion.Count != 0) - { - -
    Several possible options
    - -
    +
    - @foreach (var item in Suggestion) - { + @foreach (var item in Suggestion) + { -
    - @item -
    - } +
    + @item +
    + } + + +
    + } + +
    -
    - } -
    @@ -105,16 +125,31 @@ }; function myJavaScriptFunction(wishlistId) { - UpdateMenu(wishlistId); + UpdateMenu(wishlistId); } + + + document.getElementById('button_open').addEventListener('click', changetyle); + + @code { [Parameter] public string chatId { get; set; } - public string inputValue = ""; + public string inputValue = ""; protected override async Task OnParametersSetAsync() { @@ -139,7 +174,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { + await JSRuntime.InvokeVoidAsync("myJavaScriptHeight"); await JSRuntime.InvokeVoidAsync("scrollToBottom", chatMessageRef); + } private async Task UpdateSideMenu(string wishlistId) @@ -151,6 +188,7 @@ private void ClickOption(string item) { inputValue = item; + AddNewMessage(inputValue); } } \ No newline at end of file diff --git a/ShoppingAssistantWebClient.Web/Pages/Chat.razor.cs b/ShoppingAssistantWebClient.Web/Pages/Chat.razor.cs index 99eee0a..b59a8c1 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Chat.razor.cs +++ b/ShoppingAssistantWebClient.Web/Pages/Chat.razor.cs @@ -15,100 +15,107 @@ namespace ShoppingAssistantWebClient.Web.Pages; public partial class Chat : ComponentBase { - [Inject] - private ApiClient _apiClient { get; set; } - [Inject] - private NavigationManager Navigation { get; set; } - [Inject] - private SearchService _searchServise { get; set; } + [Inject] + private ApiClient _apiClient { get; set; } + [Inject] + private NavigationManager Navigation { get; set; } + [Inject] + private SearchService _searchServise { get; set; } - public List Messages { get; set; } + public List Messages { get; set; } - public List Products { get; set; } = new List(); + public List Products { get; set; } = new List(); - public List Suggestion { get; set; } = new List(); - - public Messages Message { get; set; } - public Messages MessageBot { get; set; } + public List Suggestion { get; set; } = new List(); - private CancellationTokenSource cancelTokenSource; - private bool isWaitingForResponse = false; - private MessageCreateDto messageCreateDto; - public bool isLoading = true; - private string name = ""; - protected override async Task OnInitializedAsync() + public Messages Message { get; set; } + public Messages MessageBot { get; set; } + + private CancellationTokenSource cancelTokenSource; + private bool isWaitingForResponse = false; + private MessageCreateDto messageCreateDto; + public bool isLoading = true; + private string name = ""; + protected override async Task OnInitializedAsync() + { + try { - try{ - var input = _searchServise.FirstMessage; + var input = _searchServise.FirstMessage; - if (input!=null){ + if (input != null) + { - await LoadMessages(); + await LoadMessages(); - await AddNewMessage(input); + await AddNewMessage(input); - string wishlistId = chatId; - var request = new GraphQLRequest - { - Query = @"mutation GenerateNameForPersonalWishlist($wishlistId: String!) { + string wishlistId = chatId; + var request = new GraphQLRequest + { + Query = @"mutation GenerateNameForPersonalWishlist($wishlistId: String!) { generateNameForPersonalWishlist(wishlistId: $wishlistId) { id name } }", - Variables = new - { - wishlistId + Variables = new + { + wishlistId - } - }; - - var response = await _apiClient.QueryAsync(request); - _searchServise.SetFirstMessage(null); - isLoading = false; - await UpdateSideMenu(wishlistId); - StateHasChanged(); - - }else{ - await LoadMessages(); } - }catch(Exception ex){ - Console.WriteLine($"Error OnInitializedAsync: {ex.Message}"); + }; + + var response = await _apiClient.QueryAsync(request); + _searchServise.SetFirstMessage(null); + isLoading = false; + await UpdateSideMenu(wishlistId); + StateHasChanged(); + } - + else + { + await LoadMessages(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error OnInitializedAsync: {ex.Message}"); } + } - private async Task LoadMessages() + + private async Task LoadMessages() + { + try { - try{ - string wishlistId = chatId; - - var request = new GraphQLRequest - { - Query = @"query PersonalWishlist( $wishlistId: String!) { + string wishlistId = chatId; + + var request = new GraphQLRequest + { + Query = @"query PersonalWishlist( $wishlistId: String!) { personalWishlist(wishlistId: $wishlistId) { name } }", - Variables = new - { - wishlistId, - } - }; + Variables = new + { + wishlistId, + } + }; var response = await _apiClient.QueryAsync(request); var responseData = response.Data; name = responseData.personalWishlist.name; - isLoading = true; - int pageNumber = 1; - request = new GraphQLRequest - { - Query = @"query MessagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { + isLoading = true; + int pageNumber = 1; + request = new GraphQLRequest + { + Query = @"query MessagesPageFromPersonalWishlist($wishlistId: String!, $pageNumber: Int!, $pageSize: Int!) { messagesPageFromPersonalWishlist( wishlistId: $wishlistId, pageNumber: $pageNumber, pageSize: $pageSize) { items { @@ -120,112 +127,125 @@ public partial class Chat : ComponentBase } }", - Variables = new - { - wishlistId, - pageNumber, - pageSize = 200 - } - }; - - - - response = await _apiClient.QueryAsync(request); - responseData = response.Data; - var jsonCategoriesResponse = JsonConvert.SerializeObject(responseData.messagesPageFromPersonalWishlist.items); - this.Messages = JsonConvert.DeserializeObject>(jsonCategoriesResponse); - Messages.Reverse(); - isLoading = false; + Variables = new + { + wishlistId, + pageNumber, + pageSize = 200 + } + }; + + + + response = await _apiClient.QueryAsync(request); + responseData = response.Data; + var jsonCategoriesResponse = JsonConvert.SerializeObject(responseData.messagesPageFromPersonalWishlist.items); + this.Messages = JsonConvert.DeserializeObject>(jsonCategoriesResponse); + Messages.Reverse(); + isLoading = false; - }catch(Exception ex){ - Console.WriteLine($"Error : {ex.Message}"); - } } + catch (Exception ex) + { + Console.WriteLine($"Error : {ex.Message}"); + } + } private async Task AddNewMessage(string inputMessage) { - if (!isWaitingForResponse && !string.IsNullOrWhiteSpace(inputMessage)) + if (!isWaitingForResponse && !string.IsNullOrWhiteSpace(inputMessage)) { JSRuntime.InvokeVoidAsync("clearInput"); isWaitingForResponse = true; - try{ - messageCreateDto = new MessageCreateDto { Text = inputMessage };; - Message = new Messages(); - Message.Text = inputMessage; - Message.Role = "User"; - Message.Id = ""; - Message.CreatedById = ""; - - Suggestion = new List(); - Products = new List(); - Messages.Add(Message); - StateHasChanged(); - - cancelTokenSource = new CancellationTokenSource(); - var cancellationToken = cancelTokenSource.Token; - - var serverSentEvent = _apiClient.GetServerSentEventStreamed($"ProductsSearch/search/{chatId}", messageCreateDto, cancellationToken); - bool first = true; - - MessageBot = new Messages(); - MessageBot.Role = "bot"; - MessageBot.Id = ""; - MessageBot.CreatedById = ""; - MessageBot.Text = "Waiting for response"; - Messages.Add(MessageBot); - var lengt = Messages.Count(); - StateHasChanged(); - - await foreach (var sseEvent in serverSentEvent.WithCancellation(cancellationToken)) - { - Console.WriteLine($"Received SSE Event: {sseEvent.Event}, Data: {sseEvent.Data}"); - - string input = sseEvent.Data; - Regex regex = new Regex("\"(.*?)\""); - Match match = regex.Match(input); - string result = match.Groups[1].Value; - - if(sseEvent.Event == SearchEventType.Message){ - - - if (first) - { - Messages[lengt-1].Text = result; - first = false; - } - else - { - Messages[lengt-1].Text += result; - } + try + { + messageCreateDto = new MessageCreateDto { Text = inputMessage }; ; + Message = new Messages(); + Message.Text = inputMessage; + Message.Role = "User"; + Message.Id = ""; + Message.CreatedById = ""; + Suggestion = new List(); + Products = new List(); + Messages.Add(Message); StateHasChanged(); - - } else if(sseEvent.Event == SearchEventType.Product){ - - string pattern = "[\\\\\"]"; - input = Regex.Replace(input, pattern, ""); + cancelTokenSource = new CancellationTokenSource(); + var cancellationToken = cancelTokenSource.Token; - Products.Add(input); + var serverSentEvent = _apiClient.GetServerSentEventStreamed($"ProductsSearch/search/{chatId}", messageCreateDto, cancellationToken); + bool first = true; - } else if(sseEvent.Event == SearchEventType.Suggestion){ + MessageBot = new Messages(); + MessageBot.Role = "bot"; + MessageBot.Id = ""; + MessageBot.CreatedById = ""; + MessageBot.Text = "Waiting for response"; + Messages.Add(MessageBot); + var lengt = Messages.Count(); + StateHasChanged(); + + await foreach (var sseEvent in serverSentEvent.WithCancellation(cancellationToken)) + { + Console.WriteLine($"Received SSE Event: {sseEvent.Event}, Data: {sseEvent.Data}"); + + string input = sseEvent.Data; + Regex regex = new Regex("\"(.*?)\""); + Match match = regex.Match(input); + string result = match.Groups[1].Value; + + if (sseEvent.Event == SearchEventType.Message) + { + if (first) + { + Messages[lengt - 1].Text = result; + first = false; + } + else + { + Messages[lengt - 1].Text += result; + } + + StateHasChanged(); + + } + else if (sseEvent.Event == SearchEventType.Product) + { + + string pattern = "[\\\\\"]"; + + input = Regex.Replace(input, pattern, ""); + + Products.Add(input); + + } + else if (sseEvent.Event == SearchEventType.Suggestion) + { + if (Suggestion.Count < 3) + { + Suggestion.Add(result); + StateHasChanged(); + } + } + } + + if (Products.Count != 0) + { + string n = name; + _searchServise.SetProducts(Products); + Products = null; + var url = $"/cards/{name}/{chatId}"; + Navigation.NavigateTo(url); + } + isWaitingForResponse = false; - Suggestion.Add(result); } - - } - if(Products.Count!=0) { - string n = name; - _searchServise.SetProducts(Products); - Products = null; - var url = $"/cards/{name}/{chatId}"; - Navigation.NavigateTo(url); - } - isWaitingForResponse = false; - } catch(Exception ex){ + catch (Exception ex) + { Console.WriteLine($"Error : {ex.Message}"); + } } } - } } diff --git a/ShoppingAssistantWebClient.Web/Pages/Chat.razor.css b/ShoppingAssistantWebClient.Web/Pages/Chat.razor.css index ef44028..0fb9912 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Chat.razor.css +++ b/ShoppingAssistantWebClient.Web/Pages/Chat.razor.css @@ -7,7 +7,7 @@ width: 100%; } - + .button_open_menu { z-index: 2; width: 1.43em; @@ -17,6 +17,10 @@ left: 1.56em; cursor: pointer; visibility: hidden; + + @media screen and (max-width: 900px) { + visibility: visible; + } } .button_open_menu span { @@ -38,52 +42,85 @@ } .title_one_frame { + white-space: nowrap; /* Запобігає переносу тексту на новий рядок */ + overflow: hidden; /* Сховує текст, який не влазить в блок */ + text-overflow: ellipsis; /* Додає три крапки на кінці обрізаного тексту */ + margin-left: 4em; + margin-right: 4em; padding-top: 1.25em; color: #0052CC; font-size: 1.0625em; text-align: center; } - + .chat_input { background-color: #EAEAEA; position: absolute; - display: flex; + display: inline-flex; + /* Использовать inline-flex-контейнер */ align-items: center; bottom: 2em; margin-left: 25%; width: 50%; border-radius: 0.6em; + + @media screen and (max-width: 750px) { + margin-left: 15%; + width: 70%; + + } + + @media screen and (max-width: 480px) { + margin-left: 2%; + width: 96%; + + } } + .possible_options { position: absolute; bottom: 5.5em; margin-left: 25%; width: 50%; border-radius: 0.6em; + + @media screen and (max-width: 750px) { + margin-left: 15%; + width: 70%; + + } + + @media screen and (max-width: 480px) { + margin-left: 2%; + width: 96%; + + } } -.tite_options{ + +.tite_options { font-size: 0.9em; color: #ADADAD; margin-bottom: 0.5em; } -.options{ + +.options { justify-content: space-between; align-items: center; font-size: 1em; } -.topic_options -{ - display: inline-block; - padding: 0.5em; - border: 0.09em solid; - border-color: #009FFF; - border-radius: 0.6em; - margin: 0em 0.6em; - flex: 1; - text-align: center; - cursor: pointer; + +.topic_options { + display: inline-block; + padding: 0.5em; + border: 0.09em solid; + border-color: #009FFF; + border-radius: 0.6em; + margin: 0.2em 0.2em; + flex: 1; + text-align: center; + cursor: pointer; } .input_messages { @@ -117,9 +154,10 @@ .chat_message { position: relative; overflow-y: auto; - height: calc(100% - 8em); + height: calc(100% - 8.5em); width: 100%; } + .chat_message::-webkit-scrollbar { border-radius: 20px; width: 0.2em; @@ -134,37 +172,99 @@ } - .chat_box{ - border-radius: 10px; - position: absolute; - margin-left: 25%; - margin-top: 35px; - width: 50%; - list-style: none; - padding:0; - +.chat_box { + border-radius: 10px; + position: absolute; + margin-left: 25%; + margin-top: 35px; + width: 50%; + list-style: none; + padding: 0; + + @media screen and (max-width: 750px) { + margin-left: 15%; + width: 70%; + } - .chat_outgoing{ - display: flex; + + @media screen and (max-width: 480px) { + margin-left: 4%; + width: 92%; + } - .chat_incoming{ - display: flex; +} + +.chat_outgoing { + display: flex; +} + +.chat_incoming { + display: inline-flex; + /* Использовать inline-flex-контейнер */ + align-items: center; + /* Выравнивание по центру */ + background-color: #EAEAEA; + border-radius: 10px; + color: black; + padding: 10px; + margin-bottom: 20px; + margin-top: 20px; + max-width: 70%; + /* Максимальная ширина по вашему усмотрению */ + min-width: 155px; + /* Максимальная ширина по вашему усмотрению */ +} + +.chat_incoming_wait { + display: inline-flex; /* Использовать inline-flex-контейнер */ + align-items: center; /* Выравнивание по центру */ + background-color: #EAEAEA; + border-radius: 10px; + color: black; + padding: 10px; + margin-bottom: 20px; + margin-top: 20px; + max-width: 70%; /* Максимальная ширина по вашему усмотрению */ + min-width: 155px; /* Максимальная ширина по вашему усмотрению */ +} + +.chat_box .chat_outgoing p { + margin-left: auto; + background-color: #009FFF; + border-radius: 10px; + color: white; + padding: 10px; + max-width: 60%; + +} + + +@keyframes spin { + 0% { + transform: rotate(0deg); } - .chat_box .chat_outgoing p { - margin-left: auto; - background-color: #009FFF; - border-radius: 10px; - color: white; - padding: 10px; - max-width: 60%; - + + 100% { + transform: rotate(360deg); } - .chat_box .chat_incoming p { - background-color: #EAEAEA; - border-radius: 10px; - color: black; - padding: 10px; - width: 60%; - margin-bottom: 20px ; - margin-top: 20px ; - } \ No newline at end of file +} + +.loading-spinner { + border: 4px solid rgba(0, 82, 204, 0.1); + border-top: 4px solid #0052CC; + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 1s linear infinite; + margin-left: 10px; + /* Добавлен отступ для разделения текста и загрузки */ +} + +.gradient { + background: linear-gradient(rgb(255, 255, 255), rgba(0, 0, 0, 0)); + position: absolute; + height: 50px; + width: 100%; + z-index: 1; + margin-top: -0.2em; +} \ No newline at end of file diff --git a/ShoppingAssistantWebClient.Web/Pages/ConfirmationModal.razor b/ShoppingAssistantWebClient.Web/Pages/ConfirmationModal.razor new file mode 100644 index 0000000..e43a5bb --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/ConfirmationModal.razor @@ -0,0 +1,24 @@ + + + +@code { + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = default!; + + private async Task Close() => await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + private async Task ConfirmDelete() => await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + private async Task Cancel() => await BlazoredModal.CloseAsync(ModalResult.Cancel()); + +} \ No newline at end of file diff --git a/ShoppingAssistantWebClient.Web/Pages/ConfirmationModal.razor.css b/ShoppingAssistantWebClient.Web/Pages/ConfirmationModal.razor.css new file mode 100644 index 0000000..a5ffb15 --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/ConfirmationModal.razor.css @@ -0,0 +1,48 @@ +.modal-dialog.modal-sm { + max-width: 300px; +} + +.medium-text { + font-size: 1rem; +} + +.btn-sm { + font-size: 0.9rem; + padding: 6px 12px; +} + +.btn-yes { + color: #FFFFFF; + background-color: #FF0000; +} + +.btn:active { + transform: scale(0.95); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.btn:focus { + outline: none !important; + box-shadow: none !important; +} + +.modal-header, .modal-footer { + padding: 8px; +} + +@media (max-width: 600px) { + .modal-dialog { + max-width: 100%; + margin: 10px auto; + } + .modal-content { + margin: 0; + } +} + + +.close { + background: none; + border: none; + outline: none; +} \ No newline at end of file diff --git a/ShoppingAssistantWebClient.Web/Pages/Index.razor b/ShoppingAssistantWebClient.Web/Pages/Index.razor index 2ac3083..f2cadad 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Index.razor +++ b/ShoppingAssistantWebClient.Web/Pages/Index.razor @@ -11,18 +11,18 @@
    -
    +
    New chat
    What you're looking for
    -
    +
    -
    - Gift -
    + +
    @@ -30,33 +30,36 @@
    @@ -70,11 +73,11 @@ var choose = "Product"; function switchGift() { - choose_gift.style.backgroundColor = "#0052CC"; - choose_product.style.backgroundColor = "transparent"; - switchGi.style.color = "white"; - switchProd.style.color = "#202124"; - choose = "Gift"; + // choose_gift.style.backgroundColor = "#0052CC"; + // choose_product.style.backgroundColor = "transparent"; + // switchGi.style.color = "white"; + //switchProd.style.color = "#202124"; + //choose = "Gift"; } @@ -88,17 +91,10 @@ } function myJavaScriptFunction(wishlistId) { - UpdateMenu(wishlistId); + UpdateMenu(wishlistId); } - - - - - - - document.getElementById('choose_gift').addEventListener('click', switchGift); document.getElementById('choose_product').addEventListener('click', switchProduct); @@ -110,17 +106,31 @@ private void Сhoose_product() { selectedChoice = "Product"; } - private void Сhoose_gift() { + private void Сhoose_gift() { selectedChoice = "Gift"; } private async Task UpdateSideMenu(string wishlistId) { - await JSRuntime.InvokeVoidAsync("myJavaScriptFunction", wishlistId); + await JSRuntime.InvokeVoidAsync("myJavaScriptFunction", wishlistId); } + + public void ClickTopic(int input) + { + if (input == 1){ + inputValue ="I need a present for a date"; + } + if (input == 2){ + inputValue ="I need a present for halloween"; + } + if (input == 3){ + inputValue ="I need a present for a birthday"; + } + CreateNewChat(); + } private void InputChanged(ChangeEventArgs e) { // Оновіть значення поля введення при кожному введенні тексту diff --git a/ShoppingAssistantWebClient.Web/Pages/Index.razor.cs b/ShoppingAssistantWebClient.Web/Pages/Index.razor.cs index c6cd6ab..7fd361c 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Index.razor.cs +++ b/ShoppingAssistantWebClient.Web/Pages/Index.razor.cs @@ -26,7 +26,6 @@ namespace ShoppingAssistantWebClient.Web.Pages private MessageCreateDto messageCreateDto; private string inputValue = ""; - private async Task CreateNewChat() { try diff --git a/ShoppingAssistantWebClient.Web/Pages/Index.razor.css b/ShoppingAssistantWebClient.Web/Pages/Index.razor.css index 0a7fe71..863df0f 100644 --- a/ShoppingAssistantWebClient.Web/Pages/Index.razor.css +++ b/ShoppingAssistantWebClient.Web/Pages/Index.razor.css @@ -16,7 +16,6 @@ left: 1.56em; cursor: pointer; visibility: hidden; - @media screen and (max-width: 900px) { visibility: visible; } @@ -62,6 +61,11 @@ font-size: 2.5em; text-align: center; font-weight: 600; + @media screen and (max-width: 480px) { + + font-size: 1.7125em; + + } } .title_three_frame { @@ -71,6 +75,13 @@ font-size: 1.25em; text-align: center; font-weight: 400; + @media screen and (max-width: 480px) { + + font-size: 1.0125em; + padding-left: 4%; + padding-right: 4%; + + } } .topic { @@ -80,6 +91,12 @@ color: #009FFF; width: 15.625em; margin: 0 auto; + @media screen and (max-width: 480px) { + + width: 11.875em; + font-size: 0.9125em; + + } } .topic div { @@ -106,6 +123,11 @@ border-radius: 0.6em; text-align: center; position: relative; + @media screen and (max-width: 480px) { + height: 2.4125em; + width: 16.875em; + + } } .switch_product { @@ -137,7 +159,41 @@ transition: 0.8s; color: #202124; } +button::before { + content: attr(text); +} + +button:hover::before { + content: attr(hover-text); +} +button { + outline: none; /* Для синий ободки */ + border: 0; + background: transparent; +} +.message { + right: 0; + position: absolute; + border-radius: 0.6em; + margin: 0.3125em; + width: calc(50% - 0.625em); + height: calc(100% - 0.625em); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: 0.8s; + color: #202124; + padding: 4px; /* Добавьте подходящий отступ, если необходимо */ +} +.hidden { + display: none; +} + +.show { + display: block; +} .chat_message { position: relative; overflow-y: auto; @@ -148,14 +204,25 @@ .chat_input { background-color: #EAEAEA; position: absolute; - display: flex; + display: inline-flex; /* Использовать inline-flex-контейнер */ align-items: center; bottom: 2em; margin-left: 25%; width: 50%; border-radius: 0.6em; + @media screen and (max-width: 750px) { + margin-left: 15%; + width: 70%; + + } + @media screen and (max-width: 480px) { + margin-left: 2%; + width: 96%; + + } } + .input_messages { width: 100%; height: 2.5em; diff --git a/ShoppingAssistantWebClient.Web/Pages/Login.cshtml b/ShoppingAssistantWebClient.Web/Pages/Login.cshtml deleted file mode 100644 index 198c17b..0000000 --- a/ShoppingAssistantWebClient.Web/Pages/Login.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@page "/login" -@model ShoppingAssistantWebClient.Web.Pages.LoginModel - -

    Login

    - -@{ - -} diff --git a/ShoppingAssistantWebClient.Web/Pages/Login.cshtml.cs b/ShoppingAssistantWebClient.Web/Pages/Login.cshtml.cs deleted file mode 100644 index f57fb3e..0000000 --- a/ShoppingAssistantWebClient.Web/Pages/Login.cshtml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace ShoppingAssistantWebClient.Web.Pages -{ - public class LoginModel : PageModel - { - public void OnGet() - { - } - } -} diff --git a/ShoppingAssistantWebClient.Web/Pages/Login.razor b/ShoppingAssistantWebClient.Web/Pages/Login.razor new file mode 100644 index 0000000..2e9bae7 --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/Login.razor @@ -0,0 +1,82 @@ +@page "/login" + +@using System.Text.RegularExpressions +@using Microsoft.AspNetCore.Components.Forms +@using ShoppingAssistantWebClient.Web.Models.Input +@using Models.GlobalInstances + + + + + + + + + +@code { + private string phoneValidationMessage = ""; + private string emailValidationMessage = ""; + private bool isPhoneInvalid = false; + private bool isEmailInvalid = false; + + private LoginInputModel LoginInput = new LoginInputModel(); + + private void ValidatePhone() + { + if (!string.IsNullOrWhiteSpace(LoginInput.Phone) && !Regex.IsMatch(LoginInput.Phone, @"^\+[0-9]{1,15}$")) + { + phoneValidationMessage = "Please enter a valid phone number"; + isPhoneInvalid = true; + } + else + { + phoneValidationMessage = ""; + isPhoneInvalid = false; + } + } + + private void ValidateEmail() + { + if (!string.IsNullOrWhiteSpace(LoginInput.Email) && !Regex.IsMatch(LoginInput.Email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$")) + { + emailValidationMessage = "Please enter a valid email address"; + isEmailInvalid = true; + } + else + { + emailValidationMessage = ""; + isEmailInvalid = false; + } + } + + private bool HasValidationErrors() + { + return !string.IsNullOrWhiteSpace(phoneValidationMessage) || !string.IsNullOrWhiteSpace(emailValidationMessage); + } + private async Task HandleLogin() + { + if (HasValidationErrors()) + { + return; + } + + await LoginUser(LoginInput); + } +} diff --git a/ShoppingAssistantWebClient.Web/Pages/Login.razor.cs b/ShoppingAssistantWebClient.Web/Pages/Login.razor.cs new file mode 100644 index 0000000..2a15b52 --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/Login.razor.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Components; +using ShoppingAssistantWebClient.Web.Network; +using ShoppingAssistantWebClient.Web.Models.Input; + + +namespace ShoppingAssistantWebClient.Web.Pages; + +public partial class Login : ComponentBase +{ + + [Inject] + NavigationManager NavigationManager { get; set; } + + [Inject] + private AuthenticationService _authenticationService { get; set; } + + private string errorMessage = ""; + + + private void RedirectToNewChat() { + var url = $"/"; + NavigationManager.NavigateTo(url); + } + + public async Task LoginUser(LoginInputModel login) { + if (login.IsEmailOrPhoneProvided) + { + try + { + await _authenticationService.LoginAsync(login); + RedirectToNewChat(); + } + catch (Exception ex) + { + errorMessage = "Login failed. Please try again."; + } + } + else + { + errorMessage = "Please provide an email or phone number."; + } + } +} diff --git a/ShoppingAssistantWebClient.Web/Pages/Settings.razor b/ShoppingAssistantWebClient.Web/Pages/Settings.razor new file mode 100644 index 0000000..ecfacaa --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/Settings.razor @@ -0,0 +1,158 @@ +@using System.Text.RegularExpressions +@using Models.GlobalInstances +@using ShoppingAssistantWebClient.Web.Models + +@inject IHttpContextAccessor httpContextAccessor; +@inject NavigationManager NavigationManager; + + + + + +@code { + [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } + + private string phoneValidationMessage = ""; + private string emailValidationMessage = ""; + private bool isPhoneInvalid = false; + private bool isEmailInvalid = false; + private bool isApplyDisabled = true; + private string phone = ""; + private string email = ""; + private string password = ""; + + private void ValidatePhone(ChangeEventArgs e) + { + errorMessage = ""; + phone = e.Value.ToString(); + if (!string.IsNullOrWhiteSpace(phone) && !Regex.IsMatch(phone, @"^\+[0-9]{1,15}$")) + { + phoneValidationMessage = "Please enter a valid phone number"; + isPhoneInvalid = true; + } + else + { + phoneValidationMessage = ""; + isPhoneInvalid = false; + } + + UpdateApplyButtonState(); + } + + private void ValidateEmail(ChangeEventArgs e) + { + errorMessage = ""; + email = e.Value.ToString(); + if (!string.IsNullOrWhiteSpace(email) && !Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$")) + { + emailValidationMessage = "Please enter a valid email address."; + isEmailInvalid = true; + } + else + { + emailValidationMessage = ""; + isEmailInvalid = false; + } + UpdateApplyButtonState(); + } + + private void UpdateApplyButtonState() + { + if(user.Roles.Any(role => role.Name == "User")) + isApplyDisabled = (string.IsNullOrWhiteSpace(phone) && string.IsNullOrWhiteSpace(email)) || isPhoneInvalid || isEmailInvalid; + else + isApplyDisabled = string.IsNullOrWhiteSpace(password) || (string.IsNullOrWhiteSpace(phone) && string.IsNullOrWhiteSpace(email)) || isPhoneInvalid || isEmailInvalid; + } + + private void OnPasswordInput(ChangeEventArgs e) + { + errorMessage = ""; + password = e.Value.ToString(); + UpdateApplyButtonState(); + } + + private async Task Close() => await BlazoredModal.CloseAsync(ModalResult.Ok(true)); + private async Task Cancel() => await BlazoredModal.CancelAsync(); + + private async Task Apply() { + + await UpdateUser(); + isApplyDisabled = true; + await GetUser(); + StateHasChanged(); + + if(user.Roles.Any(role => role.Name == "User")) { + await Task.Delay(3000); + await InvokeAsync(() => { + updateMessage = ""; + StateHasChanged(); + }); + } + } + + private void RedirectToLogin() { + var url = $"/login"; + NavigationManager.NavigateTo(url); + } + + private string ShowErrorDivClass() + { + return string.IsNullOrEmpty(errorMessage) ? "hidden" : "alert alert-danger"; + } + + private string ShowUpdateDivClass() { + return string.IsNullOrEmpty(updateMessage) ? "hidden" : "alert alert-success"; + } +} diff --git a/ShoppingAssistantWebClient.Web/Pages/Settings.razor.cs b/ShoppingAssistantWebClient.Web/Pages/Settings.razor.cs new file mode 100644 index 0000000..d3b475d --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/Settings.razor.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Components; +using ShoppingAssistantWebClient.Web.Network; +using ShoppingAssistantWebClient.Web.Models; +using ShoppingAssistantWebClient.Web.Models.GlobalInstances; +using GraphQL; +using Newtonsoft.Json; + + +namespace ShoppingAssistantWebClient.Web.Pages; + +public partial class Settings : ComponentBase +{ + [Inject] + private ApiClient _apiClient { get; set; } + + [Inject] + private IHttpContextAccessor _httpContextAccessor { get; set; } + + public User user = new User(); + + private string errorMessage = ""; + + private string updateMessage = ""; + + protected override async Task OnInitializedAsync() + { + await GetUser(); + } + + public async Task GetUser() { + try { + var request = new GraphQLRequest { + Query = @" + query User($id: String!) { + user(id: $id) { + roles { + id + name + } + phone + email + } + }", + Variables = new + { + id = GlobalUser.Id, + } + }; + + var response = await _apiClient.QueryAsync(request); + var responseData = response.Data; + //System.Console.WriteLine(responseData); + + var jsonCategoriesResponse = JsonConvert.SerializeObject(responseData.user); + this.user = JsonConvert.DeserializeObject(jsonCategoriesResponse); + user.GuestId = _httpContextAccessor.HttpContext.Request.Cookies["guestId"]; + + this.phone = user.Phone; + this.email = user.Email; + StateHasChanged(); + + } + catch(Exception ex) + { + Console.WriteLine($"Error in GetUser: {ex}"); + } + } + + public async Task UpdateUser() + { + try { + if(user.Roles.Any(role => role.Name == "User")) + { + updateMessage = "Your data has been successfully updated"; + } + if(phone == "") { + phone = user.Phone; + } + if(email == "") { + email = user.Email; + } + var request = new GraphQLRequest + { + Query = @" + mutation UpdateUser($userDto: UserDtoInput!) { + updateUser(userDto: $userDto) { + tokens { accessToken, refreshToken }, + user { email, phone } + } + }", + Variables = new + { + userDto = new + { + id = GlobalUser.Id, + guestId = user.GuestId, + roles = user.Roles.Select(r => new { id = r.Id, name = r.Name }), + email = email, + phone = phone, + password = password + } + } + }; + + var response = await _apiClient.QueryAsync(request); + var responseData = response.Data; + System.Console.WriteLine(responseData); + errorMessage = ""; + phone = ""; + email = ""; + } + catch(Exception ex) + { + if (ex.Message.Contains("The HTTP request failed with status code InternalServerError")) { + errorMessage = "This user is already registered."; + } else { + errorMessage = "Something went wrong, please try again."; + } + Console.WriteLine($"Error in UpdateUser: {ex}"); + } + } +} diff --git a/ShoppingAssistantWebClient.Web/Pages/Settings.razor.css b/ShoppingAssistantWebClient.Web/Pages/Settings.razor.css new file mode 100644 index 0000000..cc2ba3a --- /dev/null +++ b/ShoppingAssistantWebClient.Web/Pages/Settings.razor.css @@ -0,0 +1,260 @@ +.modal-dialog { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: auto; +} + +.close { + background: none; + border: none; + outline: none; +} + +.close span { + display: block; + color: #000; + font-size: 24px; +} + +.modal-content { + background-color: white; + color: #333; +} + +.form-group label { + color: #333; +} + +.form-control { + border: 1px solid #ccc; + border-radius: 4px; +} + +.form-group { + position: relative; +} + +.btn { + width: 120px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; +} + +#phone, #email { + margin-bottom: 1.5rem; +} + +.validation-message { + position: absolute; + bottom: -20px; + left: 0; + color: red; + font-size: 0.8rem; + visibility: hidden; +} + +.validation-message.active { + visibility: visible; +} + +.btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +.btn-secondary { + background-color: #6c757d; + border-color: #6c757d; +} + +.btn-primary:hover { + background-color: #0056b3; +} + +.btn-secondary:hover { + background-color: #545b62; +} + +.btn:active { + transform: scale(0.95); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.btn:focus { + outline: none !important; + box-shadow: none !important; +} + +.modal-header, .modal-footer, .modal-body { + padding: 16px !important; +} + +.log-in-button-left { + margin-right: auto; + visibility: visible; +} + +.log-in-button-left-hidden { + visibility: hidden; +} + +.modal-footer { + display: flex; + justify-content: flex-end; +} + +.hidden { + display: none; +} + +@media (max-width: 320px) { + .modal-dialog { + max-width: 90%; + margin: auto; + } + + .modal-header, .modal-footer, .modal-body { + padding: 6px !important; + } + + .form-control { + font-size: 0.85rem; + padding: 0.375rem 0.75rem; + } + + .btn { + font-size: 0.85rem; + width: 80px; + height: 28px; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; + } + + #phone, #email { + margin-bottom: 0.8rem; + } +} + +@media (min-width: 321px) and (max-width: 376px) { + .modal-dialog { + max-width: 300px; + margin: auto; + } + + .modal-header, .modal-footer, .modal-body { + padding: 8px !important; + } + + .form-control { + font-size: 0.85rem; + padding: 0.375rem 0.75rem; + } + + .btn { + font-size: 0.85rem; + width: 70px; + height: 28px; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; + } + + #phone, #email { + margin-bottom: 1rem; + } + + #phone-validation, #email-validation{ + margin-top: -1rem; + } + +} + +@media (min-width: 376px) and (max-width: 426px) { + .modal-dialog { + max-width: 300px; + margin: auto; + } + + .modal-header, .modal-footer, .modal-body { + padding: 10px !important; + } + + .form-control { + font-size: 0.9rem; + padding: 0.375rem 0.75rem; + } + + .btn { + font-size: 0.9rem; + width: 80px; + height: 28px; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; + } + + #phone, #email { + margin-bottom: 1rem; + } + + #phone-validation, #email-validation{ + margin-top: -1rem; + } +} + +@media (min-width: 426px) and (max-width: 768px) { + .modal-dialog { + width: 60%; + max-width: 400px; + min-width: 330px; + margin: auto; + } + + .modal-header, .modal-footer, .modal-body { + padding: 12px !important; + } + + .form-control { + font-size: 0.95rem; + padding: 0.375rem 0.75rem; + } + + .btn { + font-size: 0.95rem; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; + } + + #phone, #email { + margin-bottom: 1.3rem; + } + + #phone-validation, #email-validation{ + margin-top: -1.3rem; + } +} + +@media (min-width: 426px) and (max-width: 582px) { + .btn { + font-size: 0.95rem; + width: 80px !important; + height: 28px !important; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; + } +} diff --git a/ShoppingAssistantWebClient.Web/Pages/_Host.cshtml b/ShoppingAssistantWebClient.Web/Pages/_Host.cshtml index 5f51717..f9403ad 100644 --- a/ShoppingAssistantWebClient.Web/Pages/_Host.cshtml +++ b/ShoppingAssistantWebClient.Web/Pages/_Host.cshtml @@ -12,6 +12,8 @@ + + diff --git a/ShoppingAssistantWebClient.Web/Program.cs b/ShoppingAssistantWebClient.Web/Program.cs index 4809cc6..bae7570 100644 --- a/ShoppingAssistantWebClient.Web/Program.cs +++ b/ShoppingAssistantWebClient.Web/Program.cs @@ -1,8 +1,10 @@ +using Blazored.Modal; using GraphQL.Client.Http; using ShoppingAssistantWebClient.Web.Configurations; using ShoppingAssistantWebClient.Web.Data; using ShoppingAssistantWebClient.Web.Network; using ShoppingAssistantWebClient.Web.Services; +using Blazored.Modal; var builder = WebApplication.CreateBuilder(args); @@ -13,6 +15,8 @@ builder.Services.AddServerSideBlazor().AddCircuitOptions(options => { options.De builder.Services.AddSingleton(); builder.Services.AddApiClient(builder.Configuration); builder.Services.AddSingleton(); +builder.Services.AddBlazoredModal(); + var app = builder.Build(); diff --git a/ShoppingAssistantWebClient.Web/Shared/MainLayout.razor b/ShoppingAssistantWebClient.Web/Shared/MainLayout.razor index 2b9f5d4..e164adc 100644 --- a/ShoppingAssistantWebClient.Web/Shared/MainLayout.razor +++ b/ShoppingAssistantWebClient.Web/Shared/MainLayout.razor @@ -1,10 +1,14 @@ @inherits LayoutComponentBase @using ShoppingAssistantWebClient.Web.Pages +@using Blazored.Modal + CARTAID + +