Files
MyIP/frontend/components/ip-infos/ASNInfo.vue
T
Claude 96525d2fc7 Chore: trim the branch — leftovers, redundancy, comments
A pass over every file the branch touched, after the iOS animation hunt
left tries and reverts behind it. Removed what nothing reaches: the
view-transition rule for a transition no code starts, an unreachable
guard in the transition hook, a no-op KeepAlive key branch, a sign-in
guard the menu already enforces, usage-dialog wrappers that only
forwarded to the store, a duplicated modifier-click check and card
filter (isPlainClick / listedTools), a test that hand-rolled
listedTools, a drawer-era fixture parameter. auth-flow.js folded into
auth-redirect.js. Comments cut to what the code cannot say itself —
file headers of a few lines, the measured iOS reasons, non-obvious
orderings — and stale ones fixed (Nav under KeepAlive, Home.vue
reusing DEFAULT_META, "from Nav", enabledCards). AGENTS.md sections
tightened to the code as it is now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
2026-10-05 16:50:16 +00:00

113 lines
4.5 KiB
Vue

<template>
<!-- ASN Info Panel: embedded in IPCard's Collapsible to expand -->
<div class="rounded-md border bg-muted/40 text-sm">
<!-- Top note -->
<div class="px-3 pt-3 pb-2 flex items-start gap-2 text-xs text-muted-foreground">
<span>{{ t('ipInfos.ASNInfo.note') }}</span>
</div>
<div v-if="asnInfos[asn]" class="px-3 pb-3 space-y-3">
<!-- Basic information: compact dl two columns -->
<dl class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
<div v-for="(item, key) in basicInfo" :key="key" :class="{ 'col-span-2': key === 'asnOrgName' }">
<dt class="text-xs text-muted-foreground mb-0.5">{{ t(`ipInfos.ASNInfo.${key}`) }}</dt>
<dd class="font-normal flex items-center gap-1.5 wrap-break-word">
<template v-if="key === 'asnCountryCode'">
<Icon v-if="item" :icon="'circle-flags:' + item.toLowerCase()" class="shrink-0 size-4" />
<span>{{ getCountryName(item, lang) }}</span>
</template>
<template v-else>{{ item }}</template>
</dd>
</div>
</dl>
<!-- Connection quality (Cloudflare speed test aggregates) -->
<AsnConnectionQuality :info="asnInfos[asn]" />
<!-- Traffic shares (pair bars) -->
<AsnTrafficShares :info="asnInfos[asn]" />
<!-- Full profile: a real link; a plain click routes in-app -->
<div v-if="profileHref" class="pt-1">
<Button as-child variant="outline" size="sm" class="h-7 cursor-pointer gap-1.5 text-xs">
<a :href="profileHref" @click="openProfile">
<ArrowUpRight class="size-3.5" />{{ t('ipInfos.ASNInfo.openProfile') }}
</a>
</Button>
</div>
</div>
<!-- Loading state skeleton -->
<div v-else class="px-3 pb-3 space-y-2">
<div v-for="(w, i) in placeholderSizes" :key="i" class="h-3.5 bg-muted rounded animate-pulse"
:style="`width: ${(w / 12) * 100}%`"></div>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
import { useMainStore } from '@/store';
import { computed } from 'vue';
import getCountryName from '@/data/country-name.js';
import AsnConnectionQuality from './AsnConnectionQuality.vue';
import AsnTrafficShares from './AsnTrafficShares.vue';
import { useRouter } from 'vue-router';
import { trackEvent } from '@/utils/analytics';
import { parseAsnInput } from '@/utils/ip/asn-input.js';
import { isToolAvailable } from '@/utils/tool-availability.js';
import { TOOL_BY_SLUG } from '@/data/tools.js';
import { isPlainClick } from '@/utils/nav-target.js';
import { Button } from '@/components/ui/button';
import { Icon } from '@iconify/vue';
import { ArrowUpRight } from '@lucide/vue';
const { t } = useI18n();
const store = useMainStore();
const lang = computed(() => store.lang);
const placeholderSizes = [12, 8, 6, 8, 4];
const props = defineProps({
index: { type: Number, required: true },
isDarkMode: { type: Boolean, required: true },
asn: { type: String, required: true },
asnInfos: { type: Object, required: true }
});
// Fired before a plain-click open, so a host dialog (QueryIP) closes first.
const emit = defineEmits(['open-profile']);
// Extract basic information (non-pair data)
const basicInfo = computed(() => {
const data = props.asnInfos[props.asn];
if (!data) return {};
const info = {};
const keys = [
'asnName', 'asnCountryCode', 'asnOrgName', 'estimatedUsers',
];
for (const key of keys) {
if (data[key]) info[key] = data[key];
}
return info;
});
// ASN Profile entry: hidden when the tool is gated off (no Cloudflare key)
// or the ASN doesn't parse; `q` makes the tool run the lookup.
const router = useRouter();
const profileAsn = computed(() => {
if (!isToolAvailable(TOOL_BY_SLUG.get('asn'), store.configs)) return null;
const asn = parseAsnInput(props.asn);
return asn === null ? null : `AS${asn}`;
});
const profileHref = computed(() => (profileAsn.value ? `/tools/asn?q=${profileAsn.value}` : ''));
const openProfile = (e) => {
if (!isPlainClick(e)) return;
e.preventDefault();
trackEvent('IPCheck', 'ASNProfileClick', 'Open ASN Profile');
emit('open-profile');
router.push({ path: '/tools/asn', query: { q: profileAsn.value } });
};
</script>