montana/macOS/MontanaPresence/DomainView.swift

364 lines
12 KiB
Swift
Raw Permalink Normal View History

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