+
+
+
+ {{ primary.business?.title ?? 'کسبوکار' }}
-
- {{ serviceTitle }}
-
-
- {{ groupHint }}
-
-
- {{ formatDate(booking.scheduledAt) }}
-
-
- {{ booking.customer.fullName ? `${booking.customer.fullName} · ` : '' }}{{ booking.customer.mobile }}
-
-
{{ booking.note }}
- {{ bookingStatusLabel(booking.status) }}
+ {{ statusLabel }}
+
+
+ کلاس {{ classTitle }}
+
+
+
+ -
+ {{ serviceTitleForBooking(booking) }}
+
+ {{ bookingStatusLabel(booking.status) }}
+
+
+
+
+ {{ serviceTitleForBooking(primary) }}
+
+
+
+ {{ formatDate(primary.scheduledAt) }}
+
+
+ {{
+ primary.customer.fullName
+ ? `${primary.customer.fullName} · `
+ : ''
+ }}{{ primary.customer.mobile }}
+
+
+ {{ primary.note }}
+
تأیید
@@ -47,8 +73,8 @@
v-if="canCancel"
variant="danger"
class="!w-auto flex-1 px-3"
- :loading="actingId === booking.id"
- @click="emit('cancel', booking)"
+ :loading="acting"
+ @click="emit('cancel', bookings)"
>
لغو
@@ -58,37 +84,46 @@
diff --git a/src/components/booking/BookingOrderSummary.vue b/src/components/booking/BookingOrderSummary.vue
new file mode 100644
index 0000000..63b186e
--- /dev/null
+++ b/src/components/booking/BookingOrderSummary.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
diff --git a/src/components/booking/BookingServiceClassStep.vue b/src/components/booking/BookingServiceClassStep.vue
new file mode 100644
index 0000000..180ff3a
--- /dev/null
+++ b/src/components/booking/BookingServiceClassStep.vue
@@ -0,0 +1,54 @@
+
+
+
ابتدا کلاس خودرو یا خدمت خود را انتخاب کنید.
+
+
+
{{ error }}
+
+
+ کلاس خدماتی تعریف نشده است. میتوانید با زدن «مرحله بعد» ادامه دهید.
+
+
+
+
+
+
+
+
+
diff --git a/src/components/booking/BookingStepper.vue b/src/components/booking/BookingStepper.vue
new file mode 100644
index 0000000..4a5c080
--- /dev/null
+++ b/src/components/booking/BookingStepper.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
diff --git a/src/components/booking/BookingSuccessView.vue b/src/components/booking/BookingSuccessView.vue
new file mode 100644
index 0000000..762ccfd
--- /dev/null
+++ b/src/components/booking/BookingSuccessView.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
رزرو شما با موفقیت ثبت شد!
+
+ جزئیات رزرو برای شما پیامک و در بخش رزروها ذخیره شد.
+
+
+
+
+
+
+
+
+
- خدمت انتخابی
+ - {{ serviceTitle }}
+
+
+
- تاریخ
+ - {{ dateLabel }}
+
+
+
- ساعت
+ - {{ timeLabel }}
+
+
+
- مدت زمان
+ - {{ durationLabel }}
+
+
+
- آدرس
+ - {{ address }}
+
+
+
- شماره رزرو
+ - #{{ bookingRef }}
+
+
+
+
+
+
+
+ مشاهده رزروها
+
+
+
+
+ افزودن به تقویم
+
+
+
+
+
+ بازگشت به صفحه اصلی
+
+
+
+
+
+
+
diff --git a/src/components/booking/DesktopBookingDateTime.vue b/src/components/booking/DesktopBookingDateTime.vue
new file mode 100644
index 0000000..0779f26
--- /dev/null
+++ b/src/components/booking/DesktopBookingDateTime.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
انتخاب تاریخ
+
+
+
+
+
+
+
انتخاب ساعت
+
+
+
+
{{ slotsError }}
+
+ برای این روز ساعت خالی وجود ندارد. روز دیگری انتخاب کنید.
+
+
+
+
+
+
+
+
+
+
مدت تقریبی انجام خدمات: {{ durationMinutes }} دقیقه
+
+
+
+
+
diff --git a/src/components/booking/DesktopBookingFlow.vue b/src/components/booking/DesktopBookingFlow.vue
new file mode 100644
index 0000000..c619033
--- /dev/null
+++ b/src/components/booking/DesktopBookingFlow.vue
@@ -0,0 +1,624 @@
+
+
+
+
رزرو خدمات
+
+
+ بازگشت به صفحه اصلی
+
+
+
+
+
+
+
+
+ {{ currentStepLabel }}
+
+
+
+
+
+
+ میتوانید یک یا چند خدمت انتخاب کنید. برای لغو انتخاب، دوباره روی همان خدمت بزنید.
+
+
+
{{ servicesError }}
+
+
+
+
+
+
+ {{ selectedServices.length }} خدمت انتخاب شده
+
+
+ جمع: {{ formatPriceToman(selectedPrice) }} تومان
+
+
+
+
+
+
+
+
+
+
+
+
+
اطلاعات تکمیلی
+
+
+ پرداخت در محل انجام میشود. نیازی به پرداخت آنلاین نیست.
+
+
+
+
+
بررسی نهایی
+
+
+
- کلاس
+ - {{ selectedClassTitle }}
+
+
+
- خدمات
+ -
+ {{ selectedServiceTitle }}
+
+
+
+
- تاریخ و ساعت
+ - {{ scheduledLabel }}
+
+
+
- مبلغ
+ -
+ {{ formatPriceToman(selectedPrice) }} تومان
+
+
+
+
- یادداشت
+ - {{ note }}
+
+
+
{{ bookingError }}
+
+
+
+
+ مرحله قبل
+
+
+ {{ nextButtonLabel }}
+
+
+
+
+
+
+
+
+ قبل
+
+
+ {{ nextButtonLabel }}
+
+
+
+
+
+
برای ادامه رزرو، ابتدا وارد حساب کاربری شوید.
+
ورود / ثبتنام
+
+
+
+
+
+
diff --git a/src/components/brand/AppWelcomeMessage.vue b/src/components/brand/AppWelcomeMessage.vue
index ad4cc3e..af77d49 100644
--- a/src/components/brand/AppWelcomeMessage.vue
+++ b/src/components/brand/AppWelcomeMessage.vue
@@ -11,13 +11,18 @@
:class="['hidden dark:block', imgClass]"
/>
- نوبت گیری سریع ، مدیرت آسان زمان
+ {{ brand.tagline }}
diff --git a/src/components/home/HomeBusinessCard.vue b/src/components/home/HomeBusinessCard.vue
new file mode 100644
index 0000000..8b3da26
--- /dev/null
+++ b/src/components/home/HomeBusinessCard.vue
@@ -0,0 +1,62 @@
+
+
+
+
![]()
+
+
+
+ {{ business.title }}
+
+
+ {{ business.description }}
+
+ —
+
+
+ {{ business.category.title }}
+
+
+
+
+ {{ business.address }}
+
+
+
+ مشاهده و رزرو
+
+
+
+
+
diff --git a/src/components/home/HomeBusinessList.vue b/src/components/home/HomeBusinessList.vue
new file mode 100644
index 0000000..a35f3bc
--- /dev/null
+++ b/src/components/home/HomeBusinessList.vue
@@ -0,0 +1,52 @@
+
+
+
+
+ {{ error }}
+
+
+ کسبوکاری یافت نشد.
+
+
+
+
+
+
diff --git a/src/components/home/HomeCustomerClubSection.vue b/src/components/home/HomeCustomerClubSection.vue
new file mode 100644
index 0000000..70969a7
--- /dev/null
+++ b/src/components/home/HomeCustomerClubSection.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
diff --git a/src/components/home/HomeDiscoveryContent.vue b/src/components/home/HomeDiscoveryContent.vue
new file mode 100644
index 0000000..a1e40cc
--- /dev/null
+++ b/src/components/home/HomeDiscoveryContent.vue
@@ -0,0 +1,113 @@
+
+
+
+
کشف خدمات
+
+ کسبوکارهای محلی نزدیک شما
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/home/HomeFeatureBar.vue b/src/components/home/HomeFeatureBar.vue
new file mode 100644
index 0000000..331c20d
--- /dev/null
+++ b/src/components/home/HomeFeatureBar.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/home/HomeHeroSection.vue b/src/components/home/HomeHeroSection.vue
new file mode 100644
index 0000000..3fe48a6
--- /dev/null
+++ b/src/components/home/HomeHeroSection.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+ {{ heroTitle }}
+ {{ heroSubtitle }}
+
+
+
+ {{ heroDescription }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
diff --git a/src/components/home/HomeSearchAndFilter.vue b/src/components/home/HomeSearchAndFilter.vue
new file mode 100644
index 0000000..018cd9a
--- /dev/null
+++ b/src/components/home/HomeSearchAndFilter.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
diff --git a/src/components/home/HomeSectionTitle.vue b/src/components/home/HomeSectionTitle.vue
new file mode 100644
index 0000000..927d9a5
--- /dev/null
+++ b/src/components/home/HomeSectionTitle.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+ {{ subtitle }}
+
+
+
+
+
diff --git a/src/components/home/HomeServiceCard.vue b/src/components/home/HomeServiceCard.vue
new file mode 100644
index 0000000..92d4564
--- /dev/null
+++ b/src/components/home/HomeServiceCard.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+ {{ description }}
+
+
+
+ مدت زمان: {{ durationLabel }}
+
+
+
+
+
diff --git a/src/components/home/HomeServicesSection.vue b/src/components/home/HomeServicesSection.vue
new file mode 100644
index 0000000..65634b1
--- /dev/null
+++ b/src/components/home/HomeServicesSection.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
diff --git a/src/components/landing/DesktopDiscoveryPage.vue b/src/components/landing/DesktopDiscoveryPage.vue
new file mode 100644
index 0000000..d07657e
--- /dev/null
+++ b/src/components/landing/DesktopDiscoveryPage.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/src/components/landing/DesktopLandingPage.vue b/src/components/landing/DesktopLandingPage.vue
new file mode 100644
index 0000000..6c2fd3e
--- /dev/null
+++ b/src/components/landing/DesktopLandingPage.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/profile/ProfileDropdown.vue b/src/components/profile/ProfileDropdown.vue
new file mode 100644
index 0000000..e3ad9e7
--- /dev/null
+++ b/src/components/profile/ProfileDropdown.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/profile/ProfileModal.vue b/src/components/profile/ProfileModal.vue
new file mode 100644
index 0000000..8e1cccc
--- /dev/null
+++ b/src/components/profile/ProfileModal.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/src/components/profile/ProfilePanel.vue b/src/components/profile/ProfilePanel.vue
new file mode 100644
index 0000000..a3491c3
--- /dev/null
+++ b/src/components/profile/ProfilePanel.vue
@@ -0,0 +1,158 @@
+
+
+
+ {{ message }}
+
+
+ {{ error }}
+
+
+
+
+
- موبایل
+ - {{ auth.user.mobile }}
+
+
+
- نقش
+ - {{ roleLabel }}
+
+
+
+
+ کسبوکار من
+
+ میتوانید درخواست ثبت کسبوکار دهید تا پس از تأیید مدیریت، پنل صاحب کسبوکار فعال شود.
+
+
+ ثبت کسبوکار من
+
+
+
+
+ مدیریت کسبوکار
+
+ ورود به پنل کسبوکار
+
+
+
+
+
+ {{ auth.user.hasPassword ? 'تغییر رمز عبور' : 'تنظیم رمز عبور' }}
+
+
+ پس از تنظیم رمز، ورود با رمز عبور یا کد یکبارمصرف هر دو ممکن است.
+
+
+
+
+
+ {{ auth.user.hasPassword ? 'بهروزرسانی رمز' : 'ذخیره رمز' }}
+
+
+
+
+ خروج از حساب
+
+ با خروج، برای رزرو مجدد باید دوباره وارد شوید.
+
+
+ خروج
+
+
+
+
+
+
diff --git a/src/components/shell/AppShell.vue b/src/components/shell/AppShell.vue
index 555785a..953a35a 100644
--- a/src/components/shell/AppShell.vue
+++ b/src/components/shell/AppShell.vue
@@ -1,7 +1,9 @@
-
-
+
diff --git a/src/components/shell/AuthHeaderAction.vue b/src/components/shell/AuthHeaderAction.vue
new file mode 100644
index 0000000..ade6f0f
--- /dev/null
+++ b/src/components/shell/AuthHeaderAction.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
diff --git a/src/components/shell/DesktopSiteHeader.vue b/src/components/shell/DesktopSiteHeader.vue
new file mode 100644
index 0000000..5d3aa79
--- /dev/null
+++ b/src/components/shell/DesktopSiteHeader.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/shell/FloatingChatButton.vue b/src/components/shell/FloatingChatButton.vue
new file mode 100644
index 0000000..7070ed6
--- /dev/null
+++ b/src/components/shell/FloatingChatButton.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/src/components/shell/SiteFooter.vue b/src/components/shell/SiteFooter.vue
new file mode 100644
index 0000000..474efc0
--- /dev/null
+++ b/src/components/shell/SiteFooter.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
diff --git a/src/components/ui/AppButton.vue b/src/components/ui/AppButton.vue
index 57e68d0..5a32616 100644
--- a/src/components/ui/AppButton.vue
+++ b/src/components/ui/AppButton.vue
@@ -14,7 +14,7 @@ import { computed } from 'vue';
const props = withDefaults(
defineProps<{
- variant?: 'primary' | 'ghost' | 'danger';
+ variant?: 'primary' | 'ghost' | 'danger' | 'outline';
type?: 'button' | 'submit';
disabled?: boolean;
loading?: boolean;
@@ -28,6 +28,8 @@ const variantClass = computed(() => {
return 'bg-transparent text-muted hover:bg-surface-3 hover:text-text';
case 'danger':
return 'bg-red-500/15 text-red-600 dark:text-red-400 hover:bg-red-500/25';
+ case 'outline':
+ return 'border border-border bg-white text-text hover:bg-surface-2 dark:bg-surface dark:hover:bg-surface-2';
default:
return 'bg-brand-600 text-white hover:bg-brand-700';
}
diff --git a/src/components/ui/AppIcon.vue b/src/components/ui/AppIcon.vue
index 7750e68..f13899a 100644
--- a/src/components/ui/AppIcon.vue
+++ b/src/components/ui/AppIcon.vue
@@ -19,31 +19,46 @@ export default defineComponent({
import type { Component } from 'vue';
import { computed } from 'vue';
import {
+ Armchair,
ArrowRight,
+ BadgePercent,
Calendar,
Camera,
+ Car,
ChevronDown,
ChevronUp,
CircleCheck,
Clock,
ClipboardList,
+ Cog,
+ Droplet,
Eye,
EyeOff,
FolderOpen,
+ Headphones,
Home,
LayoutDashboard,
MapPin,
+ MessageCircle,
Moon,
- Plus,
+ Phone,
Pencil,
+ Plus,
Search,
+ Send,
+ Settings,
+ Shield,
+ Sparkles,
+ SprayCan,
Store,
Sun,
Trash2,
User,
UserCog,
Users,
+ Wind,
X,
+ Zap,
} from '@lucide/vue';
import type { AppIconName } from '@/types/icons';
@@ -73,12 +88,27 @@ const iconMap: Record
= {
'user-cog': UserCog,
eye: Eye,
'eye-off': EyeOff,
+ shield: Shield,
+ settings: Settings,
+ droplet: Droplet,
+ headphones: Headphones,
+ 'badge-percent': BadgePercent,
+ 'message-circle': MessageCircle,
+ phone: Phone,
+ send: Send,
+ sparkles: Sparkles,
+ armchair: Armchair,
+ wind: Wind,
+ car: Car,
+ cog: Cog,
+ spray: SprayCan,
+ zap: Zap,
};
const props = withDefaults(
defineProps<{
name: AppIconName;
- size?: 'sm' | 'md' | 'lg';
+ size?: 'sm' | 'md' | 'lg' | 'xl';
strokeWidth?: number;
}>(),
{
@@ -88,7 +118,12 @@ const props = withDefaults(
);
const sizeClass = computed(() => {
- const map = { sm: 'h-4 w-4', md: 'h-5 w-5', lg: 'h-6 w-6' } as const;
+ const map = {
+ sm: 'h-4 w-4',
+ md: 'h-5 w-5',
+ lg: 'h-6 w-6',
+ xl: 'h-8 w-8',
+ } as const;
return map[props.size];
});
diff --git a/src/components/ui/AppModal.vue b/src/components/ui/AppModal.vue
index 4b9717a..e8b8d1a 100644
--- a/src/components/ui/AppModal.vue
+++ b/src/components/ui/AppModal.vue
@@ -30,13 +30,20 @@
diff --git a/src/components/ui/AppSelect.vue b/src/components/ui/AppSelect.vue
new file mode 100644
index 0000000..968e088
--- /dev/null
+++ b/src/components/ui/AppSelect.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/ui/AppTextarea.vue b/src/components/ui/AppTextarea.vue
new file mode 100644
index 0000000..075fdfa
--- /dev/null
+++ b/src/components/ui/AppTextarea.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/src/composables/useAppNav.ts b/src/composables/useAppNav.ts
new file mode 100644
index 0000000..6d7bde9
--- /dev/null
+++ b/src/composables/useAppNav.ts
@@ -0,0 +1,179 @@
+import { computed } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import type { AppIconName } from '@/types/icons';
+import { useAuthStore } from '@/stores/auth';
+import { useProfileModalStore } from '@/stores/profile-modal';
+import { useTenantStore } from '@/stores/tenant';
+import { loginRouteWithRedirect } from '@/utils/auth-redirect';
+
+export type AppNavItem = {
+ key: string;
+ name: string;
+ label: string;
+ icon: AppIconName;
+ to?: string;
+ hash?: string;
+ onClick?: () => void;
+};
+
+export function useAppNav() {
+ const auth = useAuthStore();
+ const tenant = useTenantStore();
+ const profileModal = useProfileModalStore();
+ const route = useRoute();
+ const router = useRouter();
+
+ function requireAuth(path: string) {
+ if (auth.isLoggedIn) {
+ void router.push(path);
+ return;
+ }
+ void router.push(loginRouteWithRedirect(path));
+ }
+
+ const navItems = computed((): AppNavItem[] => {
+ const items: AppNavItem[] = [
+ {
+ key: 'home',
+ to: '/app',
+ name: 'app-home',
+ label: 'خانه',
+ icon: 'home',
+ },
+ {
+ key: 'businesses',
+ to: '/app',
+ hash: '#businesses',
+ name: 'app-home',
+ label: 'کسبوکارها',
+ icon: 'store',
+ },
+ ];
+
+ if (auth.isLoggedIn) {
+ items.push({
+ key: 'bookings',
+ to: '/app/bookings',
+ name: 'app-bookings',
+ label: 'رزروها',
+ icon: 'calendar',
+ });
+
+ if (auth.isBusinessOwner) {
+ items.push({
+ key: 'owner',
+ to: '/app/owner',
+ name: 'app-owner',
+ label: 'مدیریت کسبوکار',
+ icon: 'clipboard-list',
+ });
+ }
+
+ items.push({
+ key: 'profile',
+ name: 'app-profile',
+ label: 'پروفایل',
+ icon: 'user',
+ });
+ } else {
+ items.push({
+ key: 'register',
+ name: 'app-register-business',
+ label: 'ثبت کسبوکار',
+ icon: 'plus',
+ onClick: () => requireAuth('/app/register-business'),
+ });
+ }
+
+ items.push({
+ key: 'about',
+ to: '/app',
+ hash: '#about',
+ name: 'app-home',
+ label: 'درباره ما',
+ icon: 'users',
+ });
+
+ if (tenant.isSingleBusiness) {
+ return items.filter((item) => item.key !== 'businesses');
+ }
+
+ return items;
+ });
+
+ const mobileNavItems = computed((): AppNavItem[] => {
+ const items = navItems.value.filter((item) => item.key !== 'about');
+
+ if (!auth.isLoggedIn) {
+ items.push({
+ key: 'login',
+ name: 'login',
+ label: 'ورود',
+ icon: 'user',
+ onClick: () => void router.push(loginRouteWithRedirect(route.fullPath)),
+ });
+ }
+
+ return items;
+ });
+
+ function isNavActive(item: AppNavItem): boolean {
+ if (item.key === 'profile') {
+ return profileModal.isOpen;
+ }
+ if (item.hash) {
+ return route.name === item.name && route.hash === item.hash;
+ }
+ if (item.name === 'app-home' && !item.hash) {
+ return route.name === 'app-home' && !route.hash;
+ }
+ return route.name === item.name;
+ }
+
+ function navLinkClass(item: AppNavItem, variant: 'mobile' | 'desktop' = 'mobile') {
+ const active = isNavActive(item);
+
+ if (variant === 'desktop') {
+ return active
+ ? 'text-brand-600 border-brand-600'
+ : 'text-slate-600 border-transparent hover:text-brand-600 dark:text-slate-300';
+ }
+
+ return active
+ ? 'bg-brand-500/15 text-brand-700 dark:text-brand-500'
+ : 'text-muted hover:bg-surface-3 hover:text-text';
+ }
+
+ function goToNavItem(item: AppNavItem) {
+ if (item.onClick) {
+ item.onClick();
+ return;
+ }
+ if (item.to && item.hash) {
+ void router.push({ path: item.to, hash: item.hash.replace('#', '') });
+ return;
+ }
+ if (item.to) {
+ void router.push(item.to);
+ }
+ }
+
+ const mobileNavGridClass = computed(() => {
+ const count = mobileNavItems.value.length;
+ if (count <= 3) return 'grid-cols-3';
+ if (count === 4) return 'grid-cols-4';
+ return 'grid-cols-5';
+ });
+
+ return {
+ auth,
+ route,
+ router,
+ navItems,
+ mobileNavItems,
+ mobileNavGridClass,
+ navLinkClass,
+ goToNavItem,
+ requireAuth,
+ };
+}
diff --git a/src/composables/useLandingHashScroll.ts b/src/composables/useLandingHashScroll.ts
new file mode 100644
index 0000000..efbf1ba
--- /dev/null
+++ b/src/composables/useLandingHashScroll.ts
@@ -0,0 +1,27 @@
+import { nextTick, onMounted, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
+export function useLandingHashScroll() {
+ const route = useRoute();
+
+ function scrollToHash(hash: string) {
+ const el = document.getElementById(hash.replace('#', ''));
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+
+ watch(
+ () => route.hash,
+ async (hash) => {
+ if (!hash) return;
+ await nextTick();
+ scrollToHash(hash);
+ },
+ );
+
+ onMounted(async () => {
+ if (route.hash) {
+ await nextTick();
+ scrollToHash(route.hash);
+ }
+ });
+}
diff --git a/src/composables/useLandingNav.ts b/src/composables/useLandingNav.ts
new file mode 100644
index 0000000..da171f6
--- /dev/null
+++ b/src/composables/useLandingNav.ts
@@ -0,0 +1,59 @@
+import { computed } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useAuthStore } from '@/stores/auth';
+import { loginRouteWithRedirect } from '@/utils/auth-redirect';
+
+export type LandingNavItem = {
+ key: string;
+ label: string;
+ hash?: string;
+ path?: string;
+ requiresAuth?: boolean;
+};
+
+export function useLandingNav() {
+ const auth = useAuthStore();
+ const route = useRoute();
+ const router = useRouter();
+
+ const items: LandingNavItem[] = [
+ { key: 'home', label: 'خانه', path: '/app' },
+ { key: 'services', label: 'خدمات', hash: '#services' },
+ { key: 'bookings', label: 'رزروها', path: '/app/bookings', requiresAuth: true },
+ { key: 'club', label: 'باشگاه مشتریان', hash: '#club' },
+ { key: 'about', label: 'درباره ما', hash: '#about' },
+ { key: 'contact', label: 'تماس با ما', hash: '#contact' },
+ ];
+
+ const navItems = computed(() => items);
+
+ function isActive(item: LandingNavItem): boolean {
+ if (item.path === '/app' && !item.hash) {
+ return route.path === '/app' && !route.hash;
+ }
+ if (item.hash) {
+ return route.path === '/app' && route.hash === item.hash;
+ }
+ return route.path === item.path;
+ }
+
+ function goTo(item: LandingNavItem) {
+ if (item.requiresAuth && !auth.isLoggedIn) {
+ void router.push(loginRouteWithRedirect(item.path ?? '/app/bookings'));
+ return;
+ }
+ if (item.path && item.hash) {
+ void router.push({ path: item.path, hash: item.hash.replace('#', '') });
+ return;
+ }
+ if (item.path) {
+ void router.push(item.path);
+ return;
+ }
+ if (item.hash) {
+ void router.push({ path: '/app', hash: item.hash.replace('#', '') });
+ }
+ }
+
+ return { navItems, isActive, goTo, auth, route, router };
+}
diff --git a/src/constants/site-branding.ts b/src/constants/site-branding.ts
new file mode 100644
index 0000000..647c482
--- /dev/null
+++ b/src/constants/site-branding.ts
@@ -0,0 +1,33 @@
+export type SiteBrand = {
+ name: string;
+ shortName: string;
+ markLetter: string;
+ tagline: string;
+ heroLead: string;
+ aboutText: string;
+ copyrightName: string;
+};
+
+export const MONTIME_BRAND: SiteBrand = {
+ name: 'منتایم',
+ shortName: 'منتایم',
+ markLetter: 'م',
+ tagline: 'رزرو آنلاین خدمات محلی',
+ heroLead:
+ 'کسبوکارهای محلی را کشف کنید و در کوتاهترین زمان نوبت خود را رزرو کنید.',
+ aboutText:
+ 'منتایم پلتفرم رزرو آنلاین خدمات محلی است. بهترین کسبوکارهای اطراف شما را پیدا کنید و آنلاین نوبت بگیرید.',
+ copyrightName: 'منتایم',
+};
+
+export const NIVASH_BRAND: SiteBrand = {
+ name: 'کارواش نیوواش',
+ shortName: 'نیوواش',
+ markLetter: 'ن',
+ tagline: 'تمیزی که میدرخشد',
+ heroLead:
+ 'با تجهیزات پیشرفته و مواد شوینده با کیفیت، خودروی شما مثل روز اول بدرخشد.',
+ aboutText:
+ 'کارواش نیوواش با تجهیزات پیشرفته و مواد شوینده با کیفیت، بهترین تجربه شستشو و نگهداری خودرو را برای شما فراهم میکند.',
+ copyrightName: 'کارواش نیوواش',
+};
diff --git a/src/layouts/AuthLayout.vue b/src/layouts/AuthLayout.vue
index 1bcae4e..01ec649 100644
--- a/src/layouts/AuthLayout.vue
+++ b/src/layouts/AuthLayout.vue
@@ -2,9 +2,8 @@
-
-
-
{{ subtitle }}
+
{{ title }}
@@ -13,7 +12,7 @@
-
+
diff --git a/src/layouts/DesktopLoginLayout.vue b/src/layouts/DesktopLoginLayout.vue
new file mode 100644
index 0000000..5586a1d
--- /dev/null
+++ b/src/layouts/DesktopLoginLayout.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/layouts/DesktopSiteLayout.vue b/src/layouts/DesktopSiteLayout.vue
new file mode 100644
index 0000000..d6a7515
--- /dev/null
+++ b/src/layouts/DesktopSiteLayout.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/ResponsiveAppLayout.vue b/src/layouts/ResponsiveAppLayout.vue
new file mode 100644
index 0000000..3af58ad
--- /dev/null
+++ b/src/layouts/ResponsiveAppLayout.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/ResponsiveAuthLayout.vue b/src/layouts/ResponsiveAuthLayout.vue
new file mode 100644
index 0000000..51e0a68
--- /dev/null
+++ b/src/layouts/ResponsiveAuthLayout.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/WebAppLayout.vue b/src/layouts/WebAppLayout.vue
index f9dda70..55b7dfc 100644
--- a/src/layouts/WebAppLayout.vue
+++ b/src/layouts/WebAppLayout.vue
@@ -1,87 +1,72 @@
-
-
{{ auth.user.mobile }}
+
-
-
+
+
+
+