220 lines
No EOL
8.6 KiB
Swift
220 lines
No EOL
8.6 KiB
Swift
import SwiftUI
|
|
|
|
struct InviteCodesView: View {
|
|
@EnvironmentObject var viewModel: PDSViewModel
|
|
@State private var isCreatingCode = false
|
|
@State private var isRefreshing = false
|
|
@State private var showDisabledCodes = false
|
|
|
|
// Split codes into active and disabled
|
|
var activeCodes: [InviteCode] {
|
|
return viewModel.inviteCodes.filter { !$0.disabled }
|
|
}
|
|
|
|
var disabledCodes: [InviteCode] {
|
|
return viewModel.inviteCodes.filter { $0.disabled }
|
|
}
|
|
|
|
var filteredCodes: [InviteCode] {
|
|
if showDisabledCodes {
|
|
return viewModel.inviteCodes
|
|
} else {
|
|
return viewModel.inviteCodes.filter { !$0.disabled }
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
Group {
|
|
if viewModel.isLoading && viewModel.inviteCodes.isEmpty {
|
|
ProgressView("Loading invite codes...")
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else if filteredCodes.isEmpty {
|
|
VStack {
|
|
Text("No invite codes found")
|
|
.foregroundColor(.secondary)
|
|
Button("Refresh") {
|
|
Task {
|
|
await viewModel.refreshInviteCodes()
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else {
|
|
VStack(spacing: 0) {
|
|
List {
|
|
// Active codes section
|
|
Section(header: Text("Active Codes")) {
|
|
ForEach(activeCodes) { code in
|
|
InviteCodeRow(code: code)
|
|
.contextMenu {
|
|
Button(role: .destructive) {
|
|
Task {
|
|
// After disabling the code, refresh the list
|
|
await viewModel.disableInviteCode(code.id)
|
|
}
|
|
} label: {
|
|
Label("Disable Code", systemImage: "xmark.circle")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Disabled codes section (only shown if toggle is on)
|
|
if showDisabledCodes && !disabledCodes.isEmpty {
|
|
Section(header: Text("Disabled Codes")) {
|
|
ForEach(disabledCodes) { code in
|
|
InviteCodeRow(code: code)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Toggle("Show Disabled Codes (\(disabledCodes.count))", isOn: $showDisabledCodes)
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.onChange(of: showDisabledCodes) { _ in
|
|
// Refresh UI when toggle changes
|
|
viewModel.refreshUI()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Invite Codes")
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
if viewModel.demoMode {
|
|
Text("DEMO MODE")
|
|
.font(.caption)
|
|
.padding(5)
|
|
.background(Color.green.opacity(0.2))
|
|
.foregroundColor(.green)
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button {
|
|
isCreatingCode = true
|
|
Task {
|
|
await viewModel.createInviteCode()
|
|
isCreatingCode = false
|
|
}
|
|
} label: {
|
|
if isCreatingCode {
|
|
ProgressView()
|
|
} else {
|
|
Label("Create Code", systemImage: "plus")
|
|
}
|
|
}
|
|
.disabled(isCreatingCode)
|
|
}
|
|
}
|
|
.refreshable {
|
|
print("⏳ Pull-to-refresh: Fetching invite codes")
|
|
await viewModel.refreshInviteCodes()
|
|
print("✅ Pull-to-refresh completed")
|
|
}
|
|
}
|
|
.task {
|
|
print("⏳ Task: Fetching invite codes")
|
|
// Always fetch on initial load
|
|
await viewModel.refreshInviteCodes()
|
|
print("✅ Task fetch completed")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct InviteCodeRow: View {
|
|
let code: InviteCode
|
|
@State private var copySuccess = false
|
|
|
|
var formattedDate: String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
formatter.timeStyle = .short
|
|
return formatter.string(from: code.createdAt)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text(code.id)
|
|
.font(.system(.headline, design: .monospaced))
|
|
.foregroundColor(code.disabled ? .gray : .primary)
|
|
|
|
Spacer()
|
|
|
|
if code.disabled {
|
|
Text("DISABLED")
|
|
.font(.caption)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color.red.opacity(0.2))
|
|
.foregroundColor(.red)
|
|
.cornerRadius(4)
|
|
} else {
|
|
Button {
|
|
UIPasteboard.general.string = code.id
|
|
withAnimation {
|
|
copySuccess = true
|
|
}
|
|
// Reset the success message after a delay
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
withAnimation {
|
|
copySuccess = false
|
|
}
|
|
}
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: copySuccess ? "checkmark" : "doc.on.doc")
|
|
if copySuccess {
|
|
Text("Copied!")
|
|
}
|
|
}
|
|
.foregroundColor(copySuccess ? .green : .blue)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
(copySuccess ? Color.green : Color.blue)
|
|
.opacity(0.1)
|
|
.cornerRadius(4)
|
|
)
|
|
}
|
|
.buttonStyle(BorderlessButtonStyle())
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Text("Created: \(formattedDate)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
|
|
if let uses = code.uses {
|
|
if uses.isEmpty {
|
|
Text("Unused")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Color.green.opacity(0.1))
|
|
.cornerRadius(4)
|
|
} else {
|
|
Text("Uses: \(uses.count)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Color.blue.opacity(0.1))
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 6)
|
|
.opacity(code.disabled ? 0.7 : 1.0)
|
|
}
|
|
} |