diff --git a/EbayListingTool/Helpers/NumberWords.cs b/EbayListingTool/Helpers/NumberWords.cs
new file mode 100644
index 0000000..bf50e69
--- /dev/null
+++ b/EbayListingTool/Helpers/NumberWords.cs
@@ -0,0 +1,79 @@
+namespace EbayListingTool.Helpers;
+
+public static class NumberWords
+{
+ private static readonly string[] Ones =
+ [
+ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
+ "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
+ "seventeen", "eighteen", "nineteen"
+ ];
+
+ private static readonly string[] Tens =
+ [
+ "", "", "twenty", "thirty", "forty", "fifty",
+ "sixty", "seventy", "eighty", "ninety"
+ ];
+
+ ///
+ /// Converts a price to a friendly verbal string.
+ /// £17.49 → "about seventeen pounds"
+ /// £17.50 → "about seventeen pounds fifty"
+ /// £0.50 → "fifty pence"
+ ///
+ public static string ToVerbalPrice(decimal price)
+ {
+ if (price <= 0) return "no price set";
+
+ // Snap to nearest 50p
+ var rounded = Math.Round(price * 2) / 2m;
+ int pounds = (int)rounded;
+ bool hasFifty = (rounded - pounds) >= 0.5m;
+
+ if (pounds == 0)
+ return "fifty pence";
+
+ var poundsWord = IntToWords(pounds);
+ var poundsLabel = pounds == 1 ? "pound" : "pounds";
+ var suffix = hasFifty ? " fifty" : "";
+
+ return $"about {poundsWord} {poundsLabel}{suffix}";
+ }
+
+ ///
+ /// Converts a UTC DateTime to a human-friendly relative string.
+ ///
+ public static string ToRelativeDate(DateTime utcTime)
+ {
+ var diff = DateTime.UtcNow - utcTime;
+
+ if (diff.TotalSeconds < 60) return "just now";
+ if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} minutes ago";
+ if (diff.TotalHours < 2) return "about an hour ago";
+ if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} hours ago";
+ if (diff.TotalDays < 2) return "yesterday";
+ if (diff.TotalDays < 7) return $"{(int)diff.TotalDays} days ago";
+ if (diff.TotalDays < 14) return "last week";
+ if (diff.TotalDays < 30) return $"{(int)(diff.TotalDays / 7)} weeks ago";
+ if (diff.TotalDays < 60) return "last month";
+ return $"{(int)(diff.TotalDays / 30)} months ago";
+ }
+
+ private static string IntToWords(int n)
+ {
+ if (n < 20) return Ones[n];
+ if (n < 100)
+ {
+ var t = Tens[n / 10];
+ var o = n % 10;
+ return o == 0 ? t : $"{t}-{Ones[o]}";
+ }
+ if (n < 1000)
+ {
+ var h = Ones[n / 100];
+ var rest = n % 100;
+ return rest == 0 ? $"{h} hundred" : $"{h} hundred and {IntToWords(rest)}";
+ }
+ return n.ToString(); // fallback for very large prices
+ }
+}
diff --git a/EbayListingTool/Models/SavedListing.cs b/EbayListingTool/Models/SavedListing.cs
index a269c09..fff9b2b 100644
--- a/EbayListingTool/Models/SavedListing.cs
+++ b/EbayListingTool/Models/SavedListing.cs
@@ -1,3 +1,5 @@
+using EbayListingTool.Helpers;
+
namespace EbayListingTool.Models;
public class SavedListing
@@ -18,5 +20,9 @@ public class SavedListing
public string PriceDisplay => Price > 0 ? $"£{Price:F2}" : "—";
+ public string PriceWords => NumberWords.ToVerbalPrice(Price);
+
public string SavedAtDisplay => SavedAt.ToLocalTime().ToString("d MMM yyyy, HH:mm");
+
+ public string SavedAtRelative => NumberWords.ToRelativeDate(SavedAt);
}
diff --git a/EbayListingTool/Views/PhotoAnalysisView.xaml b/EbayListingTool/Views/PhotoAnalysisView.xaml
index 7da77e3..d45b1ea 100644
--- a/EbayListingTool/Views/PhotoAnalysisView.xaml
+++ b/EbayListingTool/Views/PhotoAnalysisView.xaml
@@ -325,6 +325,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EbayListingTool/Views/PhotoAnalysisView.xaml.cs b/EbayListingTool/Views/PhotoAnalysisView.xaml.cs
index 224c041..0e6b435 100644
--- a/EbayListingTool/Views/PhotoAnalysisView.xaml.cs
+++ b/EbayListingTool/Views/PhotoAnalysisView.xaml.cs
@@ -4,6 +4,7 @@ using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
+using EbayListingTool.Helpers;
using EbayListingTool.Models;
using EbayListingTool.Services;
using Microsoft.Win32;
@@ -285,9 +286,65 @@ public partial class PhotoAnalysisView : UserControl
private void ShowResults(PhotoAnalysisResult r)
{
- IdlePanel.Visibility = Visibility.Collapsed;
- LoadingPanel.Visibility = Visibility.Collapsed;
- ResultsPanel.Visibility = Visibility.Visible;
+ IdlePanel.Visibility = Visibility.Collapsed;
+ LoadingPanel.Visibility = Visibility.Collapsed;
+ ResultsPanel.Visibility = Visibility.Collapsed; // hidden behind card preview
+ CardPreviewPanel.Visibility = Visibility.Visible;
+ CardChangePanel.Visibility = Visibility.Collapsed;
+ ChangeChevron.Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronDown;
+
+ // --- Populate card preview ---
+ CardItemName.Text = r.ItemName;
+ CardCondition.Text = r.ConditionNotes;
+ CardCategory.Text = r.CategoryKeyword;
+ CardPriceVerbal.Text = NumberWords.ToVerbalPrice(r.PriceSuggested);
+ CardPriceDigit.Text = r.PriceSuggested > 0 ? $"£{r.PriceSuggested:F2}" : "";
+ CardLivePriceNote.Visibility = Visibility.Collapsed;
+
+ // Cover photo
+ if (_currentImagePaths.Count > 0)
+ {
+ try
+ {
+ var bmp = new BitmapImage();
+ bmp.BeginInit();
+ bmp.UriSource = new Uri(_currentImagePaths[0], UriKind.Absolute);
+ bmp.CacheOption = BitmapCacheOption.OnLoad;
+ bmp.DecodePixelWidth = 220;
+ bmp.EndInit();
+ bmp.Freeze();
+ CardPhoto.Source = bmp;
+ }
+ catch { CardPhoto.Source = null; }
+ }
+
+ // Photo dots — one dot per photo, filled accent for first
+ CardPhotoDots.Children.Clear();
+ for (int i = 0; i < _currentImagePaths.Count; i++)
+ {
+ CardPhotoDots.Children.Add(new System.Windows.Shapes.Ellipse
+ {
+ Width = 7, Height = 7,
+ Margin = new Thickness(2, 0, 2, 0),
+ Fill = i == 0
+ ? (Brush)FindResource("MahApps.Brushes.Accent")
+ : (Brush)FindResource("MahApps.Brushes.Gray7")
+ });
+ }
+ CardPhotoDots.Visibility = _currentImagePaths.Count > 1
+ ? Visibility.Visible : Visibility.Collapsed;
+
+ // Price slider — centre on suggested, range ±60% clamped to sensible bounds
+ var suggested = (double)(r.PriceSuggested > 0 ? r.PriceSuggested : 10m);
+ PriceSliderCard.Minimum = Math.Max(0.50, Math.Round(suggested * 0.4 * 2) / 2);
+ PriceSliderCard.Maximum = Math.Round(suggested * 1.8 * 2) / 2;
+ PriceSliderCard.Value = Math.Round(suggested * 2) / 2; // snap to 50p
+ SliderVerbalLabel.Text = NumberWords.ToVerbalPrice(r.PriceSuggested);
+ SliderDigitLabel.Text = $"£{r.PriceSuggested:F2}";
+
+ // Card title box
+ CardTitleBox.Text = r.Title;
+ UpdateCardTitleBar(r.Title.Length);
// Item identification
ItemNameText.Text = r.ItemName;
@@ -351,10 +408,6 @@ public partial class PhotoAnalysisView : UserControl
// Reset live price row until lookup completes
LivePriceRow.Visibility = Visibility.Collapsed;
-
- // Animate results in
- var sb = (Storyboard)FindResource("ResultsReveal");
- sb.Begin(this);
}
private async Task UpdateLivePricesAsync(string query)
@@ -392,9 +445,23 @@ public partial class PhotoAnalysisView : UserControl
// Update suggested price to 40th percentile (competitive but not cheapest)
var suggested = live.Suggested;
PriceSuggestedText.Text = $"£{suggested:F2}";
- PriceOverride.Value = (double)Math.Round(suggested, 2); // Issue 6: avoid decimal→double drift
+ PriceOverride.Value = (double)Math.Round(suggested, 2);
if (_lastResult != null) _lastResult.PriceSuggested = suggested;
+ // Update card preview price
+ CardPriceVerbal.Text = NumberWords.ToVerbalPrice(suggested);
+ CardPriceDigit.Text = $"£{suggested:F2}";
+ var snapped = Math.Round((double)suggested * 2) / 2;
+ if (snapped >= PriceSliderCard.Minimum && snapped <= PriceSliderCard.Maximum)
+ PriceSliderCard.Value = snapped;
+ SliderVerbalLabel.Text = NumberWords.ToVerbalPrice(suggested);
+ SliderDigitLabel.Text = $"£{suggested:F2}";
+
+ // Show note on card
+ var noteText = $"eBay: {live.Count} similar listing{(live.Count == 1 ? "" : "s")}, range £{live.Min:F2}–£{live.Max:F2}";
+ CardLivePriceNote.Text = noteText;
+ CardLivePriceNote.Visibility = Visibility.Visible;
+
// Update status label
LivePriceSpinner.Visibility = Visibility.Collapsed;
LivePriceStatus.Text =
@@ -666,6 +733,62 @@ public partial class PhotoAnalysisView : UserControl
Clipboard.SetText(DescriptionBox.Text);
}
+ // ---- Card preview handlers ----
+
+ private void LooksGood_Click(object sender, RoutedEventArgs e)
+ => SaveListing_Click(sender, e);
+
+ private void ChangeSomething_Click(object sender, RoutedEventArgs e)
+ {
+ var expanding = CardChangePanel.Visibility != Visibility.Visible;
+ CardChangePanel.Visibility = expanding ? Visibility.Visible : Visibility.Collapsed;
+ ChangeChevron.Kind = expanding
+ ? MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronUp
+ : MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronDown;
+ }
+
+ private void PriceSliderCard_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var price = (decimal)e.NewValue;
+ SliderVerbalLabel.Text = NumberWords.ToVerbalPrice(price);
+ SliderDigitLabel.Text = $"£{price:F2}";
+ // Keep card price display in sync
+ CardPriceVerbal.Text = NumberWords.ToVerbalPrice(price);
+ CardPriceDigit.Text = $"£{price:F2}";
+ // Keep hidden ResultsPanel in sync so SaveListing_Click gets the right value
+ PriceOverride.Value = e.NewValue;
+ if (_lastResult != null) _lastResult.PriceSuggested = price;
+ }
+
+ private void CardTitleBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ var len = CardTitleBox.Text.Length;
+ UpdateCardTitleBar(len);
+ // Keep hidden ResultsPanel in sync
+ TitleBox.Text = CardTitleBox.Text;
+ }
+
+ private void UpdateCardTitleBar(int len)
+ {
+ if (!IsLoaded) return;
+ var trackWidth = CardTitleBar.Parent is Grid g ? g.ActualWidth : 0;
+ if (trackWidth <= 0) return;
+ CardTitleBar.Width = trackWidth * (len / 80.0);
+ CardTitleBar.Background = len > 75
+ ? System.Windows.Media.Brushes.OrangeRed
+ : (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
+ }
+
+ private void SaveWithChanges_Click(object sender, RoutedEventArgs e)
+ {
+ if (_lastResult != null)
+ {
+ _lastResult.Title = CardTitleBox.Text.Trim();
+ TitleBox.Text = _lastResult.Title;
+ }
+ SaveListing_Click(sender, e);
+ }
+
// ---- Loading step cycling ----
private void LoadingTimer_Tick(object? sender, EventArgs e)
diff --git a/EbayListingTool/Views/SavedListingsView.xaml.cs b/EbayListingTool/Views/SavedListingsView.xaml.cs
index 0c4a1d9..a9240fa 100644
--- a/EbayListingTool/Views/SavedListingsView.xaml.cs
+++ b/EbayListingTool/Views/SavedListingsView.xaml.cs
@@ -189,23 +189,32 @@ public partial class SavedListingsView : UserControl
Margin = new Thickness(0, 0, 0, 3)
});
- var priceRow = new StackPanel { Orientation = Orientation.Horizontal };
- priceRow.Children.Add(new TextBlock
+ // Verbal price — primary; digit price small beneath
+ var priceVerbal = new TextBlock
{
- Text = listing.PriceDisplay,
- FontSize = 13,
+ Text = listing.PriceWords,
+ FontSize = 13,
FontWeight = FontWeights.Bold,
Foreground = (Brush)FindResource("MahApps.Brushes.Accent"),
- Margin = new Thickness(0, 0, 8, 0)
- });
- textStack.Children.Add(priceRow);
+ TextTrimming = TextTrimming.CharacterEllipsis
+ };
+ textStack.Children.Add(priceVerbal);
textStack.Children.Add(new TextBlock
{
- Text = listing.SavedAtDisplay,
- FontSize = 10,
+ Text = listing.PriceDisplay,
+ FontSize = 9,
+ Opacity = 0.40,
+ Margin = new Thickness(0, 1, 0, 0)
+ });
+
+ // Relative date
+ textStack.Children.Add(new TextBlock
+ {
+ Text = listing.SavedAtRelative,
+ FontSize = 10,
Foreground = (Brush)FindResource("MahApps.Brushes.Gray5"),
- Margin = new Thickness(0, 3, 0, 0)
+ Margin = new Thickness(0, 3, 0, 0)
});
grid.Children.Add(textStack);
@@ -264,7 +273,7 @@ public partial class SavedListingsView : UserControl
DetailTitle.Text = listing.Title;
DetailPrice.Text = listing.PriceDisplay;
DetailCategory.Text = listing.Category;
- DetailDate.Text = listing.SavedAtDisplay;
+ DetailDate.Text = listing.SavedAtRelative;
DetailDescription.Text = listing.Description;
if (!string.IsNullOrWhiteSpace(listing.ConditionNotes))