feat: ios mobil arayüz tasarımı

This commit is contained in:
2026-02-11 18:06:35 +03:00
parent 69884db0ab
commit 261b2f58cc
42 changed files with 2501 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
import SwiftUI
struct BlurFogOverlay: View {
var body: some View {
Rectangle()
.fill(
LinearGradient(
colors: [Color.clear, Theme.background.opacity(0.82), Theme.background],
startPoint: .top,
endPoint: .bottom
)
)
.background(.ultraThinMaterial)
.ignoresSafeArea(edges: .bottom)
.allowsHitTesting(false)
}
}

View File

@@ -0,0 +1,57 @@
import SwiftUI
struct BookCoverCard: View {
let book: BookRemote
let imageCache: ImageCacheProtocol
var body: some View {
VStack(spacing: 6) {
RemoteImageView(url: book.coverImageUrl, imageCache: imageCache)
.frame(width: 98, height: 145)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black.opacity(0.08), lineWidth: 1)
}
.shadow(color: .black.opacity(0.16), radius: 6, y: 4)
Text(book.title)
.font(.caption)
.lineLimit(1)
.foregroundStyle(.primary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(book.title), \(book.authors.joined(separator: ", "))")
}
}
private struct RemoteImageView: View {
let url: URL?
let imageCache: ImageCacheProtocol
@State private var image: UIImage?
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(LinearGradient(colors: [Color.gray.opacity(0.22), Color.gray.opacity(0.35)], startPoint: .top, endPoint: .bottom))
if let image {
Image(uiImage: image)
.resizable()
.scaledToFill()
} else {
Image(systemName: "book.closed")
.font(.title3)
.foregroundStyle(Color.black.opacity(0.35))
}
}
.task(id: url) {
guard let url else { return }
do {
image = try await imageCache.image(for: url)
} catch {
image = nil
}
}
}
}

View File

@@ -0,0 +1,21 @@
import SwiftUI
struct NetworkErrorView: View {
let message: String
let retryAction: () -> Void
var body: some View {
VStack(spacing: 12) {
Image(systemName: "wifi.exclamationmark")
.font(.title2)
Text(message)
.font(.subheadline)
.multilineTextAlignment(.center)
Button(String(localized: "common.retry"), action: retryAction)
.buttonStyle(.borderedProminent)
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
.padding(.horizontal)
}
}

View File

@@ -0,0 +1,21 @@
import SwiftUI
struct PrimaryPillButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.contentShape(Rectangle())
}
.background(Color.black)
.clipShape(Capsule())
.shadow(color: .black.opacity(0.28), radius: 18, y: 8)
.accessibilityLabel(title)
}
}

View File

@@ -0,0 +1,14 @@
import SwiftUI
struct ScrewView: View {
var body: some View {
ZStack {
Circle()
.fill(Color.black.opacity(0.25))
Circle()
.stroke(Color.white.opacity(0.5), lineWidth: 1)
.padding(1)
}
.frame(width: 9, height: 9)
}
}

View File

@@ -0,0 +1,85 @@
import SwiftUI
struct ShelfSectionView: View {
let title: String
let books: [BookRemote]
let gradient: LinearGradient
let imageCache: ImageCacheProtocol
let onTapCategory: () -> Void
let onTapBook: (BookRemote) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Button(action: onTapCategory) {
Text(title)
.font(.title3.weight(.bold))
.foregroundStyle(Theme.textPrimary)
}
Spacer()
Text("\(books.count) books")
.font(.caption)
.foregroundStyle(Theme.textSecondary)
Image(systemName: "chevron.left")
.font(.caption2)
.foregroundStyle(.gray)
Image(systemName: "chevron.right")
.font(.caption2)
.foregroundStyle(.gray)
}
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 16)
.fill(gradient)
.frame(height: 74)
.shadow(color: .black.opacity(0.12), radius: 10, y: 8)
.overlay {
HStack {
ScrewView()
Spacer()
ScrewView()
}
.padding(.horizontal, 12)
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
if books.isEmpty {
ghostCovers
} else {
ForEach(books) { book in
Button {
onTapBook(book)
} label: {
BookCoverCard(book: book, imageCache: imageCache)
}
}
}
}
.padding(.horizontal, 16)
.padding(.bottom, 18)
}
.frame(height: 190)
}
}
.padding(.horizontal, 20)
}
private var ghostCovers: some View {
HStack(spacing: 10) {
ForEach(0..<3, id: \.self) { _ in
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.opacity(0.5))
.frame(width: 92, height: 140)
.overlay {
Image(systemName: "book")
.foregroundStyle(.white.opacity(0.8))
}
}
Text(String(localized: "home.noBooks"))
.font(.caption)
.foregroundStyle(.secondary)
.frame(width: 130, alignment: .leading)
}
}
}

View File

@@ -0,0 +1,29 @@
import SwiftUI
enum Theme {
static let background = Color(red: 0.97, green: 0.96, blue: 0.94)
static let textPrimary = Color.black
static let textSecondary = Color.gray
static let designShelf = LinearGradient(
colors: [Color(red: 0.87, green: 0.66, blue: 0.45), Color(red: 0.79, green: 0.55, blue: 0.34)],
startPoint: .top,
endPoint: .bottom
)
static let psychologyShelf = LinearGradient(
colors: [Color(red: 0.58, green: 0.67, blue: 0.78), Color(red: 0.45, green: 0.55, blue: 0.66)],
startPoint: .top,
endPoint: .bottom
)
static let novelsShelf = LinearGradient(
colors: [Color.white, Color(red: 0.9, green: 0.9, blue: 0.9)],
startPoint: .top,
endPoint: .bottom
)
static func headerSerif(size: CGFloat) -> Font {
.custom("NewYork-Regular", size: size, relativeTo: .largeTitle)
}
}