Show Telegram subscribers on post metrics; render reactions as compact pills
This commit is contained in:
parent
fc1d51ca60
commit
a5b33f5b0a
3 changed files with 85 additions and 16 deletions
|
|
@ -46,19 +46,25 @@ public function getMetrics(SocialAccount $account): array
|
|||
* Post-level metrics. Reaction counts are pushed by the webhook and stored
|
||||
* on the post platform's meta (the Bot API offers no post views to bots).
|
||||
*
|
||||
* @return array<int, array{label: string, value: int}>
|
||||
* @return array<int, array{label: string, value: int, kind?: string}>
|
||||
*/
|
||||
public function fetchPostMetrics(PostPlatform $postPlatform): array
|
||||
{
|
||||
$account = $postPlatform->socialAccount;
|
||||
$metrics = $account ? $this->getMetrics($account) : [];
|
||||
|
||||
$reactions = data_get($postPlatform->meta, 'reactions', []);
|
||||
|
||||
if (! is_array($reactions)) {
|
||||
return [];
|
||||
if (is_array($reactions)) {
|
||||
foreach ($reactions as $reaction) {
|
||||
$metrics[] = [
|
||||
'label' => (string) data_get($reaction, 'type'),
|
||||
'value' => (int) data_get($reaction, 'count'),
|
||||
'kind' => 'reaction',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_map(fn (array $reaction): array => [
|
||||
'label' => (string) data_get($reaction, 'type'),
|
||||
'value' => (int) data_get($reaction, 'count'),
|
||||
], $reactions));
|
||||
return $metrics;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { metrics as metricsRoute } from '@/routes/app/posts/platforms';
|
|||
interface Metric {
|
||||
label: string;
|
||||
value: number;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
type MetricsResponse = Metric[] | { unsupported: true; reason: string };
|
||||
|
|
@ -22,10 +23,18 @@ const props = defineProps<Props>();
|
|||
const loading = ref(true);
|
||||
const metrics = ref<Metric[]>([]);
|
||||
|
||||
const stats = computed(() =>
|
||||
metrics.value.filter((m) => m.kind !== 'reaction'),
|
||||
);
|
||||
const reactions = computed(() =>
|
||||
metrics.value.filter((m) => m.kind === 'reaction'),
|
||||
);
|
||||
|
||||
const hasMetrics = computed(() => metrics.value.length > 0);
|
||||
|
||||
const formatNumber = (n: number): string => {
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
|
||||
if (n >= 1_000_000)
|
||||
return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
|
||||
if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'K';
|
||||
return n.toString();
|
||||
};
|
||||
|
|
@ -35,7 +44,10 @@ const http = useHttp<Record<string, never>, MetricsResponse>({});
|
|||
onMounted(async () => {
|
||||
try {
|
||||
const response = await http.get(
|
||||
metricsRoute.url({ post: props.postId, postPlatform: props.postPlatformId }),
|
||||
metricsRoute.url({
|
||||
post: props.postId,
|
||||
postPlatform: props.postPlatformId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
|
|
@ -61,20 +73,45 @@ onMounted(async () => {
|
|||
|
||||
<!-- Loaded with data: full metrics block. -->
|
||||
<div v-else-if="hasMetrics" class="border-t px-4 py-3">
|
||||
<div class="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<div
|
||||
class="mb-2 flex items-center gap-1.5 text-xs font-semibold tracking-wider text-muted-foreground uppercase"
|
||||
>
|
||||
<IconChartBar class="h-3 w-3" />
|
||||
{{ $t('posts.show.metrics') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div v-if="stats.length > 0" class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
v-for="metric in metrics"
|
||||
v-for="metric in stats"
|
||||
:key="metric.label"
|
||||
class="rounded-md bg-muted/50 px-2.5 py-1.5"
|
||||
>
|
||||
<p class="text-[10px] uppercase tracking-wider text-muted-foreground">{{ metric.label }}</p>
|
||||
<p class="text-sm font-semibold tabular-nums">{{ formatNumber(metric.value) }}</p>
|
||||
<p
|
||||
class="text-[10px] tracking-wider text-muted-foreground uppercase"
|
||||
>
|
||||
{{ metric.label }}
|
||||
</p>
|
||||
<p class="text-sm font-semibold tabular-nums">
|
||||
{{ formatNumber(metric.value) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="reactions.length > 0"
|
||||
class="flex flex-wrap gap-1.5"
|
||||
:class="{ 'mt-2': stats.length > 0 }"
|
||||
>
|
||||
<span
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.label"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-muted/50 px-2 py-1 text-xs"
|
||||
>
|
||||
<span class="text-sm leading-none">{{ reaction.label }}</span>
|
||||
<span class="font-semibold tabular-nums">{{
|
||||
formatNumber(reaction.value)
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data / unsupported: render nothing so the card stays clean. -->
|
||||
|
|
|
|||
|
|
@ -33,17 +33,43 @@
|
|||
expect(app(TelegramAnalytics::class)->getMetrics($account))->toBe([]);
|
||||
});
|
||||
|
||||
it('maps stored reactions to post metrics', function () {
|
||||
it('maps stored reactions to post metrics, tagged as reactions', function () {
|
||||
config(['trypost.platforms.telegram.bot_token' => '']); // skip the subscriber lookup
|
||||
|
||||
$postPlatform = PostPlatform::factory()->create([
|
||||
'platform' => Platform::Telegram,
|
||||
'meta' => ['reactions' => [['type' => '👍', 'count' => 12], ['type' => '❤️', 'count' => 5]]],
|
||||
]);
|
||||
|
||||
expect(app(TelegramAnalytics::class)->fetchPostMetrics($postPlatform))
|
||||
->toBe([['label' => '👍', 'value' => 12], ['label' => '❤️', 'value' => 5]]);
|
||||
->toBe([
|
||||
['label' => '👍', 'value' => 12, 'kind' => 'reaction'],
|
||||
['label' => '❤️', 'value' => 5, 'kind' => 'reaction'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes the channel subscriber count alongside reactions', function () {
|
||||
$account = SocialAccount::factory()->telegram()->create();
|
||||
$postPlatform = PostPlatform::factory()->create([
|
||||
'social_account_id' => $account->id,
|
||||
'platform' => Platform::Telegram,
|
||||
'meta' => ['reactions' => [['type' => '👍', 'count' => 3]]],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*/botTESTTOKEN/getChatMemberCount*' => Http::response(['ok' => true, 'result' => 50], 200),
|
||||
]);
|
||||
|
||||
expect(app(TelegramAnalytics::class)->fetchPostMetrics($postPlatform))
|
||||
->toBe([
|
||||
['label' => 'Subscribers', 'value' => 50],
|
||||
['label' => '👍', 'value' => 3, 'kind' => 'reaction'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no post metrics when there are no reactions yet', function () {
|
||||
config(['trypost.platforms.telegram.bot_token' => '']);
|
||||
|
||||
$postPlatform = PostPlatform::factory()->create([
|
||||
'platform' => Platform::Telegram,
|
||||
'meta' => [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue