Initial version
This commit is contained in:
commit
aa0a74d4dc
|
@ -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
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KeywordsEverywhereClient\KeywordsEverywhereClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -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<GetKeywordDataResponse>(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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,205 @@
|
|||
namespace KeywordsEverywhereClient
|
||||
{
|
||||
partial class ClientForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<ResultsDataRow>();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.SetEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GetKeywordResponseOrError> GetResponseOrErrorAsync(IReadOnlyList<string> 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="Keyword.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
<metadata name="SearchVolume.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<value>True</value>
|
||||
</metadata>
|
||||
</root>
|
|
@ -0,0 +1,6 @@
|
|||
namespace KeywordsEverywhereClient;
|
||||
|
||||
internal class ClientFormConfiguration
|
||||
{
|
||||
public string ApiKey { get; set; } = "";
|
||||
}
|
|
@ -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<ClientFormConfiguration>(json) ?? new();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Save(ClientFormConfiguration clientFormConfiguration)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(clientFormConfiguration);
|
||||
File.WriteAllText("config.json", json);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace KeywordsEverywhereClient;
|
||||
|
||||
public record GetKeywordDataError
|
||||
{
|
||||
public string Message { get; init; } = "";
|
||||
}
|
|
@ -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<string> Keywords { get; init; } = [];
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace KeywordsEverywhereClient;
|
||||
|
||||
public record GetKeywordDataResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public IReadOnlyList<GetKeywordDataResponseData> 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<GetKeywordDataResponseTrend> 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; }
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using OneOf;
|
||||
|
||||
namespace KeywordsEverywhereClient;
|
||||
|
||||
[GenerateOneOf]
|
||||
public partial class GetKeywordResponseOrError : OneOfBase<GetKeywordDataResponse, GetKeywordDataError> { }
|
|
@ -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<GetKeywordResponseOrError> 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<GetKeywordDataResponse>(responseBody)
|
||||
?? throw new InvalidOperationException("Success response is null.");
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<GetKeywordDataError>(responseBody)
|
||||
?? throw new InvalidOperationException("Error response is null.");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationHighDpiMode>SystemAware</ApplicationHighDpiMode>
|
||||
<ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.271" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="KeywordsEverywhereClient.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,17 @@
|
|||
namespace KeywordsEverywhereClient
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>2</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.*</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Release</Configuration>
|
||||
<CreateWebPageOnPublish>False</CreateWebPageOnPublish>
|
||||
<GenerateManifests>true</GenerateManifests>
|
||||
<Install>True</Install>
|
||||
<InstallFrom>Disk</InstallFrom>
|
||||
<IsRevisionIncremented>True</IsRevisionIncremented>
|
||||
<IsWebBootstrapper>False</IsWebBootstrapper>
|
||||
<MapFileExtensions>True</MapFileExtensions>
|
||||
<OpenBrowserOnPublish>False</OpenBrowserOnPublish>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net8.0-windows\app.publish.clickonce\</PublishDir>
|
||||
<PublishUrl>bin\Release\net8.0-windows\publish.clickonce\</PublishUrl>
|
||||
<PublishProtocol>ClickOnce</PublishProtocol>
|
||||
<PublishReadyToRun>False</PublishReadyToRun>
|
||||
<PublishSingleFile>False</PublishSingleFile>
|
||||
<SelfContained>False</SelfContained>
|
||||
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
||||
<SignManifests>False</SignManifests>
|
||||
<SkipPublishVerification>false</SkipPublishVerification>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UpdateEnabled>False</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateRequired>False</UpdateRequired>
|
||||
<WebPageFileName>Publish.html</WebPageFileName>
|
||||
<History>True|2024-07-07T01:21:31.1264088Z;</History>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include="Microsoft.NetCore.DesktopRuntime.8.0.x64">
|
||||
<Install>True</Install>
|
||||
<ProductName>.NET Desktop Runtime 8.0.2 (x64)</ProductName>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\Release\net8.0-windows\publish\folder\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
Loading…
Reference in New Issue