Fix all TypeScript errors and remove Storybook
All checks were successful
Build Docker Image / build (push) Successful in 5m8s

- Remove all Storybook files and configuration
- Add type declarations for @vueuse/core, @formkit/core, vue3-apexcharts
- Fix TypeScript configuration (typeRoots, include paths)
- Fix Sentry config - move settings to plugin
- Fix nullable prop assignments with ?? operator
- Fix type narrowing issues with explicit type assertions
- Fix Card component linkable computed properties
- Update codegen with operationResultSuffix
- Fix GraphQL operation type definitions
This commit is contained in:
Ruslan Bakiev
2026-01-26 00:32:36 +07:00
parent b326d8cd76
commit 2b6cccdead
99 changed files with 419 additions and 1171 deletions

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './BankSearchRussia.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'BankSearchRussia',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -66,15 +66,24 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>()
interface BankSuggestion {
value: string
data: {
bic: string
correspondent_account?: string
address?: { value: string }
}
}
const query = ref('')
const suggestions = ref([])
const suggestions = ref<BankSuggestion[]>([])
const loading = ref(false)
const showDropdown = ref(false)
// Hide dropdown when clicking outside
onMounted(() => {
document.addEventListener('click', (e) => {
if (!e.target?.closest('.relative')) {
document.addEventListener('click', (e: MouseEvent) => {
if (!(e.target as HTMLElement)?.closest('.relative')) {
showDropdown.value = false
}
})

View File

@@ -44,6 +44,7 @@ const breadcrumbs = computed(() => {
let currentPath = '/clientarea'
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
if (!segment) continue
currentPath += `/${segment}`
const isLast = i === segments.length - 1

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './CalcResultContent.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'CalcResultContent',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -18,8 +18,8 @@
<!-- Results -->
<div v-else-if="productRouteOptions.length > 0" class="space-y-4">
<OfferResultCard
v-for="option in productRouteOptions"
:key="option.sourceUuid"
v-for="(option, index) in productRouteOptions"
:key="option.sourceUuid ?? index"
:location-name="getOfferData(option.sourceUuid)?.locationName"
:product-name="productName"
:price-per-unit="getOfferData(option.sourceUuid)?.pricePerUnit"
@@ -66,6 +66,21 @@
<script setup lang="ts">
import { GetNodeDocument, NearestOffersDocument, RouteToCoordinateDocument } from '~/composables/graphql/public/geo-generated'
import type { RouteStageItem } from '~/components/RouteStagesList.vue'
interface RouteStage {
fromUuid?: string | null
fromName?: string | null
toName?: string | null
distanceKm?: number | null
travelTimeSeconds?: number | null
transportType?: string | null
}
interface RoutePathType {
totalDistanceKm?: number | null
totalTimeSeconds?: number | null
stages?: (RouteStage | null)[]
}
import { GetOfferDocument, GetSupplierProfileByTeamDocument } from '~/composables/graphql/public/exchange-generated'
const route = useRoute()
@@ -140,8 +155,8 @@ const fetchOffersByHub = async () => {
RouteToCoordinateDocument,
{
offerUuid: offer.uuid,
lat: hub.latitude,
lon: hub.longitude
lat: hub.latitude!,
lon: hub.longitude!
},
'public',
'geo'
@@ -186,7 +201,7 @@ const productRouteOptions = computed(() => {
return options?.filter(Boolean) || []
})
const legacyRoutes = computed(() => {
const legacyRoutes = computed<RoutePathType[]>(() => {
return [] // Legacy routes removed
})

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './CompanyCard.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'CompanyCard',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './CompanySearchRussia.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'CompanySearchRussia',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -48,13 +48,24 @@ interface CompanyData {
address: string
}
interface CompanySuggestion {
value: string
unrestricted_value: string
data: {
inn: string
kpp?: string
ogrn?: string
address?: { value: string }
}
}
interface Props {
modelValue?: CompanyData
}
interface Emits {
(e: 'update:modelValue', value: CompanyData): void
(e: 'select', company: any): void
(e: 'select', company: CompanySuggestion): void
}
const props = withDefaults(defineProps<Props>(), {
@@ -71,14 +82,14 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>()
const query = ref('')
const suggestions = ref([])
const suggestions = ref<CompanySuggestion[]>([])
const loading = ref(false)
const showDropdown = ref(false)
// Hide dropdown when clicking outside
onMounted(() => {
document.addEventListener('click', (e) => {
if (!e.target?.closest('.relative')) {
document.addEventListener('click', (e: MouseEvent) => {
if (!(e.target as HTMLElement)?.closest('.relative')) {
showDropdown.value = false
}
})
@@ -118,10 +129,10 @@ const onInput = async () => {
}
}
const selectCompany = (company: any) => {
const selectCompany = (company: CompanySuggestion) => {
query.value = company.value
showDropdown.value = false
const companyData: CompanyData = {
companyName: company.value,
companyFullName: company.unrestricted_value,
@@ -130,7 +141,7 @@ const selectCompany = (company: any) => {
ogrn: company.data.ogrn || '',
address: company.data.address?.value || ''
}
emit('update:modelValue', companyData)
emit('select', company)
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './FooterPublic.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'FooterPublic',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './GanttTimeline.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'GanttTimeline',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './GoodsContent.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'GoodsContent',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -24,11 +24,11 @@
<Grid v-else :cols="1" :md="2" :lg="3" :gap="4">
<ProductCard
v-for="product in productsData"
:key="product.uuid"
:product="product"
v-for="(product, index) in productsData"
:key="product?.uuid ?? index"
:product="product!"
selectable
@select="selectProduct(product)"
@select="selectProduct(product!)"
/>
</Grid>
</Stack>

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './KYCFormRussia.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'KYCFormRussia',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -112,8 +112,8 @@ const profileData = ref<{
address?: string | null
director?: string | null
capital?: string | null
activities?: string[] | null
sources?: string[] | null
activities?: (string | null)[] | null
sources?: (string | null)[] | null
lastUpdated?: string | null
} | null>(null)

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './LangSwitcher.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'LangSwitcher',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './LocationsContent.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'LocationsContent',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -86,7 +86,7 @@ const calculateDistance = (lat: number, lng: number) => {
// Load logistics hubs
const { data: locationsDataRaw, pending, error, refresh } = await useServerQuery('locations', GetNodesDocument, {}, 'public', 'geo')
const locationsData = computed(() => {
return (locationsDataRaw.value || []).map((location: any) => ({
return (locationsDataRaw.value?.nodes || []).map((location: any) => ({
...location,
distance: location?.latitude && location?.longitude
? calculateDistance(location.latitude, location.longitude)

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './MapboxGlobe.client.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'MapboxGlobe.client',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -228,15 +228,16 @@ const onMapCreated = (map: MapboxMap) => {
// Click on cluster to zoom in
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] })
if (!features.length) return
const feature = features[0]
if (!feature) return
const clusterId = features[0].properties?.cluster_id
const clusterId = feature.properties?.cluster_id
const source = map.getSource('locations') as mapboxgl.GeoJSONSource
source.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return
const geometry = features[0].geometry as GeoJSON.Point
const geometry = feature.geometry as GeoJSON.Point
map.easeTo({
center: geometry.coordinates as [number, number],
zoom: zoom || 4
@@ -247,10 +248,11 @@ const onMapCreated = (map: MapboxMap) => {
// Click on individual point
map.on('click', 'unclustered-point', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['unclustered-point'] })
if (!features.length) return
const feature = features[0]
if (!feature) return
const featureProps = features[0].properties
const geometry = features[0].geometry as GeoJSON.Point
const featureProps = feature.properties
const geometry = feature.geometry as GeoJSON.Point
const location: Location = {
uuid: featureProps?.uuid,

View File

@@ -38,8 +38,8 @@
</div>
<Stack v-if="autoEdges.length > 0" gap="2">
<NuxtLink
v-for="edge in autoEdges"
:key="edge.toUuid"
v-for="(edge, index) in autoEdges"
:key="edge.toUuid ?? index"
:to="localePath(`/catalog/hubs/${edge.toUuid}`)"
class="flex flex-col gap-2 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
>
@@ -70,8 +70,8 @@
</div>
<Stack v-if="railEdges.length > 0" gap="2">
<NuxtLink
v-for="edge in railEdges"
:key="edge.toUuid"
v-for="(edge, index) in railEdges"
:key="edge.toUuid ?? index"
:to="localePath(`/catalog/hubs/${edge.toUuid}`)"
class="flex flex-col gap-2 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
>
@@ -292,8 +292,10 @@ const addHubSource = (map: MapboxMapType, id: string, hub: CurrentHub, color: st
})
map.on('click', `${id}-circle`, (e) => {
const coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const name = e.features![0].properties?.name
const feature = e.features?.[0]
if (!feature) return
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const name = feature.properties?.name
new Popup()
.setLngLat(coordinates)
@@ -390,8 +392,10 @@ const onMapCreated = (map: MapboxMapType) => {
})
const onNeighborsClick = (e: mapboxgl.MapLayerMouseEvent) => {
const coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const featureProps = e.features![0].properties
const feature = e.features?.[0]
if (!feature) return
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const featureProps = feature.properties
const name = featureProps?.name
const distanceKm = featureProps?.distanceKm

View File

@@ -37,8 +37,8 @@
<div class="order-1 lg:order-2">
<Stack gap="2">
<NuxtLink
v-for="edge in edges"
:key="edge.toUuid"
v-for="(edge, index) in edges"
:key="edge.toUuid ?? index"
:to="localePath(`/catalog/hubs/${edge.toUuid}`)"
class="flex flex-col gap-2 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
>
@@ -304,8 +304,10 @@ const onMapCreated = (map: MapboxMapType) => {
// Popups on click
map.on('click', 'current-hub-circle', (e) => {
const coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const name = e.features![0].properties?.name
const feature = e.features?.[0]
if (!feature) return
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const name = feature.properties?.name
new Popup()
.setLngLat(coordinates)
@@ -314,8 +316,10 @@ const onMapCreated = (map: MapboxMapType) => {
})
map.on('click', 'neighbors-circles', (e) => {
const coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const featureProps = e.features![0].properties
const feature = e.features?.[0]
if (!feature) return
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const featureProps = feature.properties
const name = featureProps?.name
const distanceKm = featureProps?.distanceKm

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './NovuNotificationBell.client.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'NovuNotificationBell.client',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './OrderCalendar.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'OrderCalendar',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './OrderMap.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'OrderMap',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './OrderTimeline.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'OrderTimeline',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -120,7 +120,7 @@ const fetchRouteGeometry = async (stage: RouteStage): Promise<[number, number][]
fromLon: stage.fromLon,
toLat: stage.toLat,
toLon: stage.toLon
}, 'public', 'geo')
}, 'public', 'geo') as Record<string, any>
const geometry = routeData?.[routeField]?.geometry
if (typeof geometry === 'string') {
@@ -363,13 +363,15 @@ const onMapCreated = (map: MapboxMapType) => {
// Click on marker
map.on('click', 'orders-markers-circles', (e) => {
const props = e.features?.[0]?.properties
const feature = e.features?.[0]
if (!feature) return
const props = feature.properties
const orderId = props?.orderId
if (orderId) {
emit('select-order', orderId)
}
const coordinates = (e.features?.[0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
new Popup()
.setLngLat(coordinates)
.setHTML(`<strong>${props?.name || 'Point'}</strong><br/>${props?.orderName || ''}`)

View File

@@ -90,6 +90,7 @@ const routeMarkers = computed(() => {
if (!stages.length) return
const first = stages[0]
const last = stages[stages.length - 1]
if (!first || !last) return
if (typeof first.fromLat === 'number' && typeof first.fromLon === 'number') {
markers.push({
@@ -183,7 +184,7 @@ const fetchStageGeometry = async (stage: RouteStage, routeIndex: number, stageIn
const RouteDocument = stage.transportType === 'auto' ? GetAutoRouteDocument : GetRailRouteDocument
const routeField = stage.transportType === 'auto' ? 'autoRoute' : 'railRoute'
const routeData = await execute(RouteDocument, { fromLat, fromLon, toLat, toLon }, 'public', 'geo')
const routeData = await execute(RouteDocument, { fromLat, fromLon, toLat, toLon }, 'public', 'geo') as Record<string, any>
const geometry = routeData?.[routeField]?.geometry
if (typeof geometry === 'string') {
@@ -341,8 +342,10 @@ const onMapCreated = (map: MapboxMapType) => {
})
map.on('click', 'request-markers-circles', (e) => {
const coordinates = (e.features?.[0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const featureProps = e.features?.[0].properties
const feature = e.features?.[0]
if (!feature) return
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const featureProps = feature.properties
const title = featureProps?.name || 'Точка'
const label = featureProps?.label || ''

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './RouteMap.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'RouteMap',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -227,8 +227,10 @@ const onMapCreated = (map: MapboxMapType) => {
})
map.on('click', 'route-points-layer', (e) => {
const coordinates = (e.features?.[0].geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const props = e.features?.[0].properties
const feature = e.features?.[0]
if (!feature) return
const coordinates = (feature.geometry as GeoJSON.Point).coordinates.slice() as [number, number]
const props = feature.properties
const name = props?.name || t('routeMap.points.service')
new Popup()

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './TeamCard.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'TeamCard',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -40,6 +40,19 @@
</template>
<script setup lang="ts">
interface TeamMember {
id: string
userId: string
role?: string | null
}
interface Team {
id?: string | null
name: string
createdAt?: string | null
members?: TeamMember[] | null
}
interface Props {
team: Team
}
@@ -49,7 +62,7 @@ const membersCount = computed(() => props.team?.members?.length || 1)
const displayMembers = computed(() => (props.team?.members || []).slice(0, 3))
const remainingMembers = computed(() => Math.max(0, membersCount.value - 3))
const formatDate = (dateString: string) => {
const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return ''
try {
return new Date(dateString).toLocaleDateString('ru-RU')

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './TeamCreateForm.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'TeamCreateForm',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './TimelineStages.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'TimelineStages',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './TripBadge.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'TripBadge',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './UserAvatar.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'UserAvatar',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -48,5 +48,5 @@ defineEmits<{
const localePath = useLocalePath()
const { t } = useI18n()
const linkable = computed(() => !props.selectable && props.address.uuid)
const linkable = computed(() => !props.selectable && !!props.address.uuid)
</script>

View File

@@ -17,8 +17,8 @@
<Text weight="semibold" class="mb-3">{{ country.name }}</Text>
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<HubCard
v-for="hub in country.hubs"
:key="hub.uuid"
v-for="(hub, index) in country.hubs"
:key="hub.uuid ?? index"
:hub="hub"
/>
</Grid>

View File

@@ -319,20 +319,22 @@ const initClientClusteringLayers = async (map: MapboxMapType) => {
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] })
if (!features.length) return
const clusterId = features[0].properties?.cluster_id
const feature = features[0]
if (!feature) return
const clusterId = feature.properties?.cluster_id
const source = map.getSource(sourceId.value) as mapboxgl.GeoJSONSource
source.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return
const geometry = features[0].geometry as GeoJSON.Point
const geometry = feature.geometry as GeoJSON.Point
map.easeTo({ center: geometry.coordinates as [number, number], zoom: zoom || 4 })
})
})
map.on('click', 'unclustered-point', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['unclustered-point'] })
if (!features.length) return
emit('select-item', features[0].properties?.uuid)
const feature = features[0]
if (!feature) return
emit('select-item', feature.properties?.uuid)
})
map.on('mouseenter', 'clusters', () => { map.getCanvas().style.cursor = 'pointer' })
@@ -406,8 +408,9 @@ const initClientClusteringLayers = async (map: MapboxMapType) => {
// Click handlers for related points
map.on('click', `${props.mapId}-related-circles`, (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-circles`] })
if (!features.length) return
const props_data = features[0].properties
const feature = features[0]
if (!feature) return
const props_data = feature.properties as Record<string, any> | undefined
emit('select-item', props_data?.uuid, props_data)
})
@@ -500,9 +503,10 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
// Click on cluster to zoom in
map.on('click', 'server-clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['server-clusters'] })
if (!features.length) return
const expansionZoom = features[0].properties?.expansionZoom
const geometry = features[0].geometry as GeoJSON.Point
const feature = features[0]
if (!feature) return
const expansionZoom = feature.properties?.expansionZoom
const geometry = feature.geometry as GeoJSON.Point
map.easeTo({
center: geometry.coordinates as [number, number],
zoom: expansionZoom || map.getZoom() + 2
@@ -512,8 +516,9 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
// Click on individual point - emit full properties
map.on('click', 'server-points', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['server-points'] })
if (!features.length) return
const props = features[0].properties || {}
const feature = features[0]
if (!feature) return
const props = feature.properties || {}
emit('select-item', props.id, props)
})
@@ -588,8 +593,9 @@ const initServerClusteringLayers = async (map: MapboxMapType) => {
// Click handlers for related points
map.on('click', `${props.mapId}-related-circles`, (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: [`${props.mapId}-related-circles`] })
if (!features.length) return
const props_data = features[0].properties
const feature = features[0]
if (!feature) return
const props_data = feature.properties as Record<string, any> | undefined
emit('select-item', props_data?.uuid, props_data)
})

View File

@@ -23,8 +23,8 @@
<!-- Hubs Tab -->
<template v-if="activeTab === 'hubs'">
<HubCard
v-for="hub in hubs"
:key="hub.uuid"
v-for="(hub, index) in hubs"
:key="hub.uuid ?? index"
:hub="hub"
selectable
:is-selected="selectedId === hub.uuid"
@@ -38,8 +38,8 @@
<!-- Offers Tab -->
<template v-if="activeTab === 'offers'">
<OfferCard
v-for="offer in offers"
:key="offer.uuid"
v-for="(offer, index) in offers"
:key="offer.uuid ?? index"
:offer="offer"
selectable
compact
@@ -54,8 +54,8 @@
<!-- Suppliers Tab -->
<template v-if="activeTab === 'suppliers'">
<SupplierCard
v-for="supplier in suppliers"
:key="supplier.uuid"
v-for="(supplier, index) in suppliers"
:key="supplier.uuid ?? index"
:supplier="supplier"
selectable
:is-selected="selectedId === supplier.uuid"

View File

@@ -15,7 +15,7 @@
<div v-if="filters && filters.length > 0" class="p-4 border-b border-base-300">
<CatalogFilters
:filters="filters"
:model-value="selectedFilter"
:model-value="selectedFilter ?? 'all'"
@update:model-value="$emit('update:selectedFilter', $event)"
/>
</div>

View File

@@ -14,8 +14,8 @@
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<OfferCard
v-for="offer in offers"
:key="offer.uuid"
v-for="(offer, index) in offers"
:key="offer.uuid ?? index"
:offer="offer"
/>
</Grid>

View File

@@ -14,8 +14,8 @@
<Grid :cols="1" :md="2" :lg="3" :gap="4">
<SupplierCard
v-for="supplier in suppliers"
:key="supplier.uuid"
v-for="(supplier, index) in suppliers"
:key="supplier.uuid ?? index"
:supplier="supplier"
/>
</Grid>

View File

@@ -66,7 +66,7 @@ defineEmits<{
const localePath = useLocalePath()
const { t } = useI18n()
const linkable = computed(() => !props.selectable && (props.linkTo || props.hub.uuid))
const linkable = computed(() => !props.selectable && !!(props.linkTo || props.hub.uuid))
const resolvedLink = computed(() => props.linkTo || localePath(`/catalog/hubs/${props.hub.uuid}`))
// ISO code to emoji flag

View File

@@ -76,7 +76,7 @@ const trend = computed(() => {
if (props.priceHistory.length < 2) return 0
const first = props.priceHistory[0]
const last = props.priceHistory[props.priceHistory.length - 1]
if (!first || first === 0) return 0
if (!first || first === 0 || !last) return 0
return Math.round(((last - first) / first) * 100)
})

View File

@@ -186,12 +186,18 @@ const emit = defineEmits<{
'close': []
'add-to-filter': []
'open-info': [type: InfoEntityType, uuid: string]
'select-product': [uuid: string]
'select-product': [uuid: string | null]
}>()
const { t } = useI18n()
const { entityColors } = useCatalogSearch()
// Safe accessors for optional arrays
const relatedProducts = computed(() => props.relatedProducts ?? [])
const relatedHubs = computed(() => props.relatedHubs ?? [])
const relatedSuppliers = computed(() => props.relatedSuppliers ?? [])
const relatedOffers = computed(() => props.relatedOffers ?? [])
// Current active tab
const currentTab = ref<string>('offers')
@@ -279,8 +285,9 @@ const availableTabs = computed(() => {
watch(
() => props.entityType,
() => {
if (availableTabs.value.length > 0) {
currentTab.value = availableTabs.value[0].id
const firstTab = availableTabs.value[0]
if (firstTab) {
currentTab.value = firstTab.id
}
},
{ immediate: true }

View File

@@ -73,7 +73,7 @@ defineEmits<{
const localePath = useLocalePath()
const { t } = useI18n()
const linkable = computed(() => !props.selectable && props.offer.uuid)
const linkable = computed(() => !props.selectable && !!props.offer.uuid)
const formattedDate = computed(() => {
if (!props.offer.validUntil) return ''

View File

@@ -23,7 +23,9 @@ const props = defineProps<{
const isUptrend = computed(() => {
if (props.data.length < 2) return true
return props.data[props.data.length - 1] >= props.data[0]
const last = props.data[props.data.length - 1]
const first = props.data[0]
return (last ?? 0) >= (first ?? 0)
})
const lineColor = computed(() => isUptrend.value ? '#22c55e' : '#ef4444')

View File

@@ -48,5 +48,5 @@ defineEmits<{
const localePath = useLocalePath()
const { t } = useI18n()
const linkable = computed(() => !props.selectable && props.product.uuid)
const linkable = computed(() => !props.selectable && !!props.product.uuid)
</script>

View File

@@ -29,9 +29,9 @@
<!-- Products -->
<template v-if="selectMode === 'product'">
<div
v-for="item in filteredItems"
:key="item.uuid"
@mouseenter="emit('hover', item.uuid)"
v-for="(item, index) in filteredItems"
:key="item.uuid ?? index"
@mouseenter="emit('hover', item.uuid ?? null)"
@mouseleave="emit('hover', null)"
>
<ProductCard
@@ -47,9 +47,9 @@
<!-- Hubs -->
<template v-else-if="selectMode === 'hub'">
<div
v-for="item in filteredItems"
:key="item.uuid"
@mouseenter="emit('hover', item.uuid)"
v-for="(item, index) in filteredItems"
:key="item.uuid ?? index"
@mouseenter="emit('hover', item.uuid ?? null)"
@mouseleave="emit('hover', null)"
>
<HubCard
@@ -64,9 +64,9 @@
<!-- Suppliers -->
<template v-else-if="selectMode === 'supplier'">
<div
v-for="item in filteredItems"
:key="item.uuid"
@mouseenter="emit('hover', item.uuid)"
v-for="(item, index) in filteredItems"
:key="item.uuid ?? index"
@mouseenter="emit('hover', item.uuid ?? null)"
@mouseleave="emit('hover', null)"
>
<SupplierCard

View File

@@ -71,7 +71,7 @@ defineEmits<{
const localePath = useLocalePath()
const { t } = useI18n()
const linkable = computed(() => !props.selectable && props.supplier.uuid)
const linkable = computed(() => !props.selectable && !!props.supplier.uuid)
const reliabilityLabel = computed(() => {
if (props.supplier.onTimeRate !== undefined && props.supplier.onTimeRate !== null) {

View File

@@ -131,7 +131,7 @@
<!-- Chips below (with colored circle icons) -->
<div
v-if="availableChips.length > 0"
v-if="availableChips?.length"
class="flex items-center justify-center gap-2"
>
<button

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Alert.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Alert',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,46 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import Button from './Button.vue'
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
render: (args) => ({
components: { Button },
setup() {
return { args }
},
template: '<Button v-bind="args">{{ args.label }}</Button>'
}),
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'outline']
}
},
tags: ['autodocs']
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {
label: 'Primary button',
variant: 'primary'
}
}
export const Outline: Story = {
args: {
label: 'Outline button',
variant: 'outline'
}
}
export const FullWidth: Story = {
args: {
label: 'Full width',
variant: 'primary',
fullWidth: true
}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Card.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Card',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Container.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Container',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './FieldButton.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/FieldButton',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -20,7 +20,7 @@ const props = defineProps({
default: '',
},
type: {
type: String,
type: String as () => 'button' | 'submit' | 'reset',
default: 'button',
},
chevron: {

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Grid.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Grid',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './GridItem.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/GridItem',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Heading.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Heading',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './IconCircle.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/IconCircle',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Input.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Input',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Pill.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Pill',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Section.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Section',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Select.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Select',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Spinner.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Spinner',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Stack.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Stack',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Text.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Text',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}

View File

@@ -1,21 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3'
import StoryComponent from './Textarea.vue'
const meta: Meta<typeof StoryComponent> = {
title: 'Ui/Textarea',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind="args" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {}
}