import SwiftUI struct CreatorChannelDonationSection: View { let channelDonations: [CreatorChannelDonationResponse] let onSelectTab: (CreatorChannelTab) -> Void let onTapDonate: () -> Void init( channelDonations: [CreatorChannelDonationResponse], onSelectTab: @escaping (CreatorChannelTab) -> Void, onTapDonate: @escaping () -> Void = {} ) { self.channelDonations = channelDonations self.onSelectTab = onSelectTab self.onTapDonate = onTapDonate } var body: some View { VStack(alignment: .leading, spacing: SodaSpacing.s14) { SectionTitle(title: I18n.CreatorChannelHome.donation) { onSelectTab(.donation) } if channelDonations.isEmpty { CreatorChannelDonationEmptyCard(onTapDonate: onTapDonate) .padding(.horizontal, SodaSpacing.s14) } else { ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: SodaSpacing.s4) { ForEach(channelDonations.indices, id: \.self) { index in CreatorChannelDonationCard(donation: channelDonations[index]) .frame(width: 374) } } .padding(.leading, SodaSpacing.s14) .padding(.trailing, SodaSpacing.s14) } Button(action: onTapDonate) { HStack(spacing: SodaSpacing.s6) { Image("ic_new_donation") .resizable() .renderingMode(.template) .foregroundColor(.white) .scaledToFit() .frame(width: 20, height: 20) Text(I18n.ContentDetail.DonationDialog.title) .appFont(size: 16, weight: .medium) .foregroundColor(.white) .lineLimit(1) } .frame(maxWidth: .infinity) .padding(SodaSpacing.s12) .overlay( Capsule() .stroke(Color.white.opacity(0.3), lineWidth: 1) ) .contentShape(Rectangle()) } .buttonStyle(.plain) .padding(.horizontal, SodaSpacing.s14) } } .padding(.top, SodaSpacing.s20) } } private struct CreatorChannelDonationEmptyCard: View { let onTapDonate: () -> Void var body: some View { VStack(alignment: .leading, spacing: 0) { Text("처음으로 크리에이터를\n후원해 보세요!") .appFont(size: 24, weight: .bold) .foregroundColor(.white) .lineSpacing(0) .padding(.top, SodaSpacing.s20) .padding(.leading, SodaSpacing.s20) Spacer(minLength: 0) Button(action: onTapDonate) { HStack(spacing: SodaSpacing.s6) { Image("ic_new_donation") .resizable() .renderingMode(.template) .foregroundColor(.black) .scaledToFit() .frame(width: 20, height: 20) Text(I18n.ContentDetail.DonationDialog.title) .appFont(size: 16, weight: .medium) .foregroundColor(.black) .lineLimit(1) } .frame(maxWidth: .infinity) .padding(SodaSpacing.s12) .background(Color.white) .clipShape(Capsule()) } .buttonStyle(.plain) .padding(.horizontal, SodaSpacing.s20) .padding(.bottom, SodaSpacing.s20) } .frame(height: 196) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } } private struct CreatorChannelDonationCard: View { let donation: CreatorChannelDonationResponse var body: some View { VStack(alignment: .leading, spacing: SodaSpacing.s14) { header Text(donation.messageText) .appFont(size: 16, weight: .regular) .foregroundColor(.white) .lineLimit(2) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, SodaSpacing.s14) } .padding(.bottom, SodaSpacing.s14) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray900) .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } private var header: some View { HStack(alignment: .center, spacing: SodaSpacing.s8) { DownsampledKFImage( url: URL(string: donation.profileImageUrl), size: CGSize(width: 42, height: 42) ) .background(Color.white.opacity(0.6)) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text(donation.nickname) .appFont(size: 14, weight: .medium) .foregroundColor(.black) .lineLimit(1) .truncationMode(.tail) Text(donation.relativeTimeText()) .appFont(size: 14, weight: .regular) .foregroundColor(Color.gray700) .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) canBadge } .padding(SodaSpacing.s14) .background(donation.tierColor) } private var canBadge: some View { HStack(spacing: 2) { Image("ic_bar_cash") .resizable() .scaledToFit() .frame(width: 18, height: 18) Text("\(donation.can)캔") .appFont(size: 14, weight: .regular) .foregroundColor(.white) .lineLimit(1) } .padding(.horizontal, SodaSpacing.s6) .padding(.vertical, SodaSpacing.s4) .background(Color.gray900) .clipShape(Capsule()) } } private extension CreatorChannelDonationResponse { var messageText: String { let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines) return trimmedMessage.isEmpty ? "\(can)캔을 후원하였습니다" : trimmedMessage } func relativeTimeText(now: Date = Date()) -> String { DateParser.relativeTimeText(fromUTC: createdAtUtc, fallback: createdAtUtc, now: now) } var tierColor: Color { switch can { case 1...50: return Color(hex: "E2E2E2") case 51...100: return Color(hex: "73EE01") case 101...500: return Color(hex: "00EAFF") default: return Color(hex: "FF4C3C") } } } struct CreatorChannelDonationSection_Previews: PreviewProvider { static var previews: some View { VStack(spacing: SodaSpacing.s20) { CreatorChannelDonationSection( channelDonations: [], onSelectTab: { _ in } ) CreatorChannelDonationSection( channelDonations: [ CreatorChannelDonationResponse( nickname: "팬 이름 501", profileImageUrl: "https://picsum.photos/120/120?4", can: 501, message: "501캔 이상 후원 컬러를 확인하는 Preview 샘플입니다.", createdAtUtc: "2026-07-03T03:41:00Z" ), CreatorChannelDonationResponse( nickname: "팬 이름 50", profileImageUrl: "https://picsum.photos/120/120?1", can: 50, message: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", createdAtUtc: "2026-07-03T03:44:00Z" ), CreatorChannelDonationResponse( nickname: "팬 이름 100", profileImageUrl: "https://picsum.photos/120/120?2", can: 100, message: "51~100캔 후원 컬러를 확인하는 Preview 샘플입니다.", createdAtUtc: "2026-07-03T03:43:00Z" ), CreatorChannelDonationResponse( nickname: "팬 이름 500", profileImageUrl: "https://picsum.photos/120/120?3", can: 500, message: "101~500캔 후원 컬러를 확인하는 Preview 샘플입니다.", createdAtUtc: "2026-07-03T03:42:00Z" ) ], onSelectTab: { _ in } ) } .background(Color.black) .previewLayout(.sizeThatFits) } }