Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,114 @@
<script setup>
import { ref } from 'vue';
import GroupedStackedChangelogCard from './GroupedStackedChangelogCard.vue';
const sampleCards = [
{
id: 'chatwoot-captain',
title: 'Chatwoot Captain',
meta_title: 'Chatwoot Captain',
meta_description:
'Watch how our latest feature can transform your workflow with powerful automation tools.',
slug: 'chatwoot-captain',
feature_image:
'https://www.chatwoot.com/images/captain/captain_thumbnail.jpg',
},
{
id: 'smart-routing',
title: 'Smart Routing Forms',
meta_title: 'Smart Routing Forms',
meta_description:
'Screen bookers with intelligent forms and route them to the right team member.',
slug: 'smart-routing',
feature_image: 'https://www.chatwoot.com/images/dashboard-dark.webp',
},
{
id: 'instant-meetings',
title: 'Instant Meetings',
meta_title: 'Instant Meetings',
meta_description: 'Start instant meetings directly from shared links.',
slug: 'instant-meetings',
feature_image:
'https://images.unsplash.com/photo-1587614382346-4ec70e388b28?w=600',
},
{
id: 'analytics',
title: 'Advanced Analytics',
meta_title: 'Advanced Analytics',
meta_description:
'Track meeting performance, conversion, and response rates in one place.',
slug: 'analytics',
feature_image:
'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=500',
},
{
id: 'team-collaboration',
title: 'Team Collaboration',
meta_title: 'Team Collaboration',
meta_description:
'Coordinate with your team seamlessly using shared availability.',
slug: 'team-collaboration',
feature_image:
'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400',
},
];
const visibleCards = ref([...sampleCards]);
const currentIndex = ref(0);
const dismissingCards = ref([]);
const handleReadMore = slug => {
console.log(`Read more: ${slug}`);
};
const handleDismiss = slug => {
dismissingCards.value.push(slug);
setTimeout(() => {
const idx = visibleCards.value.findIndex(c => c.slug === slug);
if (idx !== -1) visibleCards.value.splice(idx, 1);
dismissingCards.value = dismissingCards.value.filter(s => s !== slug);
if (currentIndex.value >= visibleCards.value.length) currentIndex.value = 0;
}, 200);
};
const handleImgClick = data => {
currentIndex.value = data.index;
console.log(`Card clicked: ${visibleCards.value[data.index].title}`);
};
const resetDemo = () => {
visibleCards.value = [...sampleCards];
currentIndex.value = 0;
dismissingCards.value = [];
};
</script>
<template>
<Story
title="Components/ChangelogCard/GroupedStackedChangelogCard"
:layout="{ type: 'grid', width: '320px' }"
>
<Variant title="Interactive Demo">
<div class="p-4 bg-n-solid-2 rounded-md mx-auto w-64 h-[400px]">
<GroupedStackedChangelogCard
:posts="visibleCards"
:current-index="currentIndex"
:is-active="currentIndex === 0"
:dismissing-slugs="dismissingCards"
class="min-h-[270px]"
@read-more="handleReadMore"
@dismiss="handleDismiss"
@img-click="handleImgClick"
/>
<button
class="mt-3 px-3 py-1 text-xs font-medium bg-n-brand text-white rounded hover:bg-n-brand/80 transition"
@click="resetDemo"
>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
{{ 'Reset Cards' }}
</button>
</div>
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,74 @@
<script setup>
import { computed } from 'vue';
import StackedChangelogCard from './StackedChangelogCard.vue';
const props = defineProps({
posts: {
type: Array,
required: true,
},
currentIndex: {
type: Number,
default: 0,
},
dismissingSlugs: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['readMore', 'dismiss', 'imgClick']);
const stackedPosts = computed(() => props.posts?.slice(0, 5));
const isPostDismissing = post => props.dismissingSlugs.includes(post.slug);
const handleReadMore = post => emit('readMore', post.slug);
const handleDismiss = post => emit('dismiss', post.slug);
const handlePostClick = (post, index) => {
if (index === props.currentIndex && !isPostDismissing(post)) {
emit('imgClick', { slug: post.slug, index });
}
};
const getCardClasses = index => {
const pos =
(index - props.currentIndex + stackedPosts.value.length) %
stackedPosts.value.length;
const base =
'relative transition-all duration-500 ease-out col-start-1 row-start-1';
const layers = [
'z-50 scale-100 translate-y-0 opacity-100',
'z-40 scale-[0.95] -translate-y-3 opacity-90',
'z-30 scale-[0.9] -translate-y-6 opacity-70',
'z-20 scale-[0.85] -translate-y-9 opacity-50',
'z-10 scale-[0.8] -translate-y-12 opacity-30',
];
return pos < layers.length
? `${base} ${layers[pos]}`
: `${base} opacity-0 scale-75 -translate-y-16`;
};
</script>
<template>
<div class="overflow-hidden">
<div class="relative grid grid-cols-1 pt-8 pb-1 px-2">
<div
v-for="(post, index) in stackedPosts"
:key="post.slug || index"
:class="getCardClasses(index)"
>
<StackedChangelogCard
:card="post"
:is-active="index === currentIndex"
:is-dismissing="isPostDismissing(post)"
@read-more="handleReadMore(post)"
@dismiss="handleDismiss(post)"
@img-click="handlePostClick(post, index)"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,46 @@
<script setup>
import StackedChangelogCard from './StackedChangelogCard.vue';
const imageCards = {
id: 'chatwoot-captain',
title: 'Chatwoot Captain',
meta_title: 'Chatwoot Captain',
meta_description:
'Watch how our latest feature can transform your workflow with powerful automation tools.',
slug: 'chatwoot-captain',
feature_image:
'https://www.chatwoot.com/images/captain/captain_thumbnail.jpg',
};
const handleReadMore = () => {
console.log(`Read more: ${imageCards.title}`);
};
const handleDismiss = () => {
console.log(`Dismissed: ${imageCards.title}`);
};
const handleImgClick = () => {
console.log(`Card clicked: ${imageCards.title}`);
};
</script>
<template>
<Story
title="Components/ChangelogCard/StackedChangelogCard"
:layout="{ type: 'grid', width: '260px' }"
>
<Variant title="Single Card - With Image">
<div class="p-3 bg-n-solid-2 w-56">
<StackedChangelogCard
:card="imageCards"
is-active
:is-dismissing="false"
@read-more="handleReadMore(imageCards)"
@dismiss="handleDismiss(imageCards)"
@img-click="handleImgClick(imageCards)"
/>
</div>
</Variant>
</Story>
</template>

View File

@@ -0,0 +1,119 @@
<script setup>
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
card: {
type: Object,
required: true,
},
isActive: {
type: Boolean,
default: false,
},
isDismissing: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['readMore', 'dismiss', 'imgClick']);
const handleReadMore = () => {
emit('readMore');
};
const handleDismiss = () => {
emit('dismiss');
};
const handleImgClick = () => {
emit('imgClick');
};
</script>
<template>
<div
data-testid="changelog-card"
class="flex flex-col justify-between p-3 w-full rounded-lg border shadow-sm transition-all duration-200 border-n-weak bg-n-card text-n-slate-12"
:class="{
'animate-fade-out pointer-events-none': isDismissing,
'hover:shadow': isActive,
}"
>
<div>
<h5
:title="card.meta_title"
class="mb-1 text-sm font-semibold line-clamp-1 text-n-slate-12"
>
{{ card.meta_title }}
</h5>
<p
:title="card.meta_description"
class="mb-0 text-xs leading-relaxed text-n-slate-11 line-clamp-2"
>
{{ card.meta_description }}
</p>
</div>
<div
v-if="card.feature_image"
class="block overflow-hidden my-3 rounded-md border border-n-weak/40"
>
<img
:src="card.feature_image"
:alt="`${card.title} preview image`"
class="object-cover w-full h-24 rounded-md cursor-pointer"
loading="lazy"
@click.stop="handleImgClick"
/>
</div>
<div
v-else
class="block overflow-hidden my-3 rounded-md border border-n-weak/40"
>
<img
:src="card.feature_image"
:alt="`${card.title} preview image`"
class="object-cover w-full h-24 rounded-md cursor-pointer"
loading="lazy"
@click.stop="handleImgClick"
/>
</div>
<div class="flex justify-between items-center mt-1">
<Button
label="Read more"
color="slate"
link
sm
class="text-xs font-normal hover:!no-underline"
@click.stop="handleReadMore"
/>
<Button
label="Dismiss"
color="slate"
link
sm
class="text-xs font-normal hover:!no-underline"
@click.stop="handleDismiss"
/>
</div>
</div>
</template>
<style scoped>
@keyframes fade-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
.animate-fade-out {
animation: fade-out 0.2s ease-out forwards;
}
</style>