landing desktop

This commit is contained in:
2026-06-12 20:16:14 +03:30
parent 820e80ac16
commit 5eec309b54
89 changed files with 4343 additions and 2309 deletions
+78 -116
View File
@@ -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>