Files
ccn/Packages/Persistence/Sources/Public/Models/ListingDraft.swift
T

221 lines
6.8 KiB
Swift
Raw Normal View History

import Foundation
/// What a seller fills in to list a copy. The listing's slug and placeholder cover are derived from it on publishing.
public struct ListingDraft: Sendable, Equatable {
// MARK: Properties
/// The series or book title, e.g. `Nightward`.
public var series: String
/// The issue number or volume, e.g. `1`; empty for a one-off book.
public var issue: String
/// The publisher's name.
public var publisher: String
/// The cover year.
public var year: Int
/// The format token, e.g. `single-issues`.
public var format: String
/// The printing, counted from one; zero for later or unknown.
public var printing: Int
/// The edition, e.g. `Direct`; empty when not applicable.
public var edition: String
/// The company that graded and slabbed the copy, or `nil` for a raw copy.
public var grader: String?
/// The numeric grade, or `nil` for formats that are not graded.
public var grade: Double?
/// The condition as the seller states it, e.g. `NM` or `Like new`.
public var condition: String
/// The grader's certificate number, or `nil` for a raw copy.
public var certNumber: String?
/// The page quality, or `nil` when not stated.
public var pageQuality: String?
/// Why the issue is a key, or `nil` when it is not one.
public var keyNotes: String?
/// The defects declared, from the grading guide's checklist.
public var defects: [String]
/// The seller's notes on the copy.
public var sellerNotes: String
/// The asking price, in euro cents.
public var priceCents: Int
/// The shipping cost to the Netherlands, in euro cents.
public var shippingCents: Int
/// Whether the seller takes offers on the copy.
public var acceptsOffers: Bool
/// The URL paths of the photos of this copy, the front first.
public var photos: [String]
// MARK: Initializers
public init(
series: String,
issue: String,
publisher: String,
year: Int,
format: String,
printing: Int,
edition: String,
grader: String?,
grade: Double?,
condition: String,
certNumber: String?,
pageQuality: String?,
keyNotes: String?,
defects: [String],
sellerNotes: String,
priceCents: Int,
shippingCents: Int,
acceptsOffers: Bool,
photos: [String]
) {
self.series = series
self.issue = issue
self.publisher = publisher
self.year = year
self.format = format
self.printing = printing
self.edition = edition
self.grader = grader
self.grade = grade
self.condition = condition
self.certNumber = certNumber
self.pageQuality = pageQuality
self.keyNotes = keyNotes
self.defects = defects
self.sellerNotes = sellerNotes
self.priceCents = priceCents
self.shippingCents = shippingCents
self.acceptsOffers = acceptsOffers
self.photos = photos
}
}
// MARK: - Extensions
extension ListingDraft {
// MARK: Constants
/// The tones a placeholder cover is drawn in.
static let tones = ["navy", "wine", "forest", "rust", "slate", "plum", "brick", "olive", "ink"]
// MARK: Computed
/// The placeholder cover: the series lettered over a few lines, on a tone picked by the series so every copy of it matches.
var cover: Listing.Cover {
var lines = [String]()
for word in series.uppercased().split(separator: " ").map(String.init) {
if let last = lines.last, last.count + word.count < 10 {
lines[lines.count - 1] = "\(last) \(word)"
} else {
lines.append(word)
}
}
let marker = switch format {
case "manga": issue.isEmpty ? "MANGA" : "VOL \(issue)"
case "trade-paperbacks": "TPB"
case "hardcovers": "HC"
default: issue
}
return .init(
tone: Self.tones[series.unicodeScalars.reduce(0) { ($0 + Int($1.value)) % Self.tones.count }],
title: lines.prefix(3).joined(separator: "\n"),
imprint: publisher.uppercased(),
number: marker
)
}
/// Whether the copy is a single issue.
var isSingleIssue: Bool {
format == "single-issues"
}
/// The printing in a few words, e.g. `1st print`.
var printingLabel: String {
let noun = isSingleIssue ? "print" : "printing"
switch printing {
case 1: return "1st \(noun)"
case 2: return "2nd \(noun)"
case 3: return "3rd \(noun)"
default: return "later \(noun)"
}
}
/// The printing in full, e.g. `First printing, direct edition`.
var printingDetail: String {
let ordinal = switch printing {
case 1: "First"
case 2: "Second"
case 3: "Third"
default: "Later"
}
return edition.isEmpty ? "\(ordinal) printing" : "\(ordinal) printing, \(edition.lowercased()) edition"
}
/// The listing's title, e.g. `Nightward #1`, or `Hollow Comet, Vol. 4`.
var title: String {
guard !issue.isEmpty else {
return series
}
return switch format {
case "single-issues": "\(series) #\(issue)"
case "manga": "\(series), Vol. \(issue)"
default: "\(series) \(issue)"
}
}
// MARK: Methods
/// The listing the draft publishes as.
func listing(
slug: String,
shop: Shop,
at date: Date
) -> Listing {
.init(
slug: slug,
shop: shop,
title: title,
seriesTitle: isSingleIssue ? "\(series) (\(year) series)" : series,
issueLabel: issue.isEmpty ? "—" : (isSingleIssue ? "#\(issue)" : issue),
publisher: publisher,
year: year,
format: format,
printing: printingLabel,
printingDetail: printingDetail,
isFirstPrint: printing == 1,
grader: grader,
grade: grade,
condition: condition,
certNumber: certNumber,
pageQuality: pageQuality,
keyNotes: keyNotes,
sellerNotes: sellerNotes,
priceCents: priceCents,
shippingCents: shippingCents,
acceptsOffers: acceptsOffers,
cover: cover,
listedAt: date,
defects: defects,
photos: photos
)
}
/// A string as a URL slug: lowercased, its letters and digits kept, everything else a single hyphen.
static func slug(
_ string: String
) -> String {
string
.lowercased()
.folding(options: .diacriticInsensitive, locale: nil)
.split { !($0.isASCII && ($0.isLetter || $0.isNumber)) }
.joined(separator: "-")
}
}