fix(user-app): rebuild product gallery with fullscreen preview, fix icon reactivity bug

- Extract product image gallery into a dedicated GallerySlider component
  with swipe/tap support
- Add a teleported fullscreen image preview (tap to open, thumbnail strip,
  body scroll lock) to SingleProduct
- Restore the Trust section that was accidentally commented out
- Swap back/favorite button positions and recenter the image counter
- Fix Icon.vue: tempIcon was a static ref set once at setup, so it never
  updated on prop change when Vue reused the component instance (e.g. cart
  qty steppers toggling trash/minus icons); switched to computed + watch
- Rename brand text from "منتزر" to "هنزاگلد"
- Fix a CSS typo in user-app-v2 SingleProduct (rgba alpha value)
- Rebuild dist assets
This commit is contained in:
s.esmaeili
2026-09-18 19:54:26 +03:30
parent 062fe80a68
commit e8c67bead0
52 changed files with 573 additions and 86 deletions
+30 -10
View File
@@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import icons from './icons.json'
interface Props {
@@ -14,17 +14,37 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
color: null,
})
const tempIcon = ref<string>()
const refIcon = ref<HTMLSpanElement>()
tempIcon.value = (icons as Record<string, string>)[`${props.icon}`]
onMounted(() => {
if (props.size) {
const svg = refIcon.value?.children[0] as SVGElement
svg.setAttribute('width', props.size as string)
svg.setAttribute('height', props.size as string)
}
})
// FIXED: this used to be a plain ref, assigned once at setup —
// tempIcon.value = icons[props.icon]
// — instead of a computed. Vue reuses the same component instance across
// re-renders when an `Icon` sits in the same template position (which every
// caller in this app does; none pass a `:key` keyed to the icon name), so
// setup only runs once per mount and that assignment never ran again. Any
// caller that swaps `icon` on an already-mounted instance — the exact pattern
// the cart quantity steppers use, `:icon="qty === 1 ? 'trash' : 'minus'"` —
// had its SVG freeze on whichever icon happened to render first and silently
// stop tracking the prop after that: incrementing a cart line past 1 never
// swapped the frozen trash glyph back to minus, and the reverse (decrementing
// back down to 1) never swapped a frozen minus back to trash either. A
// computed re-evaluates on every `icon` change, so the markup — and, via the
// watcher below, the width/height attributes size() sets on it — now actually
// track the prop instead of only reading it once.
const tempIcon = computed(() => (icons as Record<string, string>)[`${props.icon}`])
function applySize() {
if (!props.size) return
const svg = refIcon.value?.children[0] as SVGElement | undefined
svg?.setAttribute('width', props.size as string)
svg?.setAttribute('height', props.size as string)
}
// v-html replaces refIcon's children synchronously with the DOM update, but
// that update itself is queued — nextTick() waits for it so applySize() finds
// the new <svg> already in the DOM rather than the previous one (or none, on
// the very first render).
watch(tempIcon, () => nextTick(applySize), { immediate: true })
</script>
<style scoped>