feat: sugar/fiber macros, editable goals, keyboard dismiss, entry editing
1. Sugar + Fiber bars in TodayView macro summary card 2. Goals page: editable text fields + Save button (PUT /api/fitness/goals) 3. AI Chat: keyboard dismisses on send + tap chat area to dismiss 4. Entry detail: edit quantity (stepper) + meal type picker + Save Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,9 @@ struct AssistantChatView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
}
|
||||||
.onChange(of: vm.messages.count) {
|
.onChange(of: vm.messages.count) {
|
||||||
if let last = vm.messages.last {
|
if let last = vm.messages.last {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@@ -90,10 +93,12 @@ struct AssistantChatView: View {
|
|||||||
TextField("Describe your food...", text: $vm.inputText)
|
TextField("Describe your food...", text: $vm.inputText)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
Task { await vm.send() }
|
Task { await vm.send() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
Task { await vm.send() }
|
Task { await vm.send() }
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "arrow.up.circle.fill")
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ struct FitnessAPI {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateGoals(_ request: UpdateGoalsRequest) async throws -> DailyGoal {
|
||||||
|
try await api.put("\(basePath)/goals", body: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEntry(id: String, body: UpdateEntryRequest) async throws -> FoodEntry {
|
||||||
|
try await api.patch("\(basePath)/entries/\(id)", body: body)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Foods
|
// MARK: - Foods
|
||||||
|
|
||||||
func getFoods(limit: Int = 100) async throws -> [Food] {
|
func getFoods(limit: Int = 100) async throws -> [Food] {
|
||||||
|
|||||||
@@ -212,6 +212,29 @@ struct CreateEntryRequest: Encodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Update Goals Request
|
||||||
|
|
||||||
|
struct UpdateGoalsRequest: Encodable {
|
||||||
|
let calories: Double
|
||||||
|
let protein: Double
|
||||||
|
let carbs: Double
|
||||||
|
let fat: Double
|
||||||
|
let sugar: Double
|
||||||
|
let fiber: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Update Entry Request
|
||||||
|
|
||||||
|
struct UpdateEntryRequest: Encodable {
|
||||||
|
let quantity: Double?
|
||||||
|
let mealType: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case quantity
|
||||||
|
case mealType = "meal_type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Delete Response
|
// MARK: - Delete Response
|
||||||
|
|
||||||
struct SuccessResponse: Decodable {
|
struct SuccessResponse: Decodable {
|
||||||
|
|||||||
@@ -6,15 +6,59 @@ final class GoalsViewModel {
|
|||||||
|
|
||||||
var goal: DailyGoal?
|
var goal: DailyGoal?
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
|
var isSaving = false
|
||||||
var error: String?
|
var error: String?
|
||||||
|
var saveSuccess = false
|
||||||
|
|
||||||
|
// Editable fields
|
||||||
|
var editCalories: String = ""
|
||||||
|
var editProtein: String = ""
|
||||||
|
var editCarbs: String = ""
|
||||||
|
var editFat: String = ""
|
||||||
|
var editSugar: String = ""
|
||||||
|
var editFiber: String = ""
|
||||||
|
|
||||||
func load() async {
|
func load() async {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
do {
|
do {
|
||||||
goal = try await api.getGoalsForDate(date: Date().apiDateString)
|
let loaded = try await api.getGoalsForDate(date: Date().apiDateString)
|
||||||
|
goal = loaded
|
||||||
|
populateFields(from: loaded)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = "No active goal found"
|
self.error = "No active goal found"
|
||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func save() async {
|
||||||
|
isSaving = true
|
||||||
|
saveSuccess = false
|
||||||
|
error = nil
|
||||||
|
do {
|
||||||
|
let request = UpdateGoalsRequest(
|
||||||
|
calories: Double(editCalories) ?? 0,
|
||||||
|
protein: Double(editProtein) ?? 0,
|
||||||
|
carbs: Double(editCarbs) ?? 0,
|
||||||
|
fat: Double(editFat) ?? 0,
|
||||||
|
sugar: Double(editSugar) ?? 0,
|
||||||
|
fiber: Double(editFiber) ?? 0
|
||||||
|
)
|
||||||
|
let updated = try await api.updateGoals(request)
|
||||||
|
goal = updated
|
||||||
|
populateFields(from: updated)
|
||||||
|
saveSuccess = true
|
||||||
|
} catch {
|
||||||
|
self.error = error.localizedDescription
|
||||||
|
}
|
||||||
|
isSaving = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func populateFields(from goal: DailyGoal) {
|
||||||
|
editCalories = "\(Int(goal.calories))"
|
||||||
|
editProtein = "\(Int(goal.protein))"
|
||||||
|
editCarbs = "\(Int(goal.carbs))"
|
||||||
|
editFat = "\(Int(goal.fat))"
|
||||||
|
editSugar = "\(Int(goal.sugar))"
|
||||||
|
editFiber = "\(Int(goal.fiber))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ final class TodayViewModel {
|
|||||||
entries.reduce(0) { $0 + $1.fat }
|
entries.reduce(0) { $0 + $1.fat }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var totalSugar: Double {
|
||||||
|
entries.reduce(0) { $0 + $1.sugar }
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalFiber: Double {
|
||||||
|
entries.reduce(0) { $0 + $1.fiber }
|
||||||
|
}
|
||||||
|
|
||||||
var calorieGoal: Double {
|
var calorieGoal: Double {
|
||||||
goal?.calories ?? 2000
|
goal?.calories ?? 2000
|
||||||
}
|
}
|
||||||
@@ -71,6 +79,14 @@ final class TodayViewModel {
|
|||||||
goal?.fat ?? 65
|
goal?.fat ?? 65
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sugarGoal: Double {
|
||||||
|
goal?.sugar ?? 50
|
||||||
|
}
|
||||||
|
|
||||||
|
var fiberGoal: Double {
|
||||||
|
goal?.fiber ?? 30
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
func load() async {
|
func load() async {
|
||||||
|
|||||||
@@ -3,8 +3,24 @@ import SwiftUI
|
|||||||
struct EntryDetailView: View {
|
struct EntryDetailView: View {
|
||||||
let entry: FoodEntry
|
let entry: FoodEntry
|
||||||
let onDelete: () -> Void
|
let onDelete: () -> Void
|
||||||
|
var onUpdate: (() -> Void)?
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var showDeleteConfirmation = false
|
@State private var showDeleteConfirmation = false
|
||||||
|
@State private var editQuantity: String
|
||||||
|
@State private var editMealType: MealType
|
||||||
|
@State private var isSaving = false
|
||||||
|
@State private var saveError: String?
|
||||||
|
@State private var saveSuccess = false
|
||||||
|
|
||||||
|
private let api = FitnessAPI()
|
||||||
|
|
||||||
|
init(entry: FoodEntry, onDelete: @escaping () -> Void, onUpdate: (() -> Void)? = nil) {
|
||||||
|
self.entry = entry
|
||||||
|
self.onDelete = onDelete
|
||||||
|
self.onUpdate = onUpdate
|
||||||
|
_editQuantity = State(initialValue: String(format: "%.1f", entry.quantity))
|
||||||
|
_editMealType = State(initialValue: MealType(rawValue: entry.mealType) ?? .snack)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -21,19 +37,112 @@ struct EntryDetailView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(Color.textSecondary)
|
.foregroundStyle(Color.textSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image(systemName: (MealType(rawValue: entry.mealType) ?? .snack).icon)
|
|
||||||
.foregroundStyle(Color.mealColor(for: entry.mealType))
|
|
||||||
Text(entry.mealType.capitalized)
|
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(Color.mealColor(for: entry.mealType))
|
|
||||||
}
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
|
// Editable fields
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Text("Edit Entry")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundStyle(Color.textPrimary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
// Meal type picker
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Meal")
|
||||||
|
.font(.subheadline.weight(.medium))
|
||||||
|
.foregroundStyle(Color.textSecondary)
|
||||||
|
Picker("Meal Type", selection: $editMealType) {
|
||||||
|
ForEach(MealType.allCases, id: \.self) { meal in
|
||||||
|
Label(meal.displayName, systemImage: meal.icon)
|
||||||
|
.tag(meal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quantity field
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Quantity (\(entry.unit))")
|
||||||
|
.font(.subheadline.weight(.medium))
|
||||||
|
.foregroundStyle(Color.textSecondary)
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button {
|
||||||
|
if let val = Double(editQuantity), val > 0.5 {
|
||||||
|
editQuantity = String(format: "%.1f", val - 0.5)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "minus.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(Color.accentWarm)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField("1.0", text: $editQuantity)
|
||||||
|
.font(.title3.weight(.semibold).monospacedDigit())
|
||||||
|
.foregroundStyle(Color.textPrimary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.frame(width: 80)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color.canvas)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
|
Button {
|
||||||
|
if let val = Double(editQuantity) {
|
||||||
|
editQuantity = String(format: "%.1f", val + 0.5)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(Color.accentWarm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if saveSuccess {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundStyle(Color.emerald)
|
||||||
|
Text("Entry updated")
|
||||||
|
.font(.caption.weight(.medium))
|
||||||
|
.foregroundStyle(Color.emerald)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = saveError {
|
||||||
|
Text(error)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task { await saveChanges() }
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
if isSaving {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
Text("Save Changes")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
}
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color.emerald)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
.disabled(isSaving)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.surfaceCard)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||||
|
.shadow(color: .black.opacity(0.04), radius: 6, y: 2)
|
||||||
|
|
||||||
// Nutrition grid
|
// Nutrition grid
|
||||||
LazyVGrid(columns: [
|
LazyVGrid(columns: [
|
||||||
GridItem(.flexible()),
|
GridItem(.flexible()),
|
||||||
@@ -54,7 +163,6 @@ struct EntryDetailView: View {
|
|||||||
// Metadata
|
// Metadata
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
metadataRow("Date", value: entry.entryDate)
|
metadataRow("Date", value: entry.entryDate)
|
||||||
metadataRow("Quantity", value: "\(String(format: "%.1f", entry.quantity)) \(entry.unit)")
|
|
||||||
metadataRow("Source", value: entry.source)
|
metadataRow("Source", value: entry.source)
|
||||||
metadataRow("Method", value: entry.entryMethod)
|
metadataRow("Method", value: entry.entryMethod)
|
||||||
if let note = entry.note, !note.isEmpty {
|
if let note = entry.note, !note.isEmpty {
|
||||||
@@ -93,6 +201,24 @@ struct EntryDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func saveChanges() async {
|
||||||
|
isSaving = true
|
||||||
|
saveError = nil
|
||||||
|
saveSuccess = false
|
||||||
|
do {
|
||||||
|
let request = UpdateEntryRequest(
|
||||||
|
quantity: Double(editQuantity),
|
||||||
|
mealType: editMealType.rawValue
|
||||||
|
)
|
||||||
|
_ = try await api.updateEntry(id: entry.id, body: request)
|
||||||
|
saveSuccess = true
|
||||||
|
onUpdate?()
|
||||||
|
} catch {
|
||||||
|
saveError = error.localizedDescription
|
||||||
|
}
|
||||||
|
isSaving = false
|
||||||
|
}
|
||||||
|
|
||||||
private func nutritionCell(_ label: String, value: Double, unit: String, color: Color) -> some View {
|
private func nutritionCell(_ label: String, value: Double, unit: String, color: Color) -> some View {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("\(Int(value))")
|
Text("\(Int(value))")
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ struct GoalsView: View {
|
|||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
if vm.isLoading {
|
if vm.isLoading {
|
||||||
LoadingView()
|
LoadingView()
|
||||||
} else if let goal = vm.goal {
|
} else if vm.goal != nil {
|
||||||
goalCard(goal)
|
goalEditor
|
||||||
} else {
|
} else {
|
||||||
EmptyStateView(
|
EmptyStateView(
|
||||||
icon: "target",
|
icon: "target",
|
||||||
@@ -32,7 +32,7 @@ struct GoalsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func goalCard(_ goal: DailyGoal) -> some View {
|
private var goalEditor: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Text("Daily Goals")
|
Text("Daily Goals")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@@ -43,20 +43,59 @@ struct GoalsView: View {
|
|||||||
GridItem(.flexible()),
|
GridItem(.flexible()),
|
||||||
GridItem(.flexible()),
|
GridItem(.flexible()),
|
||||||
], spacing: 16) {
|
], spacing: 16) {
|
||||||
goalItem("Calories", value: goal.calories, unit: "kcal", color: .emerald)
|
goalField("Calories", text: $vm.editCalories, unit: "kcal", color: .emerald)
|
||||||
goalItem("Protein", value: goal.protein, unit: "g", color: .macroProtein)
|
goalField("Protein", text: $vm.editProtein, unit: "g", color: .macroProtein)
|
||||||
goalItem("Carbs", value: goal.carbs, unit: "g", color: .macroCarbs)
|
goalField("Carbs", text: $vm.editCarbs, unit: "g", color: .macroCarbs)
|
||||||
goalItem("Fat", value: goal.fat, unit: "g", color: .macroFat)
|
goalField("Fat", text: $vm.editFat, unit: "g", color: .macroFat)
|
||||||
goalItem("Sugar", value: goal.sugar, unit: "g", color: .orange)
|
goalField("Sugar", text: $vm.editSugar, unit: "g", color: .orange)
|
||||||
goalItem("Fiber", value: goal.fiber, unit: "g", color: .green)
|
goalField("Fiber", text: $vm.editFiber, unit: "g", color: .green)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
if let goal = vm.goal {
|
||||||
Text("Active since \(goal.startDate)")
|
HStack {
|
||||||
.font(.caption)
|
Text("Active since \(goal.startDate)")
|
||||||
.foregroundStyle(Color.textTertiary)
|
.font(.caption)
|
||||||
Spacer()
|
.foregroundStyle(Color.textTertiary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vm.saveSuccess {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundStyle(Color.emerald)
|
||||||
|
Text("Goals saved")
|
||||||
|
.font(.caption.weight(.medium))
|
||||||
|
.foregroundStyle(Color.emerald)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = vm.error {
|
||||||
|
Text(error)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task { await vm.save() }
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
if vm.isSaving {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(.white)
|
||||||
|
}
|
||||||
|
Text("Save Goals")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
}
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color.emerald)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
.disabled(vm.isSaving)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color.surfaceCard)
|
.background(Color.surfaceCard)
|
||||||
@@ -64,12 +103,14 @@ struct GoalsView: View {
|
|||||||
.shadow(color: .black.opacity(0.04), radius: 6, y: 2)
|
.shadow(color: .black.opacity(0.04), radius: 6, y: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func goalItem(_ label: String, value: Double, unit: String, color: Color) -> some View {
|
private func goalField(_ label: String, text: Binding<String>, unit: String, color: Color) -> some View {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
Text("\(Int(value))")
|
TextField("0", text: text)
|
||||||
.font(.title2.weight(.bold).monospacedDigit())
|
.font(.title2.weight(.bold).monospacedDigit())
|
||||||
.foregroundStyle(color)
|
.foregroundStyle(color)
|
||||||
Text("\(unit)")
|
.multilineTextAlignment(.center)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
Text(unit)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(Color.textTertiary)
|
.foregroundStyle(Color.textTertiary)
|
||||||
Text(label)
|
Text(label)
|
||||||
|
|||||||
@@ -112,6 +112,18 @@ struct TodayView: View {
|
|||||||
goal: vm.fatGoal,
|
goal: vm.fatGoal,
|
||||||
color: .macroFat
|
color: .macroFat
|
||||||
)
|
)
|
||||||
|
MacroBar(
|
||||||
|
label: "Sugar",
|
||||||
|
consumed: vm.totalSugar,
|
||||||
|
goal: vm.sugarGoal,
|
||||||
|
color: .orange
|
||||||
|
)
|
||||||
|
MacroBar(
|
||||||
|
label: "Fiber",
|
||||||
|
consumed: vm.totalFiber,
|
||||||
|
goal: vm.fiberGoal,
|
||||||
|
color: .green
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user