montana/macOS/MontanaPresence/DomainView.swift

364 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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