|
|
@@ -13,12 +13,13 @@ struct FiltersPanelView: View {
|
|
|
@State private var expanded: Set<FilterSection> = [
|
|
|
.jobTitle, .exclude, .location, .employment, .experience
|
|
|
]
|
|
|
+ @State private var salaryText: String = ""
|
|
|
|
|
|
var body: some View {
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
header
|
|
|
.padding(.horizontal, AppSpacing.md)
|
|
|
- .padding(.top, AppSpacing.lg)
|
|
|
+ .padding(.top, AppSpacing.sm)
|
|
|
.padding(.bottom, AppSpacing.md)
|
|
|
|
|
|
Divider().foregroundStyle(AppColors.border)
|
|
|
@@ -37,6 +38,8 @@ struct FiltersPanelView: View {
|
|
|
}
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
.background(AppColors.surface)
|
|
|
+ .onAppear { syncSalaryText() }
|
|
|
+ .onChange(of: criteria.salaryFloor) { _, _ in syncSalaryText() }
|
|
|
}
|
|
|
|
|
|
private var header: some View {
|
|
|
@@ -93,14 +96,13 @@ struct FiltersPanelView: View {
|
|
|
|
|
|
private var jobTitleSection: some View {
|
|
|
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
|
|
- filterField("Job title", text: $criteria.jobTitle, prompt: "e.g. iOS Developer")
|
|
|
- filterField("Keywords", text: $criteria.keywords, prompt: "Skills or keywords")
|
|
|
+ FilterTextField(title: "Job title", prompt: "e.g. iOS Developer", text: $criteria.jobTitle)
|
|
|
+ FilterTextField(title: "Keywords", prompt: "Skills or keywords", text: $criteria.keywords)
|
|
|
+
|
|
|
Toggle(isOn: $criteria.exactPhrase) {
|
|
|
Text("Exact phrase")
|
|
|
- .font(AppTypography.body)
|
|
|
- .foregroundStyle(AppColors.textSecondary)
|
|
|
}
|
|
|
- .toggleStyle(.checkbox)
|
|
|
+ .toggleStyle(FilterCheckboxToggleStyle())
|
|
|
.onChange(of: criteria.exactPhrase) { _, _ in onChange() }
|
|
|
}
|
|
|
.onChange(of: criteria.jobTitle) { _, _ in onChange() }
|
|
|
@@ -108,31 +110,31 @@ struct FiltersPanelView: View {
|
|
|
}
|
|
|
|
|
|
private var excludeSection: some View {
|
|
|
- filterField("Exclude keywords", text: $criteria.excludedKeywords, prompt: "Words to exclude")
|
|
|
- .onChange(of: criteria.excludedKeywords) { _, _ in onChange() }
|
|
|
+ FilterTextField(
|
|
|
+ title: "Exclude keywords",
|
|
|
+ prompt: "Words to exclude",
|
|
|
+ text: $criteria.excludedKeywords
|
|
|
+ )
|
|
|
+ .onChange(of: criteria.excludedKeywords) { _, _ in onChange() }
|
|
|
}
|
|
|
|
|
|
private var locationSection: some View {
|
|
|
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
|
|
- VStack(alignment: .leading, spacing: 4) {
|
|
|
- Text("Country")
|
|
|
- .font(AppTypography.caption)
|
|
|
- .foregroundStyle(AppColors.textTertiary)
|
|
|
- Picker("Country", selection: $criteria.country) {
|
|
|
- Text("Pakistan").tag("Pakistan")
|
|
|
- Text("United States").tag("United States")
|
|
|
- Text("United Kingdom").tag("United Kingdom")
|
|
|
- Text("Canada").tag("Canada")
|
|
|
- Text("Remote / Any").tag("")
|
|
|
- }
|
|
|
- .labelsHidden()
|
|
|
- .onChange(of: criteria.country) { _, _ in onChange() }
|
|
|
- }
|
|
|
-
|
|
|
- filterField("City / Region", text: $criteria.city, prompt: "e.g. Lahore")
|
|
|
- .onChange(of: criteria.city) { _, _ in onChange() }
|
|
|
-
|
|
|
- VStack(alignment: .leading, spacing: 8) {
|
|
|
+ FilterTextField(
|
|
|
+ title: "Country",
|
|
|
+ prompt: "e.g. Pakistan, United States",
|
|
|
+ text: $criteria.country
|
|
|
+ )
|
|
|
+ .onChange(of: criteria.country) { _, _ in onChange() }
|
|
|
+
|
|
|
+ FilterTextField(
|
|
|
+ title: "City / Region",
|
|
|
+ prompt: "e.g. Lahore",
|
|
|
+ text: $criteria.city
|
|
|
+ )
|
|
|
+ .onChange(of: criteria.city) { _, _ in onChange() }
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
ForEach([RemoteType.remote, .hybrid, .onsite], id: \.self) { type in
|
|
|
Toggle(isOn: Binding(
|
|
|
get: { criteria.remoteTypes.contains(type) },
|
|
|
@@ -143,17 +145,16 @@ struct FiltersPanelView: View {
|
|
|
}
|
|
|
)) {
|
|
|
Text(type.filterLabel)
|
|
|
- .font(AppTypography.body)
|
|
|
- .foregroundStyle(AppColors.textSecondary)
|
|
|
}
|
|
|
- .toggleStyle(.checkbox)
|
|
|
+ .toggleStyle(FilterCheckboxToggleStyle())
|
|
|
}
|
|
|
}
|
|
|
+ .padding(.top, 4)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private var employmentSection: some View {
|
|
|
- VStack(alignment: .leading, spacing: 8) {
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
ForEach(EmploymentType.allCases) { type in
|
|
|
Toggle(isOn: Binding(
|
|
|
get: { criteria.employmentTypes.contains(type) },
|
|
|
@@ -164,16 +165,14 @@ struct FiltersPanelView: View {
|
|
|
}
|
|
|
)) {
|
|
|
Text(type.displayName)
|
|
|
- .font(AppTypography.body)
|
|
|
- .foregroundStyle(AppColors.textSecondary)
|
|
|
}
|
|
|
- .toggleStyle(.checkbox)
|
|
|
+ .toggleStyle(FilterCheckboxToggleStyle())
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private var experienceSection: some View {
|
|
|
- VStack(alignment: .leading, spacing: 8) {
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
ForEach(ExperienceLevel.allCases) { level in
|
|
|
Toggle(isOn: Binding(
|
|
|
get: { criteria.experienceLevels.contains(level) },
|
|
|
@@ -184,48 +183,34 @@ struct FiltersPanelView: View {
|
|
|
}
|
|
|
)) {
|
|
|
Text(level.displayName)
|
|
|
- .font(AppTypography.body)
|
|
|
- .foregroundStyle(AppColors.textSecondary)
|
|
|
}
|
|
|
- .toggleStyle(.checkbox)
|
|
|
+ .toggleStyle(FilterCheckboxToggleStyle())
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private var salarySection: some View {
|
|
|
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
|
|
- Text("Minimum salary (USD / year)")
|
|
|
- .font(AppTypography.caption)
|
|
|
- .foregroundStyle(AppColors.textTertiary)
|
|
|
-
|
|
|
- TextField("e.g. 40000", value: $criteria.salaryFloor, format: .number)
|
|
|
- .textFieldStyle(.plain)
|
|
|
- .padding(10)
|
|
|
- .background(AppColors.backgroundPrimary)
|
|
|
- .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
|
|
- .overlay(
|
|
|
- RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
- .stroke(AppColors.border, lineWidth: 1)
|
|
|
- )
|
|
|
- .onChange(of: criteria.salaryFloor) { _, _ in onChange() }
|
|
|
+ FilterTextField(
|
|
|
+ title: "Minimum salary (USD / year)",
|
|
|
+ prompt: "e.g. 40000",
|
|
|
+ text: $salaryText
|
|
|
+ )
|
|
|
+ .onChange(of: salaryText) { _, newValue in
|
|
|
+ let digits = newValue.filter(\.isNumber)
|
|
|
+ if digits != newValue {
|
|
|
+ salaryText = digits
|
|
|
+ }
|
|
|
+ criteria.salaryFloor = Double(digits)
|
|
|
+ onChange()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func filterField(_ title: String, text: Binding<String>, prompt: String) -> some View {
|
|
|
- VStack(alignment: .leading, spacing: 4) {
|
|
|
- Text(title)
|
|
|
- .font(AppTypography.caption)
|
|
|
- .foregroundStyle(AppColors.textTertiary)
|
|
|
- TextField(prompt, text: text)
|
|
|
- .textFieldStyle(.plain)
|
|
|
- .font(AppTypography.body)
|
|
|
- .padding(10)
|
|
|
- .background(AppColors.backgroundPrimary)
|
|
|
- .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
|
|
- .overlay(
|
|
|
- RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
- .stroke(AppColors.border, lineWidth: 1)
|
|
|
- )
|
|
|
+ private func syncSalaryText() {
|
|
|
+ let next = criteria.salaryFloor.map { String(Int($0)) } ?? ""
|
|
|
+ if salaryText != next {
|
|
|
+ salaryText = next
|
|
|
}
|
|
|
}
|
|
|
}
|