diff --git a/EbayListingTool/Views/NewListingView.xaml b/EbayListingTool/Views/NewListingView.xaml
new file mode 100644
index 0000000..eb90c80
--- /dev/null
+++ b/EbayListingTool/Views/NewListingView.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EbayListingTool/Views/NewListingView.xaml.cs b/EbayListingTool/Views/NewListingView.xaml.cs
new file mode 100644
index 0000000..8e9f211
--- /dev/null
+++ b/EbayListingTool/Views/NewListingView.xaml.cs
@@ -0,0 +1,235 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+using EbayListingTool.Models;
+using EbayListingTool.Services;
+using Microsoft.Win32;
+
+namespace EbayListingTool.Views;
+
+public partial class NewListingView : UserControl
+{
+ // Services (injected via Initialise)
+ private EbayListingService? _listingService;
+ private EbayCategoryService? _categoryService;
+ private AiAssistantService? _aiService;
+ private EbayAuthService? _auth;
+ private SavedListingsService? _savedService;
+ private string _defaultPostcode = "";
+
+ // State A — photos
+ private readonly List _photoPaths = new();
+ private const int MaxPhotos = 12;
+
+ // State B — draft being edited (stub, populated in Task 4)
+ private ListingDraft _draft = new();
+ private PhotoAnalysisResult? _lastAnalysis;
+ private bool _suppressCategoryLookup;
+ private System.Threading.CancellationTokenSource? _categoryCts;
+ private string _suggestedPriceValue = "";
+
+ // Loading step cycling
+ private readonly DispatcherTimer _loadingTimer;
+ private int _loadingStep;
+ private static readonly string[] LoadingSteps =
+ [
+ "Examining the photo\u2026",
+ "Identifying the item\u2026",
+ "Researching eBay prices\u2026",
+ "Writing description\u2026"
+ ];
+
+ public NewListingView()
+ {
+ InitializeComponent();
+ _loadingTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2.5) };
+ _loadingTimer.Tick += (_, _) =>
+ {
+ _loadingStep = (_loadingStep + 1) % LoadingSteps.Length;
+ LoadingStepText.Text = LoadingSteps[_loadingStep];
+ };
+ }
+
+ private void UserControl_Loaded(object sender, RoutedEventArgs e) { }
+
+ public void Initialise(EbayListingService listingService, EbayCategoryService categoryService,
+ AiAssistantService aiService, EbayAuthService auth,
+ SavedListingsService savedService, string defaultPostcode)
+ {
+ _listingService = listingService;
+ _categoryService = categoryService;
+ _aiService = aiService;
+ _auth = auth;
+ _savedService = savedService;
+ _defaultPostcode = defaultPostcode;
+ }
+
+ // ---- State machine ----
+
+ private enum ListingState { DropZone, ReviewEdit, Success }
+
+ private void SetState(ListingState state)
+ {
+ StateA.Visibility = state == ListingState.DropZone ? Visibility.Visible : Visibility.Collapsed;
+ StateB.Visibility = state == ListingState.ReviewEdit ? Visibility.Visible : Visibility.Collapsed;
+ StateC.Visibility = state == ListingState.Success ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ // ---- State A: Drop zone ----
+
+ private void DropZone_DragOver(object sender, DragEventArgs e)
+ {
+ e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
+ ? DragDropEffects.Copy : DragDropEffects.None;
+ e.Handled = true;
+ }
+
+ private void DropZone_DragEnter(object sender, DragEventArgs e)
+ {
+ DropBorderRect.Stroke = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
+ e.Handled = true;
+ }
+
+ private void DropZone_DragLeave(object sender, DragEventArgs e)
+ {
+ DropBorderRect.Stroke = (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray6");
+ }
+
+ private void DropZone_Drop(object sender, DragEventArgs e)
+ {
+ DropZone_DragLeave(sender, e);
+ if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
+ var files = (string[])e.Data.GetData(DataFormats.FileDrop);
+ AddPhotos(files.Where(IsImageFile).ToArray());
+ }
+
+ private void DropZone_Click(object sender, MouseButtonEventArgs e)
+ {
+ var dlg = new OpenFileDialog
+ {
+ Title = "Select photos of the item",
+ Filter = "Images|*.jpg;*.jpeg;*.png;*.gif;*.webp;*.bmp|All files|*.*",
+ Multiselect = true
+ };
+ if (dlg.ShowDialog() == true)
+ AddPhotos(dlg.FileNames);
+ }
+
+ private void AddPhotos(string[] paths)
+ {
+ foreach (var path in paths)
+ {
+ if (_photoPaths.Count >= MaxPhotos) break;
+ if (_photoPaths.Contains(path)) continue;
+ if (!IsImageFile(path)) continue;
+ _photoPaths.Add(path);
+ }
+ UpdateThumbStrip();
+ UpdateAnalyseButton();
+ }
+
+ private void UpdateThumbStrip()
+ {
+ ThumbStrip.Children.Clear();
+ ThumbScroller.Visibility = _photoPaths.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+
+ foreach (var path in _photoPaths)
+ {
+ try
+ {
+ var bmp = new BitmapImage();
+ bmp.BeginInit();
+ bmp.UriSource = new Uri(path, UriKind.Absolute);
+ bmp.DecodePixelWidth = 80;
+ bmp.CacheOption = BitmapCacheOption.OnLoad;
+ bmp.EndInit();
+ bmp.Freeze();
+
+ var img = new Image
+ {
+ Source = bmp, Width = 60, Height = 60,
+ Stretch = System.Windows.Media.Stretch.UniformToFill,
+ Margin = new Thickness(3)
+ };
+ img.Clip = new System.Windows.Media.RectangleGeometry(
+ new Rect(0, 0, 60, 60), 4, 4);
+ ThumbStrip.Children.Add(img);
+ }
+ catch { /* skip bad files */ }
+ }
+
+ PhotoCountLabel.Text = $"{_photoPaths.Count} / {MaxPhotos} photos";
+ PhotoCountLabel.Visibility = _photoPaths.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void UpdateAnalyseButton()
+ {
+ AnalyseBtn.IsEnabled = _photoPaths.Count > 0;
+ }
+
+ private async void Analyse_Click(object sender, RoutedEventArgs e)
+ {
+ if (_aiService == null || _photoPaths.Count == 0) return;
+ SetAnalysing(true);
+ try
+ {
+ var result = await _aiService.AnalyseItemFromPhotosAsync(_photoPaths);
+ _lastAnalysis = result;
+ await PopulateStateBAsync(result);
+ SetState(ListingState.ReviewEdit);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Analysis failed:\n\n{ex.Message}", "AI Error",
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ finally
+ {
+ SetAnalysing(false);
+ }
+ }
+
+ private void SetAnalysing(bool busy)
+ {
+ AnalyseBtn.IsEnabled = !busy;
+ AnalyseSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
+ AnalyseIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
+ AnalyseBtnText.Text = busy ? "Analysing\u2026" : "Identify & Price with AI";
+ LoadingPanel.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
+ DropZoneBorder.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
+ if (busy)
+ {
+ _loadingStep = 0;
+ LoadingStepText.Text = LoadingSteps[0];
+ _loadingTimer.Start();
+ }
+ else
+ {
+ _loadingTimer.Stop();
+ }
+ }
+
+ // Stub for State B — implemented in Task 4
+ private Task PopulateStateBAsync(PhotoAnalysisResult result) => Task.CompletedTask;
+
+ // Stub for ResetToStateA — implemented in Task 4
+ public void ResetToStateA()
+ {
+ _photoPaths.Clear();
+ _draft = new ListingDraft { Postcode = _defaultPostcode };
+ _lastAnalysis = null;
+ UpdateThumbStrip();
+ UpdateAnalyseButton();
+ SetState(ListingState.DropZone);
+ }
+
+ private static bool IsImageFile(string path)
+ {
+ var ext = System.IO.Path.GetExtension(path).ToLowerInvariant();
+ return ext is ".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".bmp";
+ }
+
+ private MainWindow? GetWindow() => Window.GetWindow(this) as MainWindow;
+}