landing desktop
This commit is contained in:
+78
-116
@@ -1,153 +1,115 @@
|
||||
<template>
|
||||
<WebAppLayout>
|
||||
<BusinessCardSkeleton v-if="bootstrapping" class="py-2" />
|
||||
<DesktopLandingPage
|
||||
v-if="showDesktopLanding"
|
||||
:services="landingServices"
|
||||
:loading-services="loadingLandingServices"
|
||||
:services-error="landingServicesError"
|
||||
/>
|
||||
|
||||
<section v-else class="space-y-5">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-text">کشف خدمات</h1>
|
||||
<p class="mt-1 text-sm text-muted">کسبوکارهای محلی نزدیک شما</p>
|
||||
</div>
|
||||
<WebAppLayout v-else-if="showMobileLanding">
|
||||
<HomeHeroSection @book="goToBooking" @browse="scrollToServices" />
|
||||
<HomeServicesSection
|
||||
:items="landingServices"
|
||||
:loading="loadingLandingServices"
|
||||
:error="landingServicesError"
|
||||
/>
|
||||
<HomeFeatureBar />
|
||||
</WebAppLayout>
|
||||
|
||||
<div v-if="showSearch" class="relative">
|
||||
<label class="sr-only" for="search">جستجو</label>
|
||||
<AppIcon
|
||||
name="search"
|
||||
size="sm"
|
||||
class="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-muted"
|
||||
/>
|
||||
<input
|
||||
id="search"
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="جستجو در نام یا آدرس…"
|
||||
class="w-full rounded-xl border border-border bg-surface-2 py-3 pe-4 ps-10 text-sm text-text outline-none placeholder:text-muted focus:border-brand-500 focus:ring-2 focus:ring-brand-500/30"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!tenant.loaded && isDesktop"
|
||||
class="flex min-h-dvh items-center justify-center bg-white dark:bg-[#0d1117]"
|
||||
>
|
||||
<BusinessCardSkeleton class="w-full max-w-5xl px-8" />
|
||||
</div>
|
||||
|
||||
<CategoryFilter
|
||||
v-if="showCategoryFilter"
|
||||
:categories="activeCategories"
|
||||
:selected-id="selectedCategoryId"
|
||||
@select="onCategorySelect"
|
||||
/>
|
||||
<DesktopDiscoveryPage v-else-if="tenant.loaded && isDesktop" />
|
||||
|
||||
<BusinessCardSkeleton v-if="loadingBusinesses" class="py-2" />
|
||||
<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
|
||||
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-2">
|
||||
<li v-for="b in filteredBusinesses" :key="b.id">
|
||||
<BusinessCard :business="b" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<WebAppLayout v-else>
|
||||
<BusinessCardSkeleton v-if="!tenant.loaded" class="py-2" />
|
||||
<HomeDiscoveryContent v-else />
|
||||
</WebAppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
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 HomeDiscoveryContent from '@/components/home/HomeDiscoveryContent.vue';
|
||||
import HomeFeatureBar from '@/components/home/HomeFeatureBar.vue';
|
||||
import HomeHeroSection from '@/components/home/HomeHeroSection.vue';
|
||||
import HomeServicesSection from '@/components/home/HomeServicesSection.vue';
|
||||
import DesktopDiscoveryPage from '@/components/landing/DesktopDiscoveryPage.vue';
|
||||
import DesktopLandingPage from '@/components/landing/DesktopLandingPage.vue';
|
||||
import BusinessCardSkeleton from '@/components/ui/BusinessCardSkeleton.vue';
|
||||
import AppIcon from '@/components/ui/AppIcon.vue';
|
||||
import CategoryFilter from '@/components/business/CategoryFilter.vue';
|
||||
import WebAppLayout from '@/layouts/WebAppLayout.vue';
|
||||
import { useTenantStore } from '@/stores/tenant';
|
||||
import type { LandingServiceItem } from '@/types/landing';
|
||||
import { fetchLandingServices } from '@/utils/landing-services';
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const router = useRouter();
|
||||
const tenant = useTenantStore();
|
||||
|
||||
const categories = ref<BusinessCategory[]>([]);
|
||||
const businesses = ref<Business[]>([]);
|
||||
const selectedCategoryId = ref<string | null>(null);
|
||||
const searchQuery = ref('');
|
||||
const bootstrapping = ref(true);
|
||||
const loadingBusinesses = ref(false);
|
||||
const error = ref('');
|
||||
const landingServices = ref<LandingServiceItem[]>([]);
|
||||
const loadingLandingServices = ref(false);
|
||||
const landingServicesError = ref('');
|
||||
|
||||
const activeCategories = computed(() =>
|
||||
categories.value.filter((c) => c.isActive),
|
||||
const showDesktopLanding = computed(
|
||||
() => tenant.loaded && tenant.isSingleBusiness && isDesktop.value,
|
||||
);
|
||||
|
||||
const showCategoryFilter = computed(() => activeCategories.value.length > 1);
|
||||
const showMobileLanding = computed(
|
||||
() => tenant.loaded && tenant.isSingleBusiness && !isDesktop.value,
|
||||
);
|
||||
|
||||
const showSearch = computed(() => businesses.value.length > 1);
|
||||
const showSingleBusinessLanding = computed(
|
||||
() => showDesktopLanding.value || showMobileLanding.value,
|
||||
);
|
||||
|
||||
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 loadLandingServices() {
|
||||
if (!tenant.singleBusinessId) return;
|
||||
|
||||
async function loadCategories() {
|
||||
loadingLandingServices.value = true;
|
||||
landingServicesError.value = '';
|
||||
try {
|
||||
categories.value = await fetchBusinessCategories();
|
||||
if (activeCategories.value.length === 1) {
|
||||
selectedCategoryId.value = activeCategories.value[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
error.value =
|
||||
e instanceof ApiError ? e.message : 'بارگذاری دستهها ناموفق بود.';
|
||||
}
|
||||
}
|
||||
|
||||
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 : 'بارگذاری کسبوکارها ناموفق بود.';
|
||||
landingServices.value = await fetchLandingServices(tenant.singleBusinessId);
|
||||
} catch {
|
||||
landingServices.value = [];
|
||||
} finally {
|
||||
loadingBusinesses.value = false;
|
||||
loadingLandingServices.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onCategorySelect(id: string | null) {
|
||||
selectedCategoryId.value = id;
|
||||
function scrollToServices() {
|
||||
const el = document.getElementById('services');
|
||||
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
watch(selectedCategoryId, () => {
|
||||
if (!bootstrapping.value) {
|
||||
void loadBusinesses();
|
||||
function goToBooking() {
|
||||
const id = tenant.singleBusinessId;
|
||||
if (!id) {
|
||||
scrollToServices();
|
||||
return;
|
||||
}
|
||||
|
||||
void router.push({
|
||||
name: 'app-business-detail',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
watch(showSingleBusinessLanding, (show) => {
|
||||
if (show) {
|
||||
void loadLandingServices();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCategories();
|
||||
await loadBusinesses();
|
||||
await tenant.bootstrap();
|
||||
|
||||
if (businesses.value.length === 1) {
|
||||
await router.replace({
|
||||
name: 'app-business-detail',
|
||||
params: { id: businesses.value[0].id },
|
||||
});
|
||||
return;
|
||||
if (showSingleBusinessLanding.value) {
|
||||
await loadLandingServices();
|
||||
}
|
||||
|
||||
bootstrapping.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user