diff --git a/.env.example b/.env.example index 21abf97..8ebd7ac 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ # Dev: /api (Vite proxy) | Production: see .env.production # Full local API: http://localhost:3000 VITE_API_URL=/api +# Proxy target when VITE_API_URL=/api (uncomment for local API during dev) +# VITE_DEV_API_PROXY=http://localhost:3000 # Production: https://api.montime.ir (uploads served from API) # VITE_ASSETS_URL=https://api.montime.ir diff --git a/src/api/businesses.ts b/src/api/businesses.ts index 3dacb0f..44fafb5 100644 --- a/src/api/businesses.ts +++ b/src/api/businesses.ts @@ -1,5 +1,6 @@ import { api } from './client'; -import type { Business, UpdateBusinessPayload } from './types'; +import { apiFormData } from './client'; +import type { Business, BusinessImage, UpdateBusinessPayload } from './types'; export function fetchBusinesses(params?: { categoryId?: string; @@ -30,3 +31,23 @@ export function updateBusiness(id: string, payload: UpdateBusinessPayload) { body: JSON.stringify(payload), }); } + +export function uploadBusinessImages(files: File[]) { + const form = new FormData(); + for (const file of files) { + form.append('files', file); + } + return apiFormData('/businesses/mine/images', form); +} + +export function deleteBusinessImage(imageId: string) { + return api<{ success: true }>(`/businesses/mine/images/${imageId}`, { + method: 'DELETE', + }); +} + +export function setBusinessCoverImage(imageId: string) { + return api(`/businesses/mine/images/${imageId}/cover`, { + method: 'PATCH', + }); +} diff --git a/src/api/client.ts b/src/api/client.ts index 7865a42..7866009 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -37,6 +37,11 @@ function resolveApiErrorMessage( } const raw = typeof body?.message === 'string' ? body.message : statusText; + + if (status === 403) { + return 'شما اجازه انجام این کار را ندارید.'; + } + const genericServer = status >= 500 && (!raw || @@ -92,3 +97,37 @@ export async function api(path: string, options: ApiOptions = {}): Promise return res.json() as Promise; } + +export async function apiFormData(path: string, form: FormData): Promise { + const token = getApiToken(); + const headers: Record = {}; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + let res: Response; + try { + res = await fetch(`${baseUrl}${path}`, { + method: 'POST', + headers, + body: form, + }); + } catch { + throw new ApiError( + 0, + 'ارتباط با سرور برقرار نشد. اتصال اینترنت را بررسی کنید یا چند لحظه بعد دوباره تلاش کنید.', + ); + } + + logApiServerTiming(path, res); + + if (!res.ok) { + const body = (await res.json().catch(() => null)) as + | { message?: string | string[] } + | null; + const msg = resolveApiErrorMessage(res.status, res.statusText, body); + throw new ApiError(res.status, msg); + } + + return res.json() as Promise; +} diff --git a/src/components/landing/MobileBusinessLanding.vue b/src/components/landing/MobileBusinessLanding.vue new file mode 100644 index 0000000..550b92a --- /dev/null +++ b/src/components/landing/MobileBusinessLanding.vue @@ -0,0 +1,180 @@ + + + diff --git a/src/components/owner/BusinessImageUpload.vue b/src/components/owner/BusinessImageUpload.vue new file mode 100644 index 0000000..e3f571e --- /dev/null +++ b/src/components/owner/BusinessImageUpload.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/profile/ProfileDropdown.vue b/src/components/profile/ProfileDropdown.vue index e3ad9e7..bff1951 100644 --- a/src/components/profile/ProfileDropdown.vue +++ b/src/components/profile/ProfileDropdown.vue @@ -28,7 +28,7 @@ leave-to-class="opacity-0 translate-y-1" >
@@ -40,7 +40,7 @@ diff --git a/src/components/ui/AppIcon.vue b/src/components/ui/AppIcon.vue index f13899a..415ddcb 100644 --- a/src/components/ui/AppIcon.vue +++ b/src/components/ui/AppIcon.vue @@ -19,6 +19,8 @@ export default defineComponent({ import type { Component } from 'vue'; import { computed } from 'vue'; import { + Gift, + Info, Armchair, ArrowRight, BadgePercent, @@ -103,6 +105,8 @@ const iconMap: Record = { cog: Cog, spray: SprayCan, zap: Zap, + gift: Gift, + info: Info, }; const props = withDefaults( diff --git a/src/components/ui/AppModal.vue b/src/components/ui/AppModal.vue index e8b8d1a..67943ea 100644 --- a/src/components/ui/AppModal.vue +++ b/src/components/ui/AppModal.vue @@ -8,6 +8,8 @@