Initial version

This commit is contained in:
Robert 2024-07-06 18:31:47 -07:00
commit aa0a74d4dc
18 changed files with 920 additions and 0 deletions

80
.gitignore vendored Normal file
View File

@ -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

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
namespace KeywordsEverywhereClient;
internal class ClientFormConfiguration
{
public string ApiKey { get; set; } = "";
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
namespace KeywordsEverywhereClient;
public record GetKeywordDataError
{
public string Message { get; init; } = "";
}

View File

@ -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; } = [];
}

View File

@ -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; }
}

View File

@ -0,0 +1,6 @@
using OneOf;
namespace KeywordsEverywhereClient;
[GenerateOneOf]
public partial class GetKeywordResponseOrError : OneOfBase<GetKeywordDataResponse, GetKeywordDataError> { }

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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());
}
}
}

View File

@ -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>

View File

@ -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>