commit aa0a74d4dc71c714612c654f6c1e26ad43620bd8 Author: Robert Belcher Date: Sat Jul 6 18:31:47 2024 -0700 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81176cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +#basic visual studio directories +_UpgradeReport_Files/ +[Dd]ebug*/ +[Rr]elease*/ +!ReleaseNotesGenerator* +ipch/ +.vs/ +_ReSharper*/ +TestResults/ +*.DS_Store* +.cr/ + +obj/ +bin/ + +#ignore output mild compiler +GitExtensionsShellEx/Generated/ + +#ignore some unwanted files +*.ncb +*.suo +*.csproj.user +*.orig +*.msi +*.user +*.opendb +*.sdf +*.opensdf +*.ipch +*.iml +*.VC.db +*.sqlite +*.aps +*.bak +*.[Cc]ache +.idea/ +Thumbs.db +GitPlugin/bin/* +GitPlugin/Properties/Resources.resources +*.pidb +*.resources +*.userprefs +*.dotCover +*.ncrunchproject +*.ncrunchsolution +test-results/* +GitCommandsTests/test-results/* +/!runTests.bat +TestResult.xml +libgit2sharp +Setup/GitExtensions/ +Setup/GitExtensions-pdbs/ +Setup/GitExtensions-Portable-*.zip +Setup/GitExtensions-pdbs-*.zip +Setup/tools/tx.exe +Plugins/GitExtensions.PluginManager/* + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm* + +#nuget +packages/ +GitExtensions.*.sln.VisualState.xml +GitExtensions.settings.backup +/Setup/*.zip +/Setup/Changelog.md +*.received.* +Directory.Build.rsp +GitStatus.txt +OpenCover.GitExtensions.xml +tree.txt +*.binlog +artifacts/ +.tools/vswhere/ +.dotnet/ +*.svclog diff --git a/KeywordsEverywhereClient.Tests/KeywordsEverywhereClient.Tests.csproj b/KeywordsEverywhereClient.Tests/KeywordsEverywhereClient.Tests.csproj new file mode 100644 index 0000000..5b00cef --- /dev/null +++ b/KeywordsEverywhereClient.Tests/KeywordsEverywhereClient.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0-windows + enable + enable + false + true + + + + + + + + + + + + + + + + + + diff --git a/KeywordsEverywhereClient.Tests/SerializationTests.cs b/KeywordsEverywhereClient.Tests/SerializationTests.cs new file mode 100644 index 0000000..f9be9b4 --- /dev/null +++ b/KeywordsEverywhereClient.Tests/SerializationTests.cs @@ -0,0 +1,69 @@ +using System.Text.Json; + +namespace KeywordsEverywhereClient.Tests; + +[TestClass] +public class SerializationTests +{ + private string ValidResponseJson = """ + { + "data": [ + { + "vol": 135000, + "cpc": { + "currency": "$", + "value": "1.14" + }, + "keyword": "hello world", + "competition": 0.01, + "trend": [ + { + "month": "July", + "year": 2023, + "value": 135000 + }, + { + "month": "August", + "year": 2024, + "value": 135001 + } + ] + } + ], + "credits": 399174, + "time": 0.01 + } + """; + + [TestMethod] + public void CanDeserialize() + { + var result = JsonSerializer.Deserialize(ValidResponseJson) + ?? throw new InvalidOperationException("Success response is null."); + + Assert.AreEqual(1, result.Data.Count); + Assert.AreEqual(399174, result.Credits); + Assert.AreEqual(0.01, result.Time); + + var data = result.Data[0]; + + Assert.AreEqual(135000, data.Volume); + + Assert.AreEqual("$", data.CostPerClick.Currency); + Assert.AreEqual("1.14", data.CostPerClick.Value); + + Assert.AreEqual("hello world", data.Keyword); + + Assert.AreEqual(0.01, data.Competition); + + Assert.AreEqual(2, data.Trend.Count); + + Assert.AreEqual("July", data.Trend[0].Month); + Assert.AreEqual(2023, data.Trend[0].Year); + Assert.AreEqual(135000, data.Trend[0].Value); + + Assert.AreEqual("August", data.Trend[1].Month); + Assert.AreEqual(2024, data.Trend[1].Year); + Assert.AreEqual(135001, data.Trend[1].Value); + } +} diff --git a/KeywordsEverywhereClient.sln b/KeywordsEverywhereClient.sln new file mode 100644 index 0000000..6fc728a --- /dev/null +++ b/KeywordsEverywhereClient.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34622.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeywordsEverywhereClient", "KeywordsEverywhereClient\KeywordsEverywhereClient.csproj", "{3BF179A1-8A52-4E72-8AB3-2F98BB27D84C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeywordsEverywhereClient.Tests", "KeywordsEverywhereClient.Tests\KeywordsEverywhereClient.Tests.csproj", "{5272C08D-DA6E-48A0-A430-1AC4E8620736}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3BF179A1-8A52-4E72-8AB3-2F98BB27D84C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BF179A1-8A52-4E72-8AB3-2F98BB27D84C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BF179A1-8A52-4E72-8AB3-2F98BB27D84C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BF179A1-8A52-4E72-8AB3-2F98BB27D84C}.Release|Any CPU.Build.0 = Release|Any CPU + {5272C08D-DA6E-48A0-A430-1AC4E8620736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5272C08D-DA6E-48A0-A430-1AC4E8620736}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5272C08D-DA6E-48A0-A430-1AC4E8620736}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5272C08D-DA6E-48A0-A430-1AC4E8620736}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {499472DA-07C5-4011-A7C2-A2DDBB309D76} + EndGlobalSection +EndGlobal diff --git a/KeywordsEverywhereClient/ClientForm.Designer.cs b/KeywordsEverywhereClient/ClientForm.Designer.cs new file mode 100644 index 0000000..12de6a9 --- /dev/null +++ b/KeywordsEverywhereClient/ClientForm.Designer.cs @@ -0,0 +1,205 @@ +namespace KeywordsEverywhereClient +{ + partial class ClientForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new Label(); + this.apiKeyTextBox = new TextBox(); + this.keywordsTextBox = new TextBox(); + this.label2 = new Label(); + this.splitContainer1 = new SplitContainer(); + this.getResultsButton = new Button(); + this.creditsRemainingLabel = new Label(); + this.label3 = new Label(); + this.resultsDataGrid = new DataGridView(); + this.Keyword = new DataGridViewTextBoxColumn(); + this.SearchVolume = new DataGridViewTextBoxColumn(); + ((System.ComponentModel.ISupportInitialize)this.splitContainer1).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)this.resultsDataGrid).BeginInit(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new Point(12, 9); + this.label1.Name = "label1"; + this.label1.Size = new Size(50, 15); + this.label1.TabIndex = 0; + this.label1.Text = "API Key:"; + // + // apiKeyTextBox + // + this.apiKeyTextBox.Location = new Point(68, 6); + this.apiKeyTextBox.Name = "apiKeyTextBox"; + this.apiKeyTextBox.Size = new Size(727, 23); + this.apiKeyTextBox.TabIndex = 1; + this.apiKeyTextBox.Leave += this.apiKeyTextBox_Leave; + // + // keywordsTextBox + // + this.keywordsTextBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + this.keywordsTextBox.Location = new Point(3, 18); + this.keywordsTextBox.Multiline = true; + this.keywordsTextBox.Name = "keywordsTextBox"; + this.keywordsTextBox.Size = new Size(350, 197); + this.keywordsTextBox.TabIndex = 4; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new Point(3, 0); + this.label2.Name = "label2"; + this.label2.Size = new Size(134, 15); + this.label2.TabIndex = 5; + this.label2.Text = "Keywords (one per line):"; + // + // splitContainer1 + // + this.splitContainer1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + this.splitContainer1.Location = new Point(12, 35); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.getResultsButton); + this.splitContainer1.Panel1.Controls.Add(this.label2); + this.splitContainer1.Panel1.Controls.Add(this.keywordsTextBox); + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.creditsRemainingLabel); + this.splitContainer1.Panel2.Controls.Add(this.label3); + this.splitContainer1.Panel2.Controls.Add(this.resultsDataGrid); + this.splitContainer1.Size = new Size(737, 247); + this.splitContainer1.SplitterDistance = 356; + this.splitContainer1.TabIndex = 6; + // + // getResultsButton + // + this.getResultsButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + this.getResultsButton.Location = new Point(3, 221); + this.getResultsButton.Name = "getResultsButton"; + this.getResultsButton.Size = new Size(350, 23); + this.getResultsButton.TabIndex = 6; + this.getResultsButton.Text = "&Get results"; + this.getResultsButton.UseVisualStyleBackColor = true; + this.getResultsButton.Click += this.getResultsButton_Click; + // + // creditsRemainingLabel + // + this.creditsRemainingLabel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + this.creditsRemainingLabel.Location = new Point(174, 225); + this.creditsRemainingLabel.Name = "creditsRemainingLabel"; + this.creditsRemainingLabel.Size = new Size(200, 19); + this.creditsRemainingLabel.TabIndex = 8; + this.creditsRemainingLabel.Text = "Credits remaining: TBD"; + this.creditsRemainingLabel.TextAlign = ContentAlignment.TopRight; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new Point(3, 0); + this.label3.Name = "label3"; + this.label3.Size = new Size(47, 15); + this.label3.TabIndex = 6; + this.label3.Text = "Results:"; + // + // resultsDataGrid + // + this.resultsDataGrid.AllowUserToAddRows = false; + this.resultsDataGrid.AllowUserToDeleteRows = false; + this.resultsDataGrid.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + this.resultsDataGrid.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.resultsDataGrid.Columns.AddRange(new DataGridViewColumn[] { this.Keyword, this.SearchVolume }); + this.resultsDataGrid.Location = new Point(3, 18); + this.resultsDataGrid.Name = "resultsDataGrid"; + this.resultsDataGrid.ReadOnly = true; + this.resultsDataGrid.RowHeadersVisible = false; + this.resultsDataGrid.Size = new Size(371, 197); + this.resultsDataGrid.TabIndex = 0; + this.resultsDataGrid.CellFormatting += this.resultsDataGrid_CellFormatting; + // + // Keyword + // + this.Keyword.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + this.Keyword.DataPropertyName = "Keyword"; + this.Keyword.HeaderText = "Keyword"; + this.Keyword.Name = "Keyword"; + this.Keyword.ReadOnly = true; + this.Keyword.SortMode = DataGridViewColumnSortMode.NotSortable; + // + // SearchVolume + // + this.SearchVolume.AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader; + this.SearchVolume.DataPropertyName = "SearchVolume"; + this.SearchVolume.HeaderText = "Search Volume"; + this.SearchVolume.Name = "SearchVolume"; + this.SearchVolume.ReadOnly = true; + this.SearchVolume.SortMode = DataGridViewColumnSortMode.NotSortable; + this.SearchVolume.Width = 91; + // + // ClientForm + // + this.AutoScaleDimensions = new SizeF(7F, 15F); + this.AutoScaleMode = AutoScaleMode.Font; + this.ClientSize = new Size(761, 294); + this.Controls.Add(this.splitContainer1); + this.Controls.Add(this.apiKeyTextBox); + this.Controls.Add(this.label1); + this.MinimumSize = new Size(694, 182); + this.Name = "ClientForm"; + this.Text = "Mewsely Keywords Everywhere"; + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel1.PerformLayout(); + this.splitContainer1.Panel2.ResumeLayout(false); + this.splitContainer1.Panel2.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)this.splitContainer1).EndInit(); + this.splitContainer1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)this.resultsDataGrid).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private Label label1; + private TextBox apiKeyTextBox; + private TextBox keywordsTextBox; + private Label label2; + private SplitContainer splitContainer1; + private Label label3; + private DataGridView resultsDataGrid; + private Button getResultsButton; + private Label creditsRemainingLabel; + private DataGridViewTextBoxColumn Keyword; + private DataGridViewTextBoxColumn SearchVolume; + } +} diff --git a/KeywordsEverywhereClient/ClientForm.cs b/KeywordsEverywhereClient/ClientForm.cs new file mode 100644 index 0000000..0a7a94f --- /dev/null +++ b/KeywordsEverywhereClient/ClientForm.cs @@ -0,0 +1,127 @@ +namespace KeywordsEverywhereClient; + +public partial class ClientForm : Form +{ + private readonly ClientFormConfigurationManager clientFormConfigurationManager = new(); + private readonly KeywordsEverywhereApi keywordsEverywhereApi = new(); + + private ClientFormConfiguration clientFormConfiguration = new(); + + public ClientForm() + { + InitializeComponent(); + + resultsDataGrid.AutoGenerateColumns = false; + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + this.clientFormConfiguration = this.clientFormConfigurationManager.Load(); + this.apiKeyTextBox.Text = this.clientFormConfiguration.ApiKey; + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + + if (this.apiKeyTextBox.Text != "") + this.keywordsTextBox.Focus(); + } + + private void apiKeyTextBox_Leave(object sender, EventArgs e) + { + this.clientFormConfiguration.ApiKey = this.apiKeyTextBox.Text; + this.clientFormConfigurationManager.Save(this.clientFormConfiguration); + } + + private async void getResultsButton_Click(object sender, EventArgs e) + { + this.SetEnabled(false); + + try + { + var keywords = this.keywordsTextBox.Text + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (keywords.Length > 0) + { + var responseOrError = await this.GetResponseOrErrorAsync(keywords); + responseOrError.Switch(this.HandleResponseAsync, this.HandleError); + } + else + { + this.resultsDataGrid.DataSource = Array.Empty(); + } + } + finally + { + this.SetEnabled(true); + } + } + + private async Task GetResponseOrErrorAsync(IReadOnlyList keywords) + { + var requestData = new GetKeywordDataRequest + { + Country = "", //global + Currency = "USD", + DataSource = "cli", + Keywords = keywords + }; + + var responseOrError = await this.keywordsEverywhereApi.GetKeywordDataAsync( + this.clientFormConfiguration.ApiKey, + requestData, + default + ); + + return responseOrError; + } + + private void HandleResponseAsync(GetKeywordDataResponse responseData) + { + var rows = responseData.Data + .OrderByDescending(o => o.Volume) + .Select(o => new ResultsDataRow(o.Keyword, o.Volume)) + .ToList(); + + this.resultsDataGrid.DataSource = rows; + + creditsRemainingLabel.Text = $"Credits remaining: {responseData.Credits}"; + } + + private void HandleError(GetKeywordDataError errorData) + { + MessageBox.Show($"Failed to get data: {errorData.Message}"); + } + + private void resultsDataGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) + { + foreach (DataGridViewRow row in resultsDataGrid.Rows) + { + if (e.RowIndex < 0 || e.ColumnIndex < 0) + continue; + + var dataRow = (ResultsDataRow)row.DataBoundItem; + + if (dataRow.SearchVolumeValue is >= 100 and < 10000) + { + row.DefaultCellStyle.BackColor = Color.FromArgb(224, 255, 224); + } + } + } + + internal record ResultsDataRow(string Keyword, int SearchVolumeValue) + { + public string SearchVolume => $"{SearchVolumeValue}"; + } + + private void SetEnabled(bool enabled) + { + this.apiKeyTextBox.Enabled = enabled; + this.keywordsTextBox.Enabled = enabled; + this.getResultsButton.Enabled = enabled; + } +} diff --git a/KeywordsEverywhereClient/ClientForm.resx b/KeywordsEverywhereClient/ClientForm.resx new file mode 100644 index 0000000..b644493 --- /dev/null +++ b/KeywordsEverywhereClient/ClientForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + True + + \ No newline at end of file diff --git a/KeywordsEverywhereClient/ClientFormConfiguration.cs b/KeywordsEverywhereClient/ClientFormConfiguration.cs new file mode 100644 index 0000000..2ce14da --- /dev/null +++ b/KeywordsEverywhereClient/ClientFormConfiguration.cs @@ -0,0 +1,6 @@ +namespace KeywordsEverywhereClient; + +internal class ClientFormConfiguration +{ + public string ApiKey { get; set; } = ""; +} diff --git a/KeywordsEverywhereClient/ClientFormConfigurationManager.cs b/KeywordsEverywhereClient/ClientFormConfigurationManager.cs new file mode 100644 index 0000000..8937b7c --- /dev/null +++ b/KeywordsEverywhereClient/ClientFormConfigurationManager.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using System; + +namespace KeywordsEverywhereClient; + +internal class ClientFormConfigurationManager +{ + public ClientFormConfiguration Load() + { + if (!File.Exists("config.json")) + return new(); + + var json = File.ReadAllText("config.json"); + var result = JsonSerializer.Deserialize(json) ?? new(); + + return result; + } + + public void Save(ClientFormConfiguration clientFormConfiguration) + { + var json = JsonSerializer.Serialize(clientFormConfiguration); + File.WriteAllText("config.json", json); + } +} diff --git a/KeywordsEverywhereClient/GetKeywordDataError.cs b/KeywordsEverywhereClient/GetKeywordDataError.cs new file mode 100644 index 0000000..ab78928 --- /dev/null +++ b/KeywordsEverywhereClient/GetKeywordDataError.cs @@ -0,0 +1,6 @@ +namespace KeywordsEverywhereClient; + +public record GetKeywordDataError +{ + public string Message { get; init; } = ""; +} diff --git a/KeywordsEverywhereClient/GetKeywordDataRequest.cs b/KeywordsEverywhereClient/GetKeywordDataRequest.cs new file mode 100644 index 0000000..798f6b7 --- /dev/null +++ b/KeywordsEverywhereClient/GetKeywordDataRequest.cs @@ -0,0 +1,9 @@ +namespace KeywordsEverywhereClient; + +public record GetKeywordDataRequest +{ + public string Country { get; init; } = ""; + public string Currency { get; init; } = ""; + public string DataSource { get; init; } = ""; + public IReadOnlyList Keywords { get; init; } = []; +} diff --git a/KeywordsEverywhereClient/GetKeywordDataResponse.cs b/KeywordsEverywhereClient/GetKeywordDataResponse.cs new file mode 100644 index 0000000..69870b8 --- /dev/null +++ b/KeywordsEverywhereClient/GetKeywordDataResponse.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace KeywordsEverywhereClient; + +public record GetKeywordDataResponse +{ + [JsonPropertyName("data")] + public IReadOnlyList Data { get; init; } = []; + + [JsonPropertyName("credits")] + public long Credits { get; init; } + + [JsonPropertyName("time")] + public double Time { get; init; } +} + +public record GetKeywordDataResponseData +{ + [JsonPropertyName("vol")] + public int Volume { get; init; } + + [JsonPropertyName("cpc")] + public GetKeywordDataResponseCostPerClick CostPerClick { get; init; } = new(); + + [JsonPropertyName("keyword")] + public string Keyword { get; init; } = ""; + + [JsonPropertyName("competition")] + public double Competition { get; init; } + + [JsonPropertyName("trend")] + public IReadOnlyList Trend { get; init; } = []; +} + +public record GetKeywordDataResponseCostPerClick +{ + [JsonPropertyName("currency")] + public string Currency { get; init; } = ""; + + [JsonPropertyName("value")] + public string Value { get; init; } = ""; +} + +public record GetKeywordDataResponseTrend +{ + [JsonPropertyName("month")] + public string Month { get; init; } = ""; + + [JsonPropertyName("year")] + public int Year { get; init; } + + [JsonPropertyName("value")] + public int Value { get; init; } +} diff --git a/KeywordsEverywhereClient/GetKeywordResponseOrError.cs b/KeywordsEverywhereClient/GetKeywordResponseOrError.cs new file mode 100644 index 0000000..f81462e --- /dev/null +++ b/KeywordsEverywhereClient/GetKeywordResponseOrError.cs @@ -0,0 +1,6 @@ +using OneOf; + +namespace KeywordsEverywhereClient; + +[GenerateOneOf] +public partial class GetKeywordResponseOrError : OneOfBase { } diff --git a/KeywordsEverywhereClient/KeywordsEverywhereApi.cs b/KeywordsEverywhereClient/KeywordsEverywhereApi.cs new file mode 100644 index 0000000..454433a --- /dev/null +++ b/KeywordsEverywhereClient/KeywordsEverywhereApi.cs @@ -0,0 +1,57 @@ +using System.Text.Json; + +namespace KeywordsEverywhereClient; + +internal class KeywordsEverywhereApi +{ + private readonly HttpClient httpClient; + + public KeywordsEverywhereApi() + { + this.httpClient = new(new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15) + }); + } + + public async Task GetKeywordDataAsync(string apiKey, GetKeywordDataRequest requestData, CancellationToken cancellationToken) + { + HttpRequestMessage request = new(HttpMethod.Post, "https://api.keywordseverywhere.com/v1/get_keyword_data"); + + request.Headers.Add("Accept", "application/json"); + request.Headers.Add("Authorization", $"Bearer {apiKey}"); + + var content = new MultipartFormDataContent + { + { new StringContent(requestData.Country), "country" }, + { new StringContent(requestData.Currency), "currency" }, + { new StringContent(requestData.DataSource), "dataSource" } + }; + + foreach (var keyword in requestData.Keywords) + { + content.Add(new StringContent(keyword), "kw[]"); + } + + request.Content = content; + + using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); + + if (response.IsSuccessStatusCode) + { + var result = JsonSerializer.Deserialize(responseBody) + ?? throw new InvalidOperationException("Success response is null."); + + return result; + } + else + { + var result = JsonSerializer.Deserialize(responseBody) + ?? throw new InvalidOperationException("Error response is null."); + + return result; + } + } +} diff --git a/KeywordsEverywhereClient/KeywordsEverywhereClient.csproj b/KeywordsEverywhereClient/KeywordsEverywhereClient.csproj new file mode 100644 index 0000000..b221ea6 --- /dev/null +++ b/KeywordsEverywhereClient/KeywordsEverywhereClient.csproj @@ -0,0 +1,22 @@ + + + + WinExe + net8.0-windows + enable + true + enable + SystemAware + true + + + + + + + + + + + + \ No newline at end of file diff --git a/KeywordsEverywhereClient/Program.cs b/KeywordsEverywhereClient/Program.cs new file mode 100644 index 0000000..bd169d1 --- /dev/null +++ b/KeywordsEverywhereClient/Program.cs @@ -0,0 +1,17 @@ +namespace KeywordsEverywhereClient +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new ClientForm()); + } + } +} \ No newline at end of file diff --git a/KeywordsEverywhereClient/Properties/PublishProfiles/ClickOnceProfile.pubxml b/KeywordsEverywhereClient/Properties/PublishProfiles/ClickOnceProfile.pubxml new file mode 100644 index 0000000..eb076ab --- /dev/null +++ b/KeywordsEverywhereClient/Properties/PublishProfiles/ClickOnceProfile.pubxml @@ -0,0 +1,42 @@ + + + + + 2 + 1.0.0.* + True + Release + False + true + True + Disk + True + False + True + False + Any CPU + bin\Release\net8.0-windows\app.publish.clickonce\ + bin\Release\net8.0-windows\publish.clickonce\ + ClickOnce + False + False + False + (none) + False + false + net8.0-windows + False + Foreground + False + Publish.html + True|2024-07-07T01:21:31.1264088Z; + + + + True + .NET Desktop Runtime 8.0.2 (x64) + + + \ No newline at end of file diff --git a/KeywordsEverywhereClient/Properties/PublishProfiles/FolderProfile.pubxml b/KeywordsEverywhereClient/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..8eed8d0 --- /dev/null +++ b/KeywordsEverywhereClient/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + Release + Any CPU + bin\Release\net8.0-windows\publish\folder\ + FileSystem + <_TargetId>Folder + + \ No newline at end of file