mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
FIX - complete CityMarkt sharing and browser flows
This commit is contained in:
@@ -2739,6 +2739,17 @@ const defaultLocales: LocaleTree = {
|
||||
cityMarktShare: 'Share to Local Pages',
|
||||
cityMarktShared: 'Shared to Local Pages.',
|
||||
cityMarktShareHint: 'One CityMarkt share per day',
|
||||
cityMarktAlreadyShared: 'Already published',
|
||||
cityMarktOpenSharedHint: 'Open the Local Pages post',
|
||||
cityMarktAppMissing: 'Local Pages is not installed',
|
||||
cityMarktInstallHint: 'Install Local Pages to share this listing',
|
||||
cityMarktAccountMissing: 'Local Pages profile required',
|
||||
cityMarktAccountHint: 'Create a Local Pages profile before sharing',
|
||||
cityMarktComposeTitle: 'Share CityMarkt listing',
|
||||
cityMarktComposeNavTitle: 'Share listing',
|
||||
cityMarktComposeHint:
|
||||
'Review the listing and publish it when you are ready.',
|
||||
cityMarktPhotosHint: 'The CityMarkt listing photos will be included.',
|
||||
authErrors: {
|
||||
invalid_email: 'Enter a valid 3–32 character iFruit address.',
|
||||
invalid_password: 'Use a password between 6 and 64 characters.',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Camera,
|
||||
CarFront,
|
||||
ChevronRight,
|
||||
CircleCheck,
|
||||
CirclePlus,
|
||||
Gift,
|
||||
Hammer,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
import {
|
||||
kBadge,
|
||||
kButton,
|
||||
kFab,
|
||||
kGlass,
|
||||
kIcon,
|
||||
kNavbar,
|
||||
@@ -51,6 +51,7 @@ import CityMarktSelect from '@/components/citymarkt/CityMarktSelect.vue'
|
||||
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
|
||||
import CityMarktOfferCard from '@/components/citymarkt/CityMarktOfferCard.vue'
|
||||
import { useAccountStore } from '@/stores/account'
|
||||
import { useAppStoreStore } from '@/stores/app-store'
|
||||
import { useEasyShareStore } from '@/stores/easyshare'
|
||||
import { useMarketplaceStore } from '@/stores/marketplace'
|
||||
import { useMessageMediaStore } from '@/stores/messageMedia'
|
||||
@@ -71,12 +72,6 @@ import type {
|
||||
} from '@/types/marketplace'
|
||||
import type { PhoneMedia } from '@/types/media'
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
type Tab = 'discover' | 'search' | 'sell' | 'inbox' | 'profile'
|
||||
type Screen = 'main' | 'detail' | 'sell' | 'chat' | 'report'
|
||||
type ChatTimelineItem =
|
||||
@@ -113,9 +108,14 @@ type MediaContext = {
|
||||
type ProfileMediaContext = { draft: MarketplaceProfileDraft }
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const detailActionColors = computed(() => ({
|
||||
bgIos: phone.isDarkMode ? 'bg-ios-dark-glass' : 'bg-ios-light-glass',
|
||||
shadowIos: phone.isDarkMode ? 'shadow-ios-dark-glass' : 'shadow-ios-light-glass',
|
||||
}))
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const account = useAccountStore()
|
||||
const appStore = useAppStoreStore()
|
||||
const easyShare = useEasyShareStore()
|
||||
const marketplace = useMarketplaceStore()
|
||||
const messageMedia = useMessageMediaStore()
|
||||
@@ -141,7 +141,6 @@ const offerAmount = ref('')
|
||||
const offerPanelOpen = ref(false)
|
||||
const offerSubmitting = ref(false)
|
||||
const feedback = ref('')
|
||||
const pagesSharePendingId = ref<string | null>(null)
|
||||
const sellStep = ref(1)
|
||||
const submitting = ref(false)
|
||||
const selectedPhotoIds = ref<string[]>([])
|
||||
@@ -241,6 +240,11 @@ const tabs = [
|
||||
] as const
|
||||
|
||||
const isAuthenticated = computed(() => account.email !== '')
|
||||
const localPagesInstalled = computed(
|
||||
() =>
|
||||
appStore.isInstalled('local-pages') &&
|
||||
!appStore.homeLayout.hidden.includes('local-pages'),
|
||||
)
|
||||
const canSaveProfile = computed(() => {
|
||||
const nameLength = profileDraft.value.displayName.trim().length
|
||||
return nameLength >= 2 && nameLength <= 40 && profileDraft.value.bio.trim().length <= 160
|
||||
@@ -371,21 +375,65 @@ function setFeedback(key: string): void {
|
||||
|
||||
async function shareToLocalPages(listingId?: string): Promise<void> {
|
||||
const id = listingId ?? selectedListing.value?.id
|
||||
if (!id || pagesSharePendingId.value) return
|
||||
pagesSharePendingId.value = id
|
||||
const response = await pages.shareCityMarkt(id)
|
||||
pagesSharePendingId.value = null
|
||||
if (response.success && response.data?.id) {
|
||||
if (!id) return
|
||||
if (!localPagesInstalled.value) {
|
||||
setFeedback('Apps.localPages.cityMarktAppMissing')
|
||||
return
|
||||
}
|
||||
if (!pages.profile?.exists) {
|
||||
setFeedback('Apps.localPages.cityMarktAccountMissing')
|
||||
return
|
||||
}
|
||||
const existingPost = pages.ownItems.find(
|
||||
(item) => item.citymarkt_listing_id === id,
|
||||
)
|
||||
if (existingPost) {
|
||||
await router.push({
|
||||
path: '/apps/local-pages',
|
||||
query: {
|
||||
easyShareId: response.data.id,
|
||||
easyShareId: existingPost.id,
|
||||
easyShareKind: 'post',
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
setFeedback(`Apps.localPages.errors.${response.error ?? 'default'}`)
|
||||
await router.push({
|
||||
path: '/apps/local-pages',
|
||||
query: {
|
||||
cityMarktListingId: id,
|
||||
compose: '1',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function localPagesShareLabel(listingId: string): string {
|
||||
if (!localPagesInstalled.value)
|
||||
return phone.t('Apps.localPages.cityMarktAppMissing')
|
||||
if (!pages.profile?.exists)
|
||||
return phone.t('Apps.localPages.cityMarktAccountMissing')
|
||||
return phone.t(
|
||||
hasLocalPagesPost(listingId)
|
||||
? 'Apps.localPages.cityMarktAlreadyShared'
|
||||
: 'Apps.localPages.cityMarktShare',
|
||||
)
|
||||
}
|
||||
|
||||
function localPagesShareHint(listingId: string): string {
|
||||
if (!localPagesInstalled.value)
|
||||
return phone.t('Apps.localPages.cityMarktInstallHint')
|
||||
if (!pages.profile?.exists)
|
||||
return phone.t('Apps.localPages.cityMarktAccountHint')
|
||||
return phone.t(
|
||||
hasLocalPagesPost(listingId)
|
||||
? 'Apps.localPages.cityMarktOpenSharedHint'
|
||||
: 'Apps.localPages.cityMarktShareHint',
|
||||
)
|
||||
}
|
||||
|
||||
function hasLocalPagesPost(listingId: string): boolean {
|
||||
return pages.ownItems.some(
|
||||
(item) => item.citymarkt_listing_id === listingId,
|
||||
)
|
||||
}
|
||||
|
||||
function shareListing(): void {
|
||||
@@ -870,7 +918,10 @@ onMounted(async () => {
|
||||
screen.value = 'sell'
|
||||
}
|
||||
if (isAuthenticated.value) {
|
||||
await marketplace.loadProfile()
|
||||
await Promise.all([
|
||||
marketplace.loadProfile(),
|
||||
localPagesInstalled.value ? pages.loadProfile() : Promise.resolve(false),
|
||||
])
|
||||
if (!marketplace.profile?.exists) {
|
||||
if (!profileSelection) syncProfileDraft()
|
||||
profileEditing.value = true
|
||||
@@ -1196,9 +1247,11 @@ onMounted(async () => {
|
||||
v-if="profileMode === 'own' && (item.status === 'active' || item.status === 'reserved')"
|
||||
class="citymarkt-profile-listing__share"
|
||||
type="button"
|
||||
:disabled="pagesSharePendingId !== null"
|
||||
@click="shareToLocalPages(item.id)"
|
||||
><Share2 :size="15" />{{ phone.t('Apps.localPages.cityMarktShare') }}</button
|
||||
><CircleCheck
|
||||
v-if="hasLocalPagesPost(item.id)"
|
||||
:size="15"
|
||||
/><Share2 v-else :size="15" />{{ localPagesShareLabel(item.id) }}</button
|
||||
></k-glass>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1211,39 +1264,40 @@ onMounted(async () => {
|
||||
class="citymarkt__detail"
|
||||
>
|
||||
<div class="citymarkt__glass-actions">
|
||||
<k-fab
|
||||
<k-glass
|
||||
component="button"
|
||||
type="button"
|
||||
:colors="glassActionColors"
|
||||
class="citymarkt-detail-action"
|
||||
:colors="detailActionColors"
|
||||
:aria-label="phone.t('Common.back')"
|
||||
@click="screen = 'main'"
|
||||
><template #icon><ArrowLeft :size="19" /></template
|
||||
></k-fab>
|
||||
><ArrowLeft :size="19" /></k-glass
|
||||
>
|
||||
<div>
|
||||
<k-fab
|
||||
<k-glass
|
||||
v-if="!selectedListing.is_owner"
|
||||
component="button"
|
||||
type="button"
|
||||
:colors="glassActionColors"
|
||||
class="citymarkt-detail-action"
|
||||
:colors="detailActionColors"
|
||||
:class="{ active: selectedListing.is_favorite }"
|
||||
:aria-label="phone.t(selectedListing.is_favorite ? 'Apps.citymarkt.removeFavorite' : 'Apps.citymarkt.addFavorite')"
|
||||
@click="toggleListingFavorite(selectedListing)"
|
||||
><template #icon
|
||||
><Heart
|
||||
:size="19"
|
||||
:fill="
|
||||
selectedListing.is_favorite ? 'currentColor' : 'none'
|
||||
" /></template
|
||||
></k-fab>
|
||||
<k-fab
|
||||
><Heart
|
||||
:size="19"
|
||||
:fill="selectedListing.is_favorite ? 'currentColor' : 'none'"
|
||||
/></k-glass
|
||||
>
|
||||
<k-glass
|
||||
v-if="!selectedListing.is_owner"
|
||||
component="button"
|
||||
type="button"
|
||||
:colors="glassActionColors"
|
||||
class="citymarkt-detail-action"
|
||||
:colors="detailActionColors"
|
||||
:aria-label="phone.t('Apps.citymarkt.reportListing')"
|
||||
@click="screen = 'report'"
|
||||
><template #icon><MoreHorizontal :size="20" /></template
|
||||
></k-fab>
|
||||
><MoreHorizontal :size="20" /></k-glass
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CityMarktGallery
|
||||
@@ -1306,14 +1360,14 @@ onMounted(async () => {
|
||||
"
|
||||
class="citymarkt__pages-share"
|
||||
type="button"
|
||||
:disabled="pagesSharePendingId !== null"
|
||||
@click="shareToLocalPages()"
|
||||
>
|
||||
<Share2 :size="17" /><span
|
||||
><strong>{{ phone.t('Apps.localPages.cityMarktShare') }}</strong
|
||||
><small>{{
|
||||
phone.t('Apps.localPages.cityMarktShareHint')
|
||||
}}</small></span
|
||||
<CircleCheck
|
||||
v-if="hasLocalPagesPost(selectedListing.id)"
|
||||
:size="17"
|
||||
/><Share2 v-else :size="17" /><span
|
||||
><strong>{{ localPagesShareLabel(selectedListing.id) }}</strong
|
||||
><small>{{ localPagesShareHint(selectedListing.id) }}</small></span
|
||||
>
|
||||
</button>
|
||||
<div class="citymarkt__owner-actions">
|
||||
@@ -1922,10 +1976,17 @@ onMounted(async () => {
|
||||
gap: 6px;
|
||||
}
|
||||
.citymarkt-detail-action {
|
||||
box-shadow:
|
||||
inset 0 0 0 0.5px #ffffff26,
|
||||
inset 0 1px 0 #ffffff1a,
|
||||
0 6px 16px #0007 !important;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.citymarkt-detail-action.active {
|
||||
color: var(--yellow);
|
||||
}
|
||||
.citymarkt__glass-list > .k-glass {
|
||||
width: 100%;
|
||||
@@ -1975,8 +2036,10 @@ onMounted(async () => {
|
||||
color: inherit;
|
||||
}
|
||||
.citymarkt-listing-card {
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
border-radius: 16px;
|
||||
display: grid !important;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
filter 0.18s ease,
|
||||
@@ -1987,6 +2050,7 @@ onMounted(async () => {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.citymarkt-listing-card > .citymarkt-listing-card__open {
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
@@ -1997,25 +2061,35 @@ onMounted(async () => {
|
||||
text-align: left;
|
||||
}
|
||||
.citymarkt-listing-card__favorite {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
grid-area: 1 / 1;
|
||||
align-self: start;
|
||||
justify-self: end;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 7px;
|
||||
padding: 0;
|
||||
border: 1px solid #ffffff12;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
display: grid !important;
|
||||
place-items: center;
|
||||
background: #161714d9;
|
||||
color: #d0d1cb;
|
||||
color: #f8f8f4;
|
||||
box-shadow: 0 3px 10px #0005;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
transition:
|
||||
color 0.16s ease,
|
||||
background 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
}
|
||||
.citymarkt-listing-card__favorite > svg {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
.citymarkt-listing-card__favorite:hover {
|
||||
background: #242520e6;
|
||||
color: #f5f5ef;
|
||||
@@ -2081,6 +2155,10 @@ onMounted(async () => {
|
||||
width: 116px;
|
||||
height: 116px;
|
||||
}
|
||||
.citymarkt__grid--wide .citymarkt-listing-card__favorite {
|
||||
justify-self: start;
|
||||
margin-left: 79px;
|
||||
}
|
||||
.citymarkt__grid--wide .citymarkt-listing-card__body {
|
||||
min-height: 116px;
|
||||
padding: 12px;
|
||||
|
||||
@@ -40,8 +40,10 @@ import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
|
||||
import { useAccountStore } from '@/stores/account'
|
||||
import { useMessageMediaStore } from '@/stores/messageMedia'
|
||||
import { useEasyShareStore } from '@/stores/easyshare'
|
||||
import { useMarketplaceStore } from '@/stores/marketplace'
|
||||
import { usePagesStore } from '@/stores/pages'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import type { MarketplaceListing } from '@/types/marketplace'
|
||||
import type { PagesCategory, PagesPost, PagesProfileDraft } from '@/types/pages'
|
||||
import type { PhoneMedia } from '@/types/media'
|
||||
import {
|
||||
@@ -68,6 +70,7 @@ const phone = usePhoneStore()
|
||||
const account = useAccountStore()
|
||||
const messageMedia = useMessageMediaStore()
|
||||
const easyShare = useEasyShareStore()
|
||||
const marketplace = useMarketplaceStore()
|
||||
const pages = usePagesStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -91,6 +94,8 @@ const profilePending = ref(false)
|
||||
const profileDraft = ref<PagesProfileDraft>({ avatarMediaId: 0, bio: '', handle: '' })
|
||||
const selectedProfilePhoto = ref<PhoneMedia | null>(null)
|
||||
const pickedPhotos = ref<SelectedPhoto[]>([])
|
||||
const cityMarktListing = ref<MarketplaceListing | null>(null)
|
||||
const cityMarktListingId = ref<string | null>(null)
|
||||
const draft = ref<ComposeDraft>({
|
||||
body: '',
|
||||
category: 'recommendation' as Exclude<PagesCategory, 'citymarkt'>,
|
||||
@@ -274,7 +279,14 @@ function syncProfileDraft(): void {
|
||||
|
||||
function ensureBrowserProfile(): void {
|
||||
const onboardingScenario = new URLSearchParams(window.location.search).get('testScenario')
|
||||
if (!import.meta.env.DEV || onboardingScenario === 'local-pages-onboarding' || pages.profile) return
|
||||
if (
|
||||
!import.meta.env.DEV ||
|
||||
['local-pages-onboarding', 'citymarkt-local-pages-account-missing'].includes(
|
||||
onboardingScenario ?? '',
|
||||
) ||
|
||||
pages.profile
|
||||
)
|
||||
return
|
||||
const handle = account.email.split('@')[0].toLowerCase()
|
||||
pages.profile = {
|
||||
avatar_media_id: null,
|
||||
@@ -387,24 +399,45 @@ async function publish(): Promise<void> {
|
||||
showFeedback('Apps.localPages.errors.invalid_post')
|
||||
return
|
||||
}
|
||||
const response = await pages.create({
|
||||
body: draft.value.body.trim(),
|
||||
category: draft.value.category,
|
||||
district: draft.value.district,
|
||||
images: draft.value.images.map((id) => ({ id })),
|
||||
title: draft.value.title.trim(),
|
||||
})
|
||||
const response = cityMarktListingId.value
|
||||
? await pages.shareCityMarkt(cityMarktListingId.value)
|
||||
: await pages.create({
|
||||
body: draft.value.body.trim(),
|
||||
category: draft.value.category,
|
||||
district: draft.value.district,
|
||||
images: draft.value.images.map((id) => ({ id })),
|
||||
title: draft.value.title.trim(),
|
||||
})
|
||||
if (!response.success) {
|
||||
showFeedback(`Apps.localPages.errors.${response.error ?? 'default'}`)
|
||||
return
|
||||
}
|
||||
draft.value = { body: '', category: 'recommendation', district: 'los_santos', images: [], title: '' }
|
||||
pickedPhotos.value = []
|
||||
cityMarktListing.value = null
|
||||
cityMarktListingId.value = null
|
||||
tab.value = 'feed'
|
||||
screen.value = 'main'
|
||||
await router.replace('/apps/local-pages')
|
||||
await Promise.all([pages.load(), pages.loadProfile()])
|
||||
showFeedback('Apps.localPages.published')
|
||||
}
|
||||
|
||||
function closeCompose(): void {
|
||||
draft.value = {
|
||||
body: '',
|
||||
category: 'recommendation',
|
||||
district: 'los_santos',
|
||||
images: [],
|
||||
title: '',
|
||||
}
|
||||
pickedPhotos.value = []
|
||||
cityMarktListing.value = null
|
||||
cityMarktListingId.value = null
|
||||
screen.value = 'main'
|
||||
void router.replace('/apps/local-pages')
|
||||
}
|
||||
|
||||
async function react(kind: 'like' | 'save'): Promise<void> {
|
||||
if (!selected.value || !isAuthenticated.value) {
|
||||
showFeedback('Apps.localPages.errors.not_authenticated')
|
||||
@@ -511,7 +544,34 @@ onMounted(async () => {
|
||||
screen.value = 'main'
|
||||
}
|
||||
onboardingReady.value = true
|
||||
if (route.query.compose === '1' && pages.profile?.exists) screen.value = 'compose'
|
||||
if (route.query.compose === '1' && pages.profile?.exists) {
|
||||
const listingId = String(route.query.cityMarktListingId ?? '')
|
||||
if (listingId) {
|
||||
const response = await marketplace.get(listingId)
|
||||
if (
|
||||
response.success &&
|
||||
response.data?.is_owner &&
|
||||
['active', 'reserved'].includes(response.data.status)
|
||||
) {
|
||||
cityMarktListing.value = response.data
|
||||
cityMarktListingId.value = response.data.id
|
||||
draft.value = {
|
||||
body: response.data.description,
|
||||
category: 'recommendation',
|
||||
district: response.data.district ?? 'los_santos',
|
||||
images: response.data.images.map((image) => image.media_id),
|
||||
title: response.data.title,
|
||||
}
|
||||
pickedPhotos.value = response.data.images.map((image) => ({
|
||||
background: image.gradient,
|
||||
id: image.media_id,
|
||||
}))
|
||||
} else {
|
||||
showFeedback('Apps.localPages.errors.citymarkt_not_found')
|
||||
}
|
||||
}
|
||||
if (!listingId || cityMarktListingId.value) screen.value = 'compose'
|
||||
}
|
||||
const easyShareId = String(route.query.easyShareId ?? '')
|
||||
if (pages.profile?.exists && easyShareId && route.query.easyShareKind === 'post') {
|
||||
const response = await pages.get(easyShareId)
|
||||
@@ -752,18 +812,22 @@ onMounted(async () => {
|
||||
</k-glass>
|
||||
</section>
|
||||
|
||||
<section v-else class="pages__compose">
|
||||
<section
|
||||
v-else
|
||||
class="pages__compose"
|
||||
:class="{ 'pages__compose--citymarkt': cityMarktListing }"
|
||||
>
|
||||
<k-navbar
|
||||
class="pages-create-navbar"
|
||||
center-title
|
||||
left-class="pages-create-action pages-create-action--close !w-11 !min-w-11 !max-w-11 !h-11 !p-0 !rounded-full"
|
||||
left-class="pages-create-action pages-create-action--close !min-w-[58px] !h-11 !p-0 !rounded-full"
|
||||
right-class="pages-create-action pages-create-action--publish !min-w-[58px] !h-11 !p-0 !rounded-full"
|
||||
:title="phone.t('Apps.localPages.shareWithCity')"
|
||||
:subtitle="phone.t('Apps.localPages.newPost')"
|
||||
:title="phone.t(cityMarktListing ? 'Apps.localPages.cityMarktComposeNavTitle' : 'Apps.localPages.shareWithCity')"
|
||||
:subtitle="phone.t(cityMarktListing ? 'Apps.localPages.categories.citymarkt' : 'Apps.localPages.newPost')"
|
||||
>
|
||||
<template #left>
|
||||
<button class="pages-create-close" type="button" :aria-label="phone.t('Common.close')" @click="screen = 'main'">
|
||||
<X :size="20" />
|
||||
<button class="pages-create-close" type="button" :aria-label="phone.t('Common.close')" @click="closeCompose">
|
||||
{{ phone.t('Common.close') }}
|
||||
</button>
|
||||
</template>
|
||||
<template #right>
|
||||
@@ -773,13 +837,20 @@ onMounted(async () => {
|
||||
</template>
|
||||
</k-navbar>
|
||||
<div class="pages__compose-scroll">
|
||||
<k-glass v-if="cityMarktListing" class="pages__citymarkt-source">
|
||||
<Store :size="19" />
|
||||
<span
|
||||
><strong>{{ phone.t('Apps.localPages.cityMarktComposeTitle') }}</strong
|
||||
><small>{{ phone.t('Apps.localPages.cityMarktComposeHint') }}</small></span
|
||||
>
|
||||
</k-glass>
|
||||
<label>{{ phone.t('Apps.localPages.title') }} <span :class="{ valid: draft.title.trim().length >= 5 }">{{ draft.title.trim().length }}/80 · {{ phone.t('Apps.citymarkt.minimumCharacters', { minimum: '5' }) }}</span><k-glass class="pages__field-glass"><input v-model="draft.title" maxlength="80" :placeholder="phone.t('Apps.localPages.titlePlaceholder')" /></k-glass></label>
|
||||
<label>{{ phone.t('Apps.localPages.body') }} <span :class="{ valid: draft.body.trim().length >= 10 }">{{ draft.body.trim().length }}/1500 · {{ phone.t('Apps.citymarkt.minimumCharacters', { minimum: '10' }) }}</span><k-glass class="pages__field-glass pages__field-glass--textarea"><textarea v-model="draft.body" maxlength="1500" :placeholder="phone.t('Apps.localPages.bodyPlaceholder')" /></k-glass></label>
|
||||
<div class="pages__form-row"><label>{{ phone.t('Apps.localPages.category') }}<CityMarktSelect :model-value="draft.category" :options="composeCategoryOptions" @change="(value) => draft.category = value as typeof draft.category" /></label><label>{{ phone.t('Apps.localPages.location') }}<CityMarktSelect :model-value="draft.district" :options="districtOptions" @change="(value) => draft.district = value" /></label></div>
|
||||
<section class="pages__photos">
|
||||
<ImagePlus :size="30" />
|
||||
<h2>{{ phone.t('Apps.citymarkt.addPhotos') }}</h2>
|
||||
<p>{{ phone.t('Apps.citymarkt.addPhotosBody') }}</p>
|
||||
<p>{{ phone.t(cityMarktListing ? 'Apps.localPages.cityMarktPhotosHint' : 'Apps.citymarkt.addPhotosBody') }}</p>
|
||||
<div class="pages__photo-actions">
|
||||
<k-glass><button type="button" @click="openMediaApp('photos')">
|
||||
<span><Images :size="20" /></span>
|
||||
@@ -981,9 +1052,7 @@ onMounted(async () => {
|
||||
height: 44px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.pages-create-action--close {
|
||||
width: 44px;
|
||||
}
|
||||
.pages-create-action--close,
|
||||
.pages-create-action--publish {
|
||||
min-width: 58px;
|
||||
}
|
||||
@@ -997,10 +1066,7 @@ onMounted(async () => {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.pages-create-close {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.pages-create-close,
|
||||
.pages-create-publish {
|
||||
min-width: 58px;
|
||||
padding: 0 13px;
|
||||
@@ -1021,6 +1087,43 @@ onMounted(async () => {
|
||||
height: auto;
|
||||
padding-bottom: 35px;
|
||||
}
|
||||
.pages__citymarkt-source {
|
||||
margin-bottom: 14px;
|
||||
padding: 11px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
border-radius: 13px;
|
||||
color: var(--yellow);
|
||||
}
|
||||
.pages__citymarkt-source span {
|
||||
min-width: 0;
|
||||
}
|
||||
.pages__citymarkt-source strong,
|
||||
.pages__citymarkt-source small {
|
||||
display: block;
|
||||
}
|
||||
.pages__citymarkt-source strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
.pages__citymarkt-source small {
|
||||
margin-top: 2px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.pages__compose--citymarkt .pages__compose-scroll > label input,
|
||||
.pages__compose--citymarkt .pages__compose-scroll > label textarea {
|
||||
pointer-events: none;
|
||||
}
|
||||
.pages__compose--citymarkt .pages__form-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.pages__compose--citymarkt .pages__form-row > label:first-child,
|
||||
.pages__compose--citymarkt .pages__photo-actions,
|
||||
.pages__compose--citymarkt .pages__selected-strip {
|
||||
display: none;
|
||||
}
|
||||
.pages__field-glass {
|
||||
min-height: 44px;
|
||||
margin-top: 5px;
|
||||
|
||||
@@ -1878,6 +1878,17 @@ const marketplaceListings = [
|
||||
],
|
||||
},
|
||||
]
|
||||
let marketplaceProfile = {
|
||||
avatar_media_id: 1,
|
||||
avatar_url: 'https://picsum.photos/seed/citymarkt-demo-avatar/240/240',
|
||||
bio: 'Fair prices, quick replies, and meetups anywhere in Los Santos.',
|
||||
display_name: 'Skyline Deals',
|
||||
email: 'demo@ifruit.com',
|
||||
exists: true,
|
||||
listing_count: marketplaceListings.filter(
|
||||
(listing) => listing.seller_account_id === 1,
|
||||
).length,
|
||||
}
|
||||
let linkedAccount = {
|
||||
devices: accountDevices,
|
||||
email: 'demo@ifruit.com',
|
||||
@@ -2454,6 +2465,32 @@ const pagesPosts = [
|
||||
images: [],
|
||||
},
|
||||
]
|
||||
const cityMarktSharedScenarioPost = {
|
||||
id: 'pages-citymarkt-owner-demo',
|
||||
account_id: 1,
|
||||
author_name: 'demo',
|
||||
source_type: 'citymarkt',
|
||||
citymarkt_listing_id: '81bc9d37-20e1-4d8a-82f8-f4b85f77cf04',
|
||||
title: 'Complete mechanic tool set',
|
||||
body: 'Complete mechanic tool set with trolley, sockets and diagnostic equipment. Everything is clean and ready for work.',
|
||||
category: 'citymarkt',
|
||||
district: 'south_los_santos',
|
||||
created_at: Date.parse('2026-08-06T13:10:00Z'),
|
||||
like_count: 4,
|
||||
images: [
|
||||
{
|
||||
media_id: 'capture-tools',
|
||||
gradient: 'linear-gradient(135deg, #ffc75f, #f96d80 48%, #4b4453)',
|
||||
sort_order: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
function pagesPostsForScenario(testScenario) {
|
||||
return testScenario === 'citymarkt-shared'
|
||||
? [cityMarktSharedScenarioPost, ...pagesPosts]
|
||||
: pagesPosts
|
||||
}
|
||||
const pagesReactions = [
|
||||
{ post_id: 'pages-1', account_id: 1, kind: 'like' },
|
||||
{ post_id: 'pages-3', account_id: 1, kind: 'save' },
|
||||
@@ -6965,7 +7002,30 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
data: {
|
||||
account: testScenario === 'feather-login' ? null : linkedAccount,
|
||||
device: {
|
||||
data: deviceData,
|
||||
data:
|
||||
testScenario.startsWith('citymarkt-')
|
||||
? {
|
||||
...deviceData,
|
||||
apps: {
|
||||
...deviceData.apps,
|
||||
payload: {
|
||||
...deviceData.apps.payload,
|
||||
homeLayout: {
|
||||
dock: [],
|
||||
grid:
|
||||
testScenario === 'citymarkt-local-pages-missing'
|
||||
? []
|
||||
: ['local-pages'],
|
||||
hidden:
|
||||
testScenario === 'citymarkt-local-pages-missing'
|
||||
? ['local-pages']
|
||||
: [],
|
||||
version: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: deviceData,
|
||||
imei: '356938035643809',
|
||||
name: 'Personal iFruit Phone',
|
||||
sim: {
|
||||
@@ -7475,7 +7535,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
}
|
||||
if (endpoint === 'pages:list') {
|
||||
const query = String(request.body.search ?? '').toLowerCase()
|
||||
let items = pagesPosts
|
||||
let items = pagesPostsForScenario(testScenario)
|
||||
if (request.body.category && request.body.category !== 'all')
|
||||
items = items.filter((item) => item.category === request.body.category)
|
||||
if (query)
|
||||
@@ -7498,7 +7558,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
return
|
||||
}
|
||||
if (endpoint === 'pages:get') {
|
||||
const post = pagesPosts.find((item) => item.id === request.body.id)
|
||||
const post = pagesPostsForScenario(testScenario).find(
|
||||
(item) => item.id === request.body.id,
|
||||
)
|
||||
response.json(
|
||||
post
|
||||
? { success: true, data: pageView(post) }
|
||||
@@ -7513,7 +7575,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
if (endpoint === 'pages:profile') {
|
||||
const email = linkedAccount?.email ?? pagesProfile.email
|
||||
const onboarding =
|
||||
testScenario === 'local-pages-onboarding' && !pagesOnboardingCompleted
|
||||
['local-pages-onboarding', 'citymarkt-local-pages-account-missing'].includes(
|
||||
testScenario,
|
||||
) && !pagesOnboardingCompleted
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -7523,7 +7587,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
email,
|
||||
exists: onboarding ? false : pagesProfile.exists,
|
||||
handle: onboarding ? email.split('@')[0] : pagesProfile.handle,
|
||||
post_count: pagesPosts.filter((item) => item.account_id === 1).length,
|
||||
post_count: pagesPostsForScenario(testScenario).filter(
|
||||
(item) => item.account_id === 1,
|
||||
).length,
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -7563,7 +7629,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
success: true,
|
||||
data: {
|
||||
hasMore: false,
|
||||
items: pagesPosts.filter((item) => item.account_id === 1).map(pageView),
|
||||
items: pagesPostsForScenario(testScenario)
|
||||
.filter((item) => item.account_id === 1)
|
||||
.map(pageView),
|
||||
offset: 0,
|
||||
},
|
||||
})
|
||||
@@ -7609,7 +7677,11 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: false, error: 'citymarkt_not_found' })
|
||||
return
|
||||
}
|
||||
if (pagesPosts.some((item) => item.citymarkt_listing_id === listing.id)) {
|
||||
if (
|
||||
pagesPostsForScenario(testScenario).some(
|
||||
(item) => item.citymarkt_listing_id === listing.id,
|
||||
)
|
||||
) {
|
||||
response.json({ success: false, error: 'citymarkt_already_shared' })
|
||||
return
|
||||
}
|
||||
@@ -7709,6 +7781,29 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
response.json({ success: false, error: 'not_authenticated' })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'marketplace:profile') {
|
||||
marketplaceProfile.listing_count = marketplaceListings.filter(
|
||||
(listing) => listing.seller_account_id === 1,
|
||||
).length
|
||||
response.json({ success: true, data: marketplaceProfile })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'marketplace:profile-save') {
|
||||
const avatarMediaId = Number(request.body.avatarMediaId)
|
||||
const avatar = mockMedia.find(
|
||||
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
|
||||
)
|
||||
marketplaceProfile = {
|
||||
...marketplaceProfile,
|
||||
avatar_media_id: avatarMediaId || null,
|
||||
avatar_url: avatar?.url ?? null,
|
||||
bio: String(request.body.bio ?? '').trim(),
|
||||
display_name: String(request.body.displayName ?? '').trim(),
|
||||
exists: true,
|
||||
}
|
||||
response.json({ success: true, data: marketplaceProfile })
|
||||
return
|
||||
}
|
||||
if (endpoint === 'marketplace:counts') {
|
||||
response.json({
|
||||
success: true,
|
||||
|
||||
@@ -1270,6 +1270,10 @@ Locales["en"] = {
|
||||
title = "Title", body = "Your story", category = "Category", titlePlaceholder = "What should people know?", bodyPlaceholder = "Add details, a recommendation or directions...",
|
||||
photos = "Photos", optional = "optional", camera = "Camera", gallery = "Gallery", photoLimit = "You can add up to six photos.",
|
||||
cityMarktShare = "Share to Local Pages", cityMarktShared = "Shared to Local Pages.", cityMarktShareHint = "One CityMarkt share per day",
|
||||
cityMarktAlreadyShared = "Already published", cityMarktOpenSharedHint = "Open the Local Pages post",
|
||||
cityMarktAppMissing = "Local Pages is not installed", cityMarktInstallHint = "Install Local Pages to share this listing",
|
||||
cityMarktAccountMissing = "Local Pages profile required", cityMarktAccountHint = "Create a Local Pages profile before sharing",
|
||||
cityMarktComposeTitle = "Share CityMarkt listing", cityMarktComposeNavTitle = "Share listing", cityMarktComposeHint = "Review the listing and publish it when you are ready.", cityMarktPhotosHint = "The CityMarkt listing photos will be included.",
|
||||
authErrors = { invalid_email = "Enter a valid 3-32 character iFruit address.", invalid_password = "Use a password between 6 and 64 characters.", invalid_credentials = "The iFruit address or password is incorrect.", email_taken = "This iFruit address is already registered.", password_mismatch = "The passwords do not match.", rate_limited = "Too many attempts. Try again shortly.", default = "The iFruit account request failed." },
|
||||
errors = { profile_required = "Create your Local Pages profile first.", invalid_profile = "Check your username and bio.", invalid_profile_image = "Choose a valid photo from this phone.", profile_handle_taken = "This username is already taken.", invalid_post = "Add a title and a little more detail.", invalid_images = "Choose valid photos from this phone.", invalid_request = "This action is not valid.", post_not_found = "This post is no longer available.", citymarkt_not_found = "This CityMarkt listing is unavailable.", citymarkt_daily_limit = "You already shared a CityMarkt listing today.", citymarkt_already_shared = "This listing was already shared.", not_authenticated = "Sign in to iFruit first.", rate_limited = "Too many requests. Try again shortly.", request_failed = "The post could not be saved.", default = "Local Pages is temporarily unavailable." },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user