Add password auth, business browsing, dev proxy, and production env.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,19 +1,131 @@
|
||||
<template>
|
||||
<WebAppLayout>
|
||||
<section class="space-y-4">
|
||||
<h1 class="text-xl font-bold text-text">کشف خدمات محلی</h1>
|
||||
<p class="text-sm text-muted">
|
||||
بهزودی: دستهبندیها، جستجو و لیست کسبوکارها
|
||||
<section class="space-y-5">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-text">کشف خدمات</h1>
|
||||
<p class="mt-1 text-sm text-muted">کسبوکارهای محلی نزدیک شما</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="sr-only" for="search">جستجو</label>
|
||||
<input
|
||||
id="search"
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="جستجو در نام یا آدرس…"
|
||||
class="w-full rounded-xl border border-border bg-surface-2 px-4 py-3 text-sm text-text outline-none placeholder:text-muted focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingCategories" class="text-center text-sm text-muted">
|
||||
در حال بارگذاری دستهها…
|
||||
</div>
|
||||
<CategoryFilter
|
||||
v-else-if="categories.length"
|
||||
:categories="activeCategories"
|
||||
:selected-id="selectedCategoryId"
|
||||
@select="onCategorySelect"
|
||||
/>
|
||||
|
||||
<div v-if="loadingBusinesses" class="py-8 text-center text-sm text-muted">
|
||||
در حال بارگذاری…
|
||||
</div>
|
||||
<p
|
||||
v-else-if="error"
|
||||
class="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||
>
|
||||
{{ error }}
|
||||
</p>
|
||||
<div
|
||||
class="rounded-2xl border border-dashed border-border bg-surface-2 p-6 text-center text-sm text-muted"
|
||||
v-else-if="!filteredBusinesses.length"
|
||||
class="rounded-2xl border border-dashed border-border bg-surface-2 p-8 text-center text-sm text-muted"
|
||||
>
|
||||
فاز بعدی — صفحهٔ خانه
|
||||
کسبوکاری یافت نشد.
|
||||
</div>
|
||||
<ul v-else class="space-y-3">
|
||||
<li v-for="b in filteredBusinesses" :key="b.id">
|
||||
<BusinessCard :business="b" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</WebAppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { fetchBusinessCategories } from '@/api/business-categories';
|
||||
import { fetchBusinesses } from '@/api/businesses';
|
||||
import { ApiError } from '@/api/client';
|
||||
import type { Business, BusinessCategory } from '@/api/types';
|
||||
import BusinessCard from '@/components/business/BusinessCard.vue';
|
||||
import CategoryFilter from '@/components/business/CategoryFilter.vue';
|
||||
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
||||
|
||||
const categories = ref<BusinessCategory[]>([]);
|
||||
const businesses = ref<Business[]>([]);
|
||||
const selectedCategoryId = ref<string | null>(null);
|
||||
const searchQuery = ref('');
|
||||
const loadingCategories = ref(true);
|
||||
const loadingBusinesses = ref(true);
|
||||
const error = ref('');
|
||||
|
||||
const activeCategories = computed(() =>
|
||||
categories.value.filter((c) => c.isActive),
|
||||
);
|
||||
|
||||
const filteredBusinesses = computed(() => {
|
||||
const q = searchQuery.value.trim().toLowerCase();
|
||||
if (!q) {
|
||||
return businesses.value;
|
||||
}
|
||||
return businesses.value.filter((b) => {
|
||||
const haystack = [b.title, b.description, b.address]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
});
|
||||
});
|
||||
|
||||
async function loadCategories() {
|
||||
loadingCategories.value = true;
|
||||
try {
|
||||
const all = await fetchBusinessCategories();
|
||||
categories.value = all;
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e instanceof ApiError ? e.message : 'بارگذاری دستهها ناموفق بود.';
|
||||
} finally {
|
||||
loadingCategories.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBusinesses() {
|
||||
loadingBusinesses.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
businesses.value = await fetchBusinesses({
|
||||
categoryId: selectedCategoryId.value ?? undefined,
|
||||
isActive: true,
|
||||
});
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e instanceof ApiError ? e.message : 'بارگذاری کسبوکارها ناموفق بود.';
|
||||
} finally {
|
||||
loadingBusinesses.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onCategorySelect(id: string | null) {
|
||||
selectedCategoryId.value = id;
|
||||
}
|
||||
|
||||
watch(selectedCategoryId, () => {
|
||||
loadBusinesses();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCategories();
|
||||
await loadBusinesses();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user