import SwiftUI /// Montana Name Service — Domain Registration /// Регистрация доменов через аукцион (N-й домен = N Ɉ) struct DomainView: View { @EnvironmentObject var engine: PresenceEngine @State private var domainInput = "" @State private var currentPrice = 1 @State private var totalSold = 0 @State private var isLoading = false @State private var isCheckingAvailability = false @State private var domainAvailable: Bool? = nil @State private var statusMessage = "" @State private var ownedDomains: [OwnedDomain] = [] @State private var showSidebar = false private let gold = Color(red: 0.85, green: 0.68, blue: 0.25) var body: some View { ZStack(alignment: .leading) { VStack(spacing: 0) { // ── BURGER MENU BUTTON ── HStack { Button(action: { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = true } }) { Image(systemName: "line.3.horizontal") .font(.system(size: 18, weight: .medium)) .foregroundColor(gold) .padding(8) } .buttonStyle(.plain) Spacer() } .padding(.horizontal, 12) .padding(.top, 8) // Header header Divider() // Main content ScrollView { VStack(spacing: 24) { // Registration card registrationCard // Owned domains if !ownedDomains.isEmpty { ownedDomainsSection } // Info section infoSection } .padding() } } .background(Color(NSColor.windowBackgroundColor)) .onAppear { loadCurrentPrice() loadOwnedDomains() } // Sidebar overlay if showSidebar { Color.black.opacity(0.3) .ignoresSafeArea() .onTapGesture { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = false } } SharedSidebar(isVisible: $showSidebar) .transition(.move(edge: .leading)) } } } // MARK: - Header private var header: some View { HStack(spacing: 12) { // Domain icon Circle() .fill( LinearGradient( colors: [ Color(red: 0.0, green: 0.83, blue: 1.0), // #00d4ff cyan Color(red: 0.48, green: 0.18, blue: 1.0) // #7b2fff purple ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 40, height: 40) .overlay( Text("@") .font(.system(size: 24, weight: .bold)) .foregroundColor(.white) ) VStack(alignment: .leading, spacing: 2) { Text("Домены Montana") .font(.headline) Text("Регистрация доменов @efir.org") .font(.caption) .foregroundColor(.secondary) } Spacer() // Current price badge HStack(spacing: 4) { Text("\(currentPrice)") .font(.title2) .fontWeight(.bold) Text("Ɉ") .font(.title2) } .foregroundColor(Color(red: 0.0, green: 0.83, blue: 1.0)) .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(red: 0.0, green: 0.83, blue: 1.0).opacity(0.1)) .cornerRadius(8) } .padding() } // MARK: - Registration Card private var registrationCard: some View { VStack(spacing: 16) { Text("Регистрация домена") .font(.title3) .fontWeight(.semibold) HStack(spacing: 8) { TextField("имя", text: $domainInput) .textFieldStyle(.plain) .font(.system(size: 16)) .padding(10) .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(8) .disabled(isLoading) .onChange(of: domainInput) { _ in domainAvailable = nil statusMessage = "" } .onSubmit { checkAvailability() } Text("@efir.org") .foregroundColor(.secondary) } if isCheckingAvailability { HStack(spacing: 4) { ProgressView().controlSize(.small) Text("Проверяю...") .font(.caption) .foregroundColor(.secondary) } } else if let available = domainAvailable { Text(available ? "✓ Домен свободен" : "✗ Домен занят") .font(.caption) .foregroundColor(available ? .green : .red) } if !statusMessage.isEmpty { Text(statusMessage) .font(.caption) .foregroundColor(statusMessage.hasPrefix("✓") ? .green : .red) } Button(action: registerDomain) { HStack { if isLoading { ProgressView() .scaleEffect(0.8) .padding(.trailing, 4) } Text("Зарегистрировать за \(currentPrice) Ɉ") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding(.vertical, 10) .background(domainInput.isEmpty || isLoading ? Color.gray : Color(red: 0.0, green: 0.83, blue: 1.0)) .foregroundColor(.white) .cornerRadius(8) } .buttonStyle(.plain) .disabled(domainInput.isEmpty || isLoading) } .padding() .background(Color.blue.opacity(0.05)) .cornerRadius(12) } // MARK: - Owned Domains Section private var ownedDomainsSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Мои домены") .font(.title3) .fontWeight(.semibold) ForEach(ownedDomains) { domain in HStack { Text(domain.name + "@efir.org") .font(.system(.body, design: .monospaced)) Spacer() Text("\(domain.pricePaid) Ɉ") .foregroundColor(.secondary) .font(.caption) } .padding() .background(Color(NSColor.controlBackgroundColor)) .cornerRadius(8) } } } // MARK: - Info Section private var infoSection: some View { VStack(alignment: .leading, spacing: 12) { Text("💡 Аукционная модель") .font(.title3) .fontWeight(.semibold) VStack(alignment: .leading, spacing: 8) { InfoRow(icon: "1️⃣", text: "1-й домен: 1 Ɉ") InfoRow(icon: "2️⃣", text: "2-й домен: 2 Ɉ") InfoRow(icon: "🔢", text: "N-й домен: N Ɉ") InfoRow(icon: "📧", text: "Формат: alice@efir.org") InfoRow(icon: "🔐", text: "Постквантовая криптография ML-DSA-65") } } .padding() .background(Color.orange.opacity(0.05)) .cornerRadius(12) } // MARK: - Actions private func loadCurrentPrice() { Task { @MainActor in do { let (price, sold) = try await engine.api.fetchAuctionPrice(serviceType: "domain") currentPrice = price totalSold = sold } catch { currentPrice = 1 } } } private func loadOwnedDomains() { guard let addr = engine.address else { return } Task { @MainActor in do { let events = try await engine.api.fetchMyEvents(address: addr) ownedDomains = events.compactMap { event in guard let type = event["event_type"] as? String, type == "domain_register", let metadata = event["metadata"] as? [String: Any], let domain = metadata["domain"] as? String, let amount = event["amount"] as? Int else { return nil } let name = domain.replacingOccurrences(of: "@efir.org", with: "") return OwnedDomain(name: name, pricePaid: amount) } } catch { ownedDomains = [] } } } private func checkAvailability() { let sanitized = domainInput.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard !sanitized.isEmpty else { return } isCheckingAvailability = true Task { @MainActor in do { let (available, price) = try await engine.api.checkDomainAvailability(domain: sanitized) domainAvailable = available if available { currentPrice = price } } catch { domainAvailable = nil } isCheckingAvailability = false } } private func registerDomain() { guard !domainInput.isEmpty else { return } guard let ownerAddress = engine.address else { statusMessage = "❌ Нет адреса кошелька" return } let sanitized = domainInput.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard sanitized.range(of: "^[a-z0-9_-]+$", options: .regularExpression) != nil else { statusMessage = "❌ Только латиница, цифры, _ и -" return } isLoading = true statusMessage = "" Task { @MainActor in do { let result = try await engine.api.registerDomain( domain: sanitized, ownerAddress: ownerAddress, amount: currentPrice ) let pricePaid = result["price_paid"] as? Int ?? currentPrice statusMessage = "✓ Домен \(sanitized)@efir.org зарегистрирован!" ownedDomains.append(OwnedDomain(name: sanitized, pricePaid: pricePaid)) domainInput = "" domainAvailable = nil await engine.syncBalance() loadCurrentPrice() } catch { statusMessage = "❌ \(error.localizedDescription)" } isLoading = false } } } // MARK: - Models struct OwnedDomain: Identifiable { let id = UUID() let name: String let pricePaid: Int } private struct InfoRow: View { let icon: String let text: String var body: some View { HStack(spacing: 8) { Text(icon) .font(.body) Text(text) .font(.callout) .foregroundColor(.secondary) } } } // MARK: - Preview #Preview { DomainView() .environmentObject(PresenceEngine.shared) .frame(width: 600, height: 500) }