From 1124f04d76ef06fe9325d01ecfe70f7d72fd4107 Mon Sep 17 00:00:00 2001 From: "smx.pusha" <139338836+smxpusha@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:49:38 +0200 Subject: [PATCH] ENH - refine CityMarkt glass interface --- .../citymarkt/CityMarktOfferCard.vue | 3 + .../components/citymarkt/CityMarktSelect.vue | 124 +- frontend/src/stores/phone.ts | 2 +- frontend/src/views/apps/CityMarktApp.vue | 1507 +++++++++++++++-- sky_phone/config/locales/en.lua | 2 +- 5 files changed, 1456 insertions(+), 182 deletions(-) diff --git a/frontend/src/components/citymarkt/CityMarktOfferCard.vue b/frontend/src/components/citymarkt/CityMarktOfferCard.vue index cdb88a1..a0be201 100644 --- a/frontend/src/components/citymarkt/CityMarktOfferCard.vue +++ b/frontend/src/components/citymarkt/CityMarktOfferCard.vue @@ -65,4 +65,7 @@ const formattedAmount = computed(() => diff --git a/frontend/src/components/citymarkt/CityMarktSelect.vue b/frontend/src/components/citymarkt/CityMarktSelect.vue index b662fca..c673714 100644 --- a/frontend/src/components/citymarkt/CityMarktSelect.vue +++ b/frontend/src/components/citymarkt/CityMarktSelect.vue @@ -1,5 +1,6 @@ diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 8ec1161..d528b88 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -496,7 +496,7 @@ const defaultLocales: LocaleTree = { reportReasons: { prohibited: 'Prohibited item', fraud: 'Suspected fraud', spam: 'Spam', offensive: 'Offensive content', other: 'Other' }, searchPlaceholder: 'What are you looking for?', allCategories: 'All categories', allDistricts: 'All districts', sortNewest: 'Newest first', sortPriceAsc: 'Lowest price', sortPriceDesc: 'Highest price', - freshOffers: 'Fresh offers', offers: 'offers', noListings: 'No offers found', + freshOffers: 'Fresh offers', offers: 'offers', compactView: 'Compact view', largeView: 'Large view', noListings: 'No offers found', noListingsBody: 'Try another search or category.', noDistrict: 'No district', free: 'Free', negotiablePrice: '${price} negotiable', money: '${price}', hoursAgo: '{count}h ago', daysAgo: '{count}d ago', photos: 'photos', activeListings: 'active listings', diff --git a/frontend/src/views/apps/CityMarktApp.vue b/frontend/src/views/apps/CityMarktApp.vue index 37fc2ae..21ff07c 100644 --- a/frontend/src/views/apps/CityMarktApp.vue +++ b/frontend/src/views/apps/CityMarktApp.vue @@ -2,7 +2,6 @@ import { ArrowLeft, BadgeDollarSign, - Bell, BriefcaseBusiness, Camera, CarFront, @@ -17,9 +16,11 @@ import { Images, Inbox, Laptop, + LayoutGrid, MapPin, MessageCircle, MoreHorizontal, + Rows3, Search, Send, Share2, @@ -29,7 +30,19 @@ import { Wrench, X, } from 'lucide-vue-next' -import { kButton } from 'konsta/vue' +import { + kBadge, + kButton, + kFab, + kGlass, + kIcon, + kNavbar, + kPage, + kSearchbar, + kTabbar, + kTabbarLink, + kToolbarPane, +} from 'konsta/vue' import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' @@ -54,11 +67,32 @@ import type { MarketplacePriceType, } from '@/types/marketplace' +const glassActionColors = { + bgIos: 'bg-ios-light-glass/75 dark:bg-ios-dark-glass/75', + activeBgIos: 'active:bg-white/90 dark:active:bg-white/20', + textIos: 'text-black/80 dark:text-white/80', +} +const yellowGlassActionColors = { + ...glassActionColors, + textIos: 'text-primary', +} + type Tab = 'discover' | 'search' | 'sell' | 'inbox' | 'profile' type Screen = 'main' | 'detail' | 'sell' | 'chat' | 'report' type ChatTimelineItem = - | { createdAt: DatabaseDateValue; key: string; kind: 'message'; value: MarketplaceMessage } - | { createdAt: DatabaseDateValue; isCounter: boolean; key: string; kind: 'offer'; value: MarketplaceOffer } + | { + createdAt: DatabaseDateValue + key: string + kind: 'message' + value: MarketplaceMessage + } + | { + createdAt: DatabaseDateValue + isCounter: boolean + key: string + kind: 'offer' + value: MarketplaceOffer + } type SelectedPhoto = { background: string; id: string } type SellDraft = { category: MarketplaceCategory @@ -89,6 +123,7 @@ const screen = ref('main') const selectedListing = ref(null) const selectedChat = ref(null) const search = ref('') +const listingLayout = ref<'compact' | 'wide'>('compact') const category = ref('all') const district = ref('all') const sort = ref('newest') @@ -142,15 +177,26 @@ const districts = [ 'paleto_bay', 'blaine_county', ] -const conditions: MarketplaceCondition[] = ['new', 'very_good', 'used', 'defective'] +const conditions: MarketplaceCondition[] = [ + 'new', + 'very_good', + 'used', + 'defective', +] const priceTypes: MarketplacePriceType[] = ['fixed', 'negotiable', 'free'] const categoryOptions = computed(() => [ { label: phone.t('Apps.citymarkt.allCategories'), value: 'all' }, - ...categories.map((item) => ({ label: label('categories', item.id), value: item.id })), + ...categories.map((item) => ({ + label: label('categories', item.id), + value: item.id, + })), ]) const districtOptions = computed(() => [ { label: phone.t('Apps.citymarkt.allDistricts'), value: 'all' }, - ...districts.map((item) => ({ label: label('districts', item), value: item })), + ...districts.map((item) => ({ + label: label('districts', item), + value: item, + })), ]) const sortOptions = computed(() => [ { label: phone.t('Apps.citymarkt.sortNewest'), value: 'newest' }, @@ -158,7 +204,10 @@ const sortOptions = computed(() => [ { label: phone.t('Apps.citymarkt.sortPriceDesc'), value: 'price_desc' }, ]) const sellCategoryOptions = computed(() => - categories.map((item) => ({ label: label('categories', item.id), value: item.id })), + categories.map((item) => ({ + label: label('categories', item.id), + value: item.id, + })), ) const conditionOptions = computed(() => conditions.map((item) => ({ label: label('conditions', item), value: item })), @@ -196,25 +245,35 @@ const draftImages = computed(() => selectedPhotoIds.value.flatMap((id, index) => { const photo = pickedPhotos.value.find((item) => item.id === id) return photo - ? [{ gradient: photo.background, media_id: photo.id, sort_order: index + 1 }] + ? [ + { + gradient: photo.background, + media_id: photo.id, + sort_order: index + 1, + }, + ] : [] }), ) const chatTimeline = computed(() => { if (!selectedChat.value) return [] - const messages: ChatTimelineItem[] = selectedChat.value.messages.map((item) => ({ - createdAt: item.created_at, - key: `message-${item.id}`, - kind: 'message', - value: item, - })) - const offers: ChatTimelineItem[] = (selectedChat.value.offers ?? []).map((item, index) => ({ - createdAt: item.created_at, - isCounter: index > 0, - key: `offer-${item.id}`, - kind: 'offer', - value: item, - })) + const messages: ChatTimelineItem[] = selectedChat.value.messages.map( + (item) => ({ + createdAt: item.created_at, + key: `message-${item.id}`, + kind: 'message', + value: item, + }), + ) + const offers: ChatTimelineItem[] = (selectedChat.value.offers ?? []).map( + (item, index) => ({ + createdAt: item.created_at, + isCounter: index > 0, + key: `offer-${item.id}`, + kind: 'offer', + value: item, + }), + ) return [...messages, ...offers].sort( (left, right) => parseDatabaseDate(left.createdAt).getTime() - @@ -222,13 +281,18 @@ const chatTimeline = computed(() => { ) }) const canMakeOffer = computed(() => { - if (!selectedChat.value || !['active', 'reserved'].includes(selectedChat.value.inquiry.status)) { + if ( + !selectedChat.value || + !['active', 'reserved'].includes(selectedChat.value.inquiry.status) + ) { return false } const inquiry = selectedChat.value.inquiry if (inquiry.offer_status === 'accepted') return false if (inquiry.offer_status === 'pending') { - return Number(inquiry.offer_proposer_account_id) !== selectedChat.value.accountId + return ( + Number(inquiry.offer_proposer_account_id) !== selectedChat.value.accountId + ) } return Number(inquiry.buyer_account_id) === selectedChat.value.accountId }) @@ -242,7 +306,8 @@ const canContinueSell = computed(() => { if (sellStep.value === 2) return ( draft.value.title.trim().length >= listingTextLimits.title.minimum && - draft.value.description.trim().length >= listingTextLimits.description.minimum + draft.value.description.trim().length >= + listingTextLimits.description.minimum ) if (sellStep.value === 3) return draft.value.priceType === 'free' || Number(draft.value.price) > 0 @@ -253,10 +318,15 @@ function label(group: string, value: string): string { return phone.t(`Apps.citymarkt.${group}.${value}`) } -function formatPrice(item: { price: number | string | null; price_type: MarketplacePriceType }): string { +function formatPrice(item: { + price: number | string | null + price_type: MarketplacePriceType +}): string { if (item.price_type === 'free') return phone.t('Apps.citymarkt.free') const value = Number(item.price ?? 0) - const formatted = new Intl.NumberFormat(phone.lang, { maximumFractionDigits: 0 }).format(value) + const formatted = new Intl.NumberFormat(phone.lang, { + maximumFractionDigits: 0, + }).format(value) return item.price_type === 'negotiable' ? phone.t('Apps.citymarkt.negotiablePrice', { price: formatted }) : phone.t('Apps.citymarkt.money', { price: formatted }) @@ -265,14 +335,18 @@ function formatPrice(item: { price: number | string | null; price_type: Marketpl function relativeDate(value: DatabaseDateValue): string { const timestamp = parseDatabaseDate(value).getTime() const hours = Math.max(1, Math.floor((Date.now() - timestamp) / 3_600_000)) - if (hours < 24) return phone.t('Apps.citymarkt.hoursAgo', { count: String(hours) }) - return phone.t('Apps.citymarkt.daysAgo', { count: String(Math.floor(hours / 24)) }) + if (hours < 24) + return phone.t('Apps.citymarkt.hoursAgo', { count: String(hours) }) + return phone.t('Apps.citymarkt.daysAgo', { + count: String(Math.floor(hours / 24)), + }) } function messageTime(value: DatabaseDateValue): string { - return new Intl.DateTimeFormat(phone.lang, { hour: '2-digit', minute: '2-digit' }).format( - parseDatabaseDate(value), - ) + return new Intl.DateTimeFormat(phone.lang, { + hour: '2-digit', + minute: '2-digit', + }).format(parseDatabaseDate(value)) } function setFeedback(key: string): void { @@ -285,9 +359,11 @@ function setFeedback(key: string): void { async function shareToLocalPages(): Promise { if (!selectedListing.value) return const response = await pages.shareCityMarkt(selectedListing.value.id) - setFeedback(response.success - ? 'Apps.localPages.cityMarktShared' - : `Apps.localPages.errors.${response.error ?? 'default'}`) + setFeedback( + response.success + ? 'Apps.localPages.cityMarktShared' + : `Apps.localPages.errors.${response.error ?? 'default'}`, + ) } async function loadFeed(): Promise { @@ -299,6 +375,20 @@ async function loadFeed(): Promise { }) } +function updateSearch(event: Event): void { + search.value = (event.target as HTMLInputElement).value +} + +function clearSearch(): void { + search.value = '' + void loadFeed() +} + +function toggleCategory(value: MarketplaceCategory): void { + category.value = category.value === value ? 'all' : value + void loadFeed() +} + async function selectCategory(value: string): Promise { category.value = value as MarketplaceCategory | 'all' await loadFeed() @@ -343,13 +433,19 @@ async function selectTab(next: Tab): Promise { return } if (next === 'discover' || next === 'search') await loadFeed() - if (next === 'inbox' && isAuthenticated.value) await marketplace.loadInquiries() + if (next === 'inbox' && isAuthenticated.value) + await marketplace.loadInquiries() if (next === 'profile' && isAuthenticated.value) { - await Promise.all([marketplace.loadOwn(), marketplace.load({ favorites: true })]) + await Promise.all([ + marketplace.loadOwn(), + marketplace.load({ favorites: true }), + ]) } } -async function openListing(item: Pick): Promise { +async function openListing( + item: Pick, +): Promise { const response = await marketplace.get(item.id) if (!response.success || !response.data) { setFeedback('Apps.citymarkt.errors.listing_not_found') @@ -394,7 +490,10 @@ function openMediaApp(app: 'camera' | 'photos'): void { sellStep: sellStep.value, } satisfies MediaContext, ) - void router.push({ path: `/apps/${app}`, query: { mediaAttachment: 'photo' } }) + void router.push({ + path: `/apps/${app}`, + query: { mediaAttachment: 'photo' }, + }) } function resetSell(): void { @@ -416,8 +515,13 @@ function resetSell(): void { function editListing(): void { if (!selectedListing.value) return - editing.value = { id: selectedListing.value.id, revision: selectedListing.value.revision } - selectedPhotoIds.value = selectedListing.value.images.map((image) => image.media_id) + editing.value = { + id: selectedListing.value.id, + revision: selectedListing.value.revision, + } + selectedPhotoIds.value = selectedListing.value.images.map( + (image) => image.media_id, + ) pickedPhotos.value = selectedListing.value.images.map((image) => ({ background: image.gradient, id: image.media_id, @@ -427,7 +531,10 @@ function editListing(): void { condition: selectedListing.value.item_condition, description: selectedListing.value.description, district: selectedListing.value.district ?? 'los_santos', - price: selectedListing.value.price === null ? '' : String(selectedListing.value.price), + price: + selectedListing.value.price === null + ? '' + : String(selectedListing.value.price), priceType: selectedListing.value.price_type, showPhone: Boolean(selectedListing.value.show_phone), title: selectedListing.value.title, @@ -448,7 +555,11 @@ async function publish(): Promise { if (submitting.value) return submitting.value = true const response = editing.value - ? await marketplace.update(editing.value.id, editing.value.revision, listingDraft()) + ? await marketplace.update( + editing.value.id, + editing.value.revision, + listingDraft(), + ) : await marketplace.create(listingDraft()) submitting.value = false if (!response.success) { @@ -502,7 +613,9 @@ function isOfferActionable(offer: MarketplaceOffer): boolean { function openOfferPanel(): void { if (!selectedChat.value || !canMakeOffer.value) return const suggestedAmount = Number( - selectedChat.value.inquiry.offer_amount ?? selectedChat.value.inquiry.price ?? 0, + selectedChat.value.inquiry.offer_amount ?? + selectedChat.value.inquiry.price ?? + 0, ) offerAmount.value = suggestedAmount > 0 ? String(suggestedAmount) : '' offerPanelOpen.value = true @@ -522,7 +635,10 @@ async function submitOffer(): Promise { return } offerSubmitting.value = true - const response = await marketplace.makeOffer(selectedChat.value.inquiry.id, amount) + const response = await marketplace.makeOffer( + selectedChat.value.inquiry.id, + amount, + ) offerSubmitting.value = false if (!response.success) { setFeedback(`Apps.citymarkt.errors.${response.error ?? 'default'}`) @@ -536,7 +652,10 @@ async function submitOffer(): Promise { async function respondOffer(action: 'accepted' | 'rejected'): Promise { if (!selectedChat.value || offerSubmitting.value) return offerSubmitting.value = true - const response = await marketplace.respondOffer(selectedChat.value.inquiry.id, action) + const response = await marketplace.respondOffer( + selectedChat.value.inquiry.id, + action, + ) offerSubmitting.value = false if (!response.success) { setFeedback(`Apps.citymarkt.errors.${response.error ?? 'default'}`) @@ -544,7 +663,9 @@ async function respondOffer(action: 'accepted' | 'rejected'): Promise { } await refreshChat() setFeedback( - action === 'accepted' ? 'Apps.citymarkt.offerAccepted' : 'Apps.citymarkt.offerDeclined', + action === 'accepted' + ? 'Apps.citymarkt.offerAccepted' + : 'Apps.citymarkt.offerDeclined', ) } @@ -552,15 +673,24 @@ async function sendChatMessage(): Promise { if (!selectedChat.value || !message.value.trim()) return const body = message.value message.value = '' - const response = await marketplace.sendMessage({ body, inquiryId: selectedChat.value.inquiry.id }) + const response = await marketplace.sendMessage({ + body, + inquiryId: selectedChat.value.inquiry.id, + }) if (response.success) { - const refreshed = await marketplace.getInquiry(selectedChat.value.inquiry.id) + const refreshed = await marketplace.getInquiry( + selectedChat.value.inquiry.id, + ) if (refreshed.data) selectedChat.value = refreshed.data } else setFeedback(`Apps.citymarkt.errors.${response.error ?? 'default'}`) } -async function setListingStatus(status: string, inquiryId?: string): Promise { - const listingId = selectedListing.value?.id ?? selectedChat.value?.inquiry.listing_id +async function setListingStatus( + status: string, + inquiryId?: string, +): Promise { + const listingId = + selectedListing.value?.id ?? selectedChat.value?.inquiry.listing_id if (!listingId) return if (await marketplace.setStatus(listingId, status, inquiryId)) { setFeedback('Apps.citymarkt.statusChanged') @@ -569,7 +699,9 @@ async function setListingStatus(status: string, inquiryId?: string): Promise { } for (const media of selection.media) { const id = String(media.id) - if (selectedPhotoIds.value.includes(id) || selectedPhotoIds.value.length >= 6) continue + if ( + selectedPhotoIds.value.includes(id) || + selectedPhotoIds.value.length >= 6 + ) + continue selectedPhotoIds.value.push(id) - pickedPhotos.value.push({ background: `url(${JSON.stringify(media.url)})`, id }) + pickedPhotos.value.push({ + background: `url(${JSON.stringify(media.url)})`, + id, + }) } } if (route.query.sell === '1') { @@ -628,37 +767,39 @@ onMounted(async () => {