ζ-3: SubmitQuoteView nativ (Form + authGate + POST /quotes)
Native Submit-Flow gegen zitare-api. SwiftUI-Form mit: - TextEditor mit 10-1000-Zeichen-Validation + Counter - Sprache (de/en/fr/es/it Picker) - Author-Name (mandatory) - Optional Source-Section (Toggle): Titel + Art (book/article/talk/film/other) + Jahr - CC-BY-SA-4.0-Zustimmung als Pflicht-Toggle - Submit-Button erst aktiv wenn alle 3 Bedingungen erfüllt - authGate.require(reason: "submit") öffnet Login-Sheet wenn nötig; Submit feuert auto nach signedIn - Error-Banner mit lokalisiertem API-Code (api.error.<code> wird in xcstrings nachgeschlagen) - Success-Banner mit Slug + "wartet auf Moderation"-Hinweis Neu in Submit-Tab als 4. Tab (Lesen / Erkunden / Einreichen / Konto). - ZitareAPI: submitQuote(_:), QuoteDraft, SubmittedQuote, ZitareAPIError - SubmitQuoteView ersetzt Placeholder-Stub - RootView: AppTab.submit ergänzt Offen: Offline-Queue (PendingSubmission via SwiftData) — bei Network- Failure bleibt der Draft im Form-State und User retried manuell. Nicht in ζ-3 abgeschlossen, gehört in ζ-3.5. Offen: api.error.*-Keys in zitare-native Localizable.xcstrings — aktuell nur DE-Source. EN/FR/ES/IT folgen separat. iOS + macOS BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7ba8684074
commit
127c81b74c
3 changed files with 323 additions and 33 deletions
|
|
@ -3,19 +3,15 @@ import ManaCore
|
|||
|
||||
/// Zitare-spezifischer API-Client. Wrapper um `AuthenticatedTransport`
|
||||
/// aus ManaCore, der die zitare-api-Endpoints kennt.
|
||||
///
|
||||
/// Phase ζ-0: nur Health-Probe. Endpoints für Submit, Share-Receive,
|
||||
/// Quote-Lookup folgen in ζ-3 / ζ-4.
|
||||
actor ZitareAPI {
|
||||
let transport: AuthenticatedTransport
|
||||
let decoder: JSONDecoder
|
||||
let encoder: JSONEncoder
|
||||
|
||||
init(auth: AuthClient) {
|
||||
transport = AuthenticatedTransport(baseURL: AppConfig.apiBaseURL, auth: auth)
|
||||
decoder = JSONDecoder()
|
||||
// ζ-3 TODO: bei echten DTOs `.iso8601withFractional`-Extension
|
||||
// aus cards-native portieren (Server liefert ISO8601 mit
|
||||
// Fractional-Seconds, Standard `.iso8601` schluckt das nicht).
|
||||
encoder = JSONEncoder()
|
||||
}
|
||||
|
||||
/// `GET /healthz` — verifiziert dass zitare-api erreichbar ist.
|
||||
|
|
@ -29,11 +25,82 @@ actor ZitareAPI {
|
|||
return http.statusCode == 200
|
||||
}
|
||||
|
||||
// MARK: - Phase ζ-3: Submit
|
||||
/// `POST /api/v1/quotes` — Quote-Draft einreichen. Server-Schema:
|
||||
/// `zitare/apps/api/src/routes/quotes.ts:submissionSchema`. Pflicht:
|
||||
/// text + acceptedTos:true + (authorName ODER authorSlug). Optionale
|
||||
/// Source-Felder kommen mit, wenn der User sie ausgefüllt hat.
|
||||
func submitQuote(_ draft: QuoteDraft) async throws -> SubmittedQuote {
|
||||
let payload = try encoder.encode(draft)
|
||||
let (data, http) = try await transport.request(
|
||||
path: "/api/v1/quotes",
|
||||
method: "POST",
|
||||
body: payload
|
||||
)
|
||||
try ensureOK(http, data: data)
|
||||
return try decoder.decode(SubmittedQuote.self, from: data)
|
||||
}
|
||||
|
||||
// func submitQuote(_ draft: QuoteDraft) async throws -> SubmittedQuote { ... }
|
||||
// MARK: - Helpers
|
||||
|
||||
// MARK: - Phase ζ-4: Share-Receive
|
||||
private func ensureOK(_ http: HTTPURLResponse, data: Data) throws {
|
||||
guard http.statusCode >= 400 else { return }
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
let code = Self.parseErrorCode(from: body)
|
||||
throw ZitareAPIError(status: http.statusCode, code: code, body: body)
|
||||
}
|
||||
|
||||
// func receiveShare(_ envelope: ShareEnvelope) async throws -> ShareReceipt { ... }
|
||||
private static func parseErrorCode(from body: String) -> String? {
|
||||
guard let data = body.data(using: .utf8),
|
||||
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let code = obj["error"] as? String
|
||||
else { return nil }
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DTOs
|
||||
|
||||
/// Submit-Payload für `POST /api/v1/quotes`. JSON-Keys matchen
|
||||
/// `submissionSchema` aus `zitare/apps/api/src/routes/quotes.ts`.
|
||||
struct QuoteDraft: Codable, Sendable, Equatable {
|
||||
var text: String
|
||||
var language: String
|
||||
var authorName: String?
|
||||
var authorSlug: String?
|
||||
var sourceTitle: String?
|
||||
var sourceKind: SourceKind?
|
||||
var sourceYear: Int?
|
||||
var editReason: String?
|
||||
var acceptedTos: Bool
|
||||
|
||||
enum SourceKind: String, Codable, Sendable, CaseIterable, Identifiable {
|
||||
case book, article, talk, film, other
|
||||
var id: String { rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Response von `POST /api/v1/quotes`. Server gibt das volle
|
||||
/// QuoteRow zurück — wir brauchen für die UI nur slug + status.
|
||||
struct SubmittedQuote: Codable, Sendable {
|
||||
let slug: String
|
||||
let status: String
|
||||
}
|
||||
|
||||
/// Lokalisierter API-Fehler. `code` ist der server-seitige Code aus
|
||||
/// `{ "error": "<code>" }`, der via `api.error.<code>` im
|
||||
/// Localizable.xcstrings nachgeschlagen wird.
|
||||
struct ZitareAPIError: LocalizedError {
|
||||
let status: Int
|
||||
let code: String?
|
||||
let body: String
|
||||
|
||||
var errorDescription: String? {
|
||||
if let code, !code.isEmpty {
|
||||
let key = "api.error.\(code)"
|
||||
let localized = Bundle.main.localizedString(forKey: key, value: nil, table: nil)
|
||||
if localized != key { return localized }
|
||||
return "Fehler \(status): \(code)"
|
||||
}
|
||||
return "HTTP \(status)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue