mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-11 01:22:06 +00:00
feat(frontend): show Grok monitoring and Free plan badge
This commit is contained in:
1 parent
0a64a6d8ce
commit
d2d3fcf57b
17 files changed
+423
-18
No files matched your search
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg
|
||||
class="h-3 w-3 flex-shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
transform="scale(.76)"
|
||||
d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"
|
||||
/>
|
||||
<path d="M18.75 14.5l.72 2.53 2.53.72-2.53.72-.72 2.53-.72-2.53-2.53-.72 2.53-.72.72-2.53z" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -33,6 +33,17 @@
|
||||
<!-- Row 2: Plan type + Privacy mode (only if either exists) -->
|
||||
<div v-if="planLabel || privacyBadge" class="inline-flex items-center overflow-hidden rounded-md">
|
||||
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1', planBadgeClass]">
|
||||
<GrokFreeIcon
|
||||
v-if="isGrokFreePlan"
|
||||
data-testid="grok-free-plan-icon"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="planIconName"
|
||||
:name="planIconName"
|
||||
size="xs"
|
||||
data-testid="grok-plan-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ planLabel }}</span>
|
||||
</span>
|
||||
<span
|
||||
@@ -57,6 +68,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AccountPlatform, AccountType } from '@/types'
|
||||
import GrokFreeIcon from './GrokFreeIcon.vue'
|
||||
import PlatformIcon from './PlatformIcon.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
@@ -97,10 +109,13 @@ const typeLabel = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const normalizedPlanType = computed(() =>
|
||||
(props.planType || '').trim().toLowerCase().replace(/[\s_-]+/g, '')
|
||||
)
|
||||
|
||||
const planLabel = computed(() => {
|
||||
if (!props.planType) return ''
|
||||
const lower = props.planType.toLowerCase()
|
||||
switch (lower) {
|
||||
if (!normalizedPlanType.value) return ''
|
||||
switch (normalizedPlanType.value) {
|
||||
case 'plus':
|
||||
return 'Plus'
|
||||
case 'team':
|
||||
@@ -109,7 +124,12 @@ const planLabel = computed(() => {
|
||||
case 'pro':
|
||||
return 'Pro'
|
||||
case 'free':
|
||||
return 'Free'
|
||||
case 'basic':
|
||||
return props.platform === 'grok' ? 'Grok Free' : 'Free'
|
||||
case 'supergrok':
|
||||
return 'SuperGrok'
|
||||
case 'supergrokheavy':
|
||||
return 'SuperGrok Heavy'
|
||||
case 'abnormal':
|
||||
return t('admin.accounts.subscriptionAbnormal')
|
||||
default:
|
||||
@@ -117,6 +137,22 @@ const planLabel = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const isGrokFreePlan = computed(() =>
|
||||
props.platform === 'grok' &&
|
||||
(normalizedPlanType.value === 'free' || normalizedPlanType.value === 'basic')
|
||||
)
|
||||
|
||||
const planIconName = computed<'bolt' | null>(() => {
|
||||
if (props.platform !== 'grok') return null
|
||||
if (
|
||||
normalizedPlanType.value === 'supergrok' ||
|
||||
normalizedPlanType.value === 'supergrokheavy'
|
||||
) {
|
||||
return 'bolt'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const platformClass = computed(() => {
|
||||
if (props.platform === 'anthropic') {
|
||||
return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
@@ -150,7 +186,7 @@ const typeClass = computed(() => {
|
||||
})
|
||||
|
||||
const planBadgeClass = computed(() => {
|
||||
if (props.planType && props.planType.toLowerCase() === 'abnormal') {
|
||||
if (normalizedPlanType.value === 'abnormal') {
|
||||
return 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400'
|
||||
}
|
||||
return typeClass.value
|
||||
@@ -159,7 +195,7 @@ const planBadgeClass = computed(() => {
|
||||
// Subscription expiration label (non-free only)
|
||||
const expiresLabel = computed(() => {
|
||||
if (!props.subscriptionExpiresAt || !props.planType) return ''
|
||||
if (props.planType.toLowerCase() === 'free') return ''
|
||||
if (normalizedPlanType.value === 'free' || normalizedPlanType.value === 'basic') return ''
|
||||
try {
|
||||
const d = new Date(props.subscriptionExpiresAt)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import GrokFreeIcon from '../GrokFreeIcon.vue'
|
||||
import PlatformTypeBadge from '../PlatformTypeBadge.vue'
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}
|
||||
})
|
||||
|
||||
describe('PlatformTypeBadge Grok plans', () => {
|
||||
it('renders FREE and BASIC as Grok Free with a lightweight plan icon', async () => {
|
||||
const wrapper = mount(PlatformTypeBadge, {
|
||||
props: {
|
||||
platform: 'grok',
|
||||
type: 'oauth',
|
||||
planType: 'BASIC',
|
||||
subscriptionExpiresAt: '2027-01-01T00:00:00Z',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('Grok Free')
|
||||
expect(wrapper.findComponent(GrokFreeIcon).exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="grok-free-plan-icon"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="grok-plan-icon"]').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('2027-01-01')
|
||||
|
||||
await wrapper.setProps({ planType: 'FREE' })
|
||||
expect(wrapper.text()).toContain('Grok Free')
|
||||
expect(wrapper.findComponent(GrokFreeIcon).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps SuperGrok labels compatible and marks paid Grok plans', async () => {
|
||||
const wrapper = mount(PlatformTypeBadge, {
|
||||
props: {
|
||||
platform: 'grok',
|
||||
type: 'oauth',
|
||||
planType: 'SuperGrok Heavy',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('SuperGrok Heavy')
|
||||
expect(wrapper.find('[data-testid="grok-plan-icon"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="grok-free-plan-icon"]').exists()).toBe(false)
|
||||
|
||||
await wrapper.setProps({ platform: 'openai', planType: 'free' })
|
||||
expect(wrapper.text()).toContain('Free')
|
||||
expect(wrapper.text()).not.toContain('Grok Free')
|
||||
expect(wrapper.find('[data-testid="grok-plan-icon"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('uses a dedicated 12px currentColor Grok mark with a Free sparkle', () => {
|
||||
const wrapper = mount(GrokFreeIcon)
|
||||
|
||||
expect(wrapper.element.tagName.toLowerCase()).toBe('svg')
|
||||
expect(wrapper.attributes('fill')).toBe('currentColor')
|
||||
expect(wrapper.classes()).toEqual(expect.arrayContaining(['h-3', 'w-3']))
|
||||
expect(wrapper.findAll('path')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user