mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Feat(footer): fold the product links into a four-column footer
The Raycast / curl / Lite badges sat in an unlabelled image row of their own under the Advanced Tools. The footer now has four columns aligned with that card grid — brand, Project, Docs & Support, Clients — so the product entries get a heading and show on every route; the curl commands keep their dialog. Docs links share one locale-aware helper. The footer never repeats on-page content, and the fixed footer height in style.css goes, as the columns size themselves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d28fe239ae
commit
7a608cb8ce
16 files changed
+343
-210
No files matched your search
@@ -179,6 +179,11 @@ Every "business state → color" mapping goes through `use-status-tone.js`
|
||||
no section is highlighted off Home.
|
||||
- The link row collapses to the hamburger + Sheet when `use-nav-overflow.js`
|
||||
measures it doesn't fit (no breakpoint); `isMobile` only drives phone behaviour.
|
||||
- **Footer** — four columns on the Advanced Tools grid: brand / Project / Docs
|
||||
& Support / Clients (only the CLI entry is gated, on the curl domains);
|
||||
never repeats on-page content (tools, sections); About / Changelog /
|
||||
Acknowledgement open in its About sheet, the CLI in its own dialog
|
||||
(`Footer.vue`).
|
||||
- **Page breadcrumb** — every page but Home opens its body with
|
||||
`<PageBreadcrumb :items />` above the `<h1>` (← Home / … / current page):
|
||||
real links, Home via `resolveBackTarget()`, section crumbs via the Nav's
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<!-- Curl Dialog -->
|
||||
<Dialog :open="isOpen" @update:open="isOpen = $event">
|
||||
<DialogContent :title="t('curl.Title')">
|
||||
<DialogHeader :icon="Terminal" :title="t('curl.Title')" />
|
||||
|
||||
<div v-if="curlDomainsHadSet" class="space-y-3">
|
||||
<!-- Description -->
|
||||
<div class="space-y-1 text-xs font-mono">
|
||||
<p class="jn-comment"><span class="text-muted-foreground">{{ t('curl.Note1') }}</span></p>
|
||||
<p class="jn-comment">
|
||||
<span class="text-muted-foreground">{{ t('curl.Note2_1') }}
|
||||
<Badge variant="outline" class="text-success">curl</Badge> {{ t('curl.Note2_2') }}</span>
|
||||
</p>
|
||||
<p class="jn-comment">
|
||||
<span class="text-muted-foreground"><Badge variant="outline" class="text-success">geo</Badge> {{ t('curl.Note3') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 3 curl command blocks -->
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv4') }}</p>
|
||||
<pre class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ ipv4Domain }}<span class="text-success">/geo</span></pre>
|
||||
</div>
|
||||
<div>
|
||||
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv6') }}</p>
|
||||
<pre class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ ipv6Domain }}<span class="text-success">/geo</span></pre>
|
||||
</div>
|
||||
<div>
|
||||
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.get6and4') }}</p>
|
||||
<pre class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ ipv64Domain }}<span class="text-success">/geo</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="py-6 text-center">
|
||||
<p class="text-sm text-muted-foreground">{{ t('curl.notAvailable') }}</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Additional Tools: external product link bar -->
|
||||
<div class="mx-auto text-center max-w-[98%]">
|
||||
<div id="morefromipchecking" class="flex justify-center items-center gap-2 sm:gap-4 flex-wrap">
|
||||
<a href="https://www.raycast.com/jason5ng32/ipcheck-ing" target="_blank" rel="noopener"
|
||||
@click="trackEvent('Additional', 'AdditionalClick', 'Raycast')">
|
||||
<img src="/additional/raycast.webp" alt="IPCheck.ing on Raycast"
|
||||
class="w-[108px] sm:w-[180px] h-auto">
|
||||
</a>
|
||||
|
||||
<button type="button" @click="openCurlModal"
|
||||
aria-label="IPCheck.ing for Curl"
|
||||
class="cursor-pointer bg-transparent border-0 p-0">
|
||||
<img src="/additional/curl.webp" alt="IPCheck.ing for Curl"
|
||||
class="w-[108px] sm:w-[180px] h-auto">
|
||||
</button>
|
||||
|
||||
<a href="https://lite.ipcheck.ing" target="_blank" rel="noopener"
|
||||
@click="trackEvent('Additional', 'AdditionalClick', 'Lite')">
|
||||
<img src="/additional/lite.webp" alt="IPCheck.ing lite"
|
||||
class="w-[108px] sm:w-[180px] h-auto">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useMainStore } from '@/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { trackEvent } from '@/utils/analytics';
|
||||
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
|
||||
import { Terminal } from '@lucide/vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const store = useMainStore();
|
||||
|
||||
const ipv4Domain = computed(() => store.curl.ipv4Domain);
|
||||
const ipv6Domain = computed(() => store.curl.ipv6Domain);
|
||||
const ipv64Domain = computed(() => store.curl.ipv64Domain);
|
||||
const curlDomainsHadSet = computed(() => store.curlDomainsHadSet);
|
||||
|
||||
const isOpen = ref(false);
|
||||
const openCurlModal = () => {
|
||||
isOpen.value = true;
|
||||
trackEvent('Additional', 'AdditionalClick', 'Curl');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.jn-curl::before {
|
||||
content: '$ ';
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 500;
|
||||
margin-right: 0.25rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.jn-comment::before {
|
||||
content: '// ';
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
+205
-58
@@ -1,65 +1,141 @@
|
||||
<!-- Site footer on every page: brand / Project / Docs & Support / Clients
|
||||
columns, plus a signature bar (with the copyright on self-hosted
|
||||
deployments). About / Changelog / Acknowledgement open in its About
|
||||
sheet, the CLI in its own dialog; nothing already on the page (tools,
|
||||
sections) repeats here. -->
|
||||
<template>
|
||||
<footer class="pb-6 text-sm text-muted-foreground" :class="!isInHomePage ? 'border-t border-secondary' : ''">
|
||||
<!-- Primary footer links: Sponsor / Privacy / About / Changelog / Special
|
||||
Thanks. The last three open the same About sheet on their own tab.
|
||||
Text-only (no per-button icons) to keep the row calm. -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-x-3 mb-2 mx-auto">
|
||||
<Button variant="link" size="default" as-child class="text-[#d63384] hover:text-[#d63384]">
|
||||
<a href="https://github.com/sponsors/jason5ng32" target="_blank" rel="noopener">
|
||||
{{ t('about.Sponsor') }}
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="link" size="default" as-child class="cursor-pointer">
|
||||
<RouterLink to="/privacy" @click="trackEvent('Footer', 'FooterClick', 'Privacy')">
|
||||
{{ t('about.Privacy') }}
|
||||
</RouterLink>
|
||||
</Button>
|
||||
<Button variant="link" size="default" as-child class="cursor-pointer">
|
||||
<a :href="'https://docs.ipcheck.ing/knowledge-base/'+(locale === 'en' ? '' : locale)" target="_blank"
|
||||
rel="noopener" @click="trackEvent('Footer', 'FooterClick', 'HelpCenter')">
|
||||
{{ t('about.HelpCenter') }}
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="link" size="default" as-child class="cursor-pointer">
|
||||
<a :href="'https://docs.ipcheck.ing/developer/'+(locale === 'en' ? '' : locale)" target="_blank" rel="noopener"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'Contribute')">
|
||||
{{ t('about.Contribute') }}
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="link" size="default" @click="openAboutTab('changelog', 'Changelog')" class="cursor-pointer">
|
||||
{{ t('changelog.Title') }}
|
||||
</Button>
|
||||
<Button variant="link" size="default" @click="openAboutTab('acknowledgement', 'Acknowledgement')"
|
||||
class="cursor-pointer">
|
||||
{{ t('acknowledgement.Title') }}
|
||||
</Button>
|
||||
<Button variant="link" size="default" @click="openAbout" class="cursor-pointer">
|
||||
{{ t('about.Title') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Author + GitHub on one line, copyright dimmed beneath — low-emphasis signature block -->
|
||||
<div class="flex flex-col items-center justify-center gap-y-1.5 text-xs opacity-70">
|
||||
<div class="flex items-center justify-center gap-x-2">
|
||||
<span>Created by Jason Ng with love</span>
|
||||
<JnTooltip :text="t('Tooltips.GithubLink')" side="top">
|
||||
<Button variant="ghost" size="icon" as-child class="size-5 text-foreground/70 hover:text-foreground">
|
||||
<a :href="t('page.footerLink')" target="_blank" rel="noopener" aria-label="Github"
|
||||
<footer class="border-t border-secondary text-sm text-muted-foreground">
|
||||
<div class="mx-auto w-full px-4 jn-container">
|
||||
<!-- Link columns: four tracks on md+, matching the Advanced Tools grid;
|
||||
on phones the brand spans the top and the rest flow two per row. -->
|
||||
<div class="grid grid-cols-2 gap-8 py-12 md:grid-cols-4">
|
||||
<!-- Brand -->
|
||||
<div class="col-span-2 md:col-span-1">
|
||||
<div class="inline-flex items-center gap-1.5 text-xl font-semibold text-foreground">
|
||||
<brandIcon />
|
||||
<span class="tracking-tight">
|
||||
<span class="font-bold">IP</span><span class="font-extralight">Check.ing</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2">{{ t('footer.Tagline') }}</p>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||
<a :href="t('page.footerLink')" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'Github')">
|
||||
<Icon icon="ri:github-line" />
|
||||
<Icon icon="ri:github-line" class="size-4" />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</Button>
|
||||
</JnTooltip>
|
||||
<a href="https://github.com/sponsors/jason5ng32" target="_blank" rel="noopener"
|
||||
class="inline-flex items-center gap-1 text-sm text-[#d63384] no-underline transition-opacity hover:opacity-80">
|
||||
<Icon icon="ri:heart-line" class="size-4" />
|
||||
{{ t('about.Sponsor') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project: the About sheet's tabs, privacy and license -->
|
||||
<nav :aria-label="t('footer.Project')">
|
||||
<h3 :class="headingClass">{{ t('footer.Project') }}</h3>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<button type="button" :class="linkClass" @click="openAbout">
|
||||
{{ t('about.Title') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" :class="linkClass" @click="openAboutTab('changelog', 'Changelog')">
|
||||
{{ t('changelog.Title') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" :class="linkClass" @click="openAboutTab('acknowledgement', 'Acknowledgement')">
|
||||
{{ t('acknowledgement.Title') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/privacy" :class="linkClass" @click="trackEvent('Footer', 'FooterClick', 'Privacy')">
|
||||
{{ t('about.Privacy') }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<a :href="`${t('page.footerLink')}/blob/main/LICENSE`" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'License')">
|
||||
{{ t('footer.License') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Docs & Support: locale-aware docs.ipcheck.ing links + issue tracker -->
|
||||
<nav :aria-label="t('footer.Support')">
|
||||
<h3 :class="headingClass">{{ t('footer.Support') }}</h3>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a :href="docsUrl('knowledge-base', locale)" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'HelpCenter')">
|
||||
{{ t('about.HelpCenter') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :href="docsUrl('developer', locale)" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'DeveloperDocs')">
|
||||
{{ t('footer.DeveloperDocs') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :href="docsUrl('developer', locale, 'getting-started/')" target="_blank" rel="noopener"
|
||||
:class="linkClass" @click="trackEvent('Footer', 'FooterClick', 'SelfHosting')">
|
||||
{{ t('footer.SelfHosting') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :href="`${t('page.footerLink')}/issues`" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'ReportIssue')">
|
||||
{{ t('footer.ReportIssue') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Clients: Raycast / Lite always; the CLI entry needs the curl
|
||||
domains configured. -->
|
||||
<nav :aria-label="t('footer.Clients')">
|
||||
<h3 :class="headingClass">{{ t('footer.Clients') }}</h3>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="https://www.raycast.com/jason5ng32/ipcheck-ing" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'Raycast')">
|
||||
<Icon icon="simple-icons:raycast" class="size-4 shrink-0" />
|
||||
{{ t('footer.Raycast') }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="curlDomainsHadSet">
|
||||
<button type="button" :class="linkClass" @click="openCurl">
|
||||
<Terminal class="size-4 shrink-0" />
|
||||
{{ t('curl.Title') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://lite.ipcheck.ing" target="_blank" rel="noopener" :class="linkClass"
|
||||
@click="trackEvent('Footer', 'FooterClick', 'Lite')">
|
||||
<Feather class="size-4 shrink-0" />
|
||||
{{ t('footer.Lite') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<template v-if="!configs.originalSite">
|
||||
<span>
|
||||
|
||||
<!-- Signature bar: every deployment; the copyright only when self-hosted -->
|
||||
<div class="flex flex-col items-center gap-y-1.5 border-t border-secondary py-6 text-center text-xs opacity-70">
|
||||
<span>Created by Jason Ng with love</span>
|
||||
<span v-if="!configs.originalSite">
|
||||
{{ t('page.copyRightName') }}
|
||||
<a :href="t('page.copyRightLink')" target="_blank" rel="noopener"
|
||||
class="text-foreground/80 hover:text-foreground hover:underline">
|
||||
{{ t('page.copyRightLinkName') }}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Sheet -->
|
||||
@@ -151,10 +227,55 @@
|
||||
</li>
|
||||
</ul>
|
||||
</TabsContent>
|
||||
|
||||
</div>
|
||||
</Tabs>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<!-- Command Line API dialog: only opened from the Clients link, which
|
||||
is listed only when the curl domains are configured. -->
|
||||
<Dialog :open="isCurlOpen" @update:open="isCurlOpen = $event">
|
||||
<DialogContent :title="t('curl.Title')">
|
||||
<DialogHeader :icon="Terminal" :title="t('curl.Title')" />
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Description -->
|
||||
<div class="space-y-1 text-xs font-mono">
|
||||
<p class="jn-comment"><span class="text-muted-foreground">{{ t('curl.Note1') }}</span></p>
|
||||
<p class="jn-comment">
|
||||
<span class="text-muted-foreground">{{ t('curl.Note2_1') }}
|
||||
<Badge variant="outline" class="text-success">curl</Badge> {{ t('curl.Note2_2') }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="jn-comment">
|
||||
<span class="text-muted-foreground">
|
||||
<Badge variant="outline" class="text-success">geo</Badge> {{ t('curl.Note3') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 3 curl command blocks -->
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv4') }}</p>
|
||||
<pre
|
||||
class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ curl.ipv4Domain }}<span class="text-success">/geo</span></pre>
|
||||
</div>
|
||||
<div>
|
||||
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.getIPv6') }}</p>
|
||||
<pre
|
||||
class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ curl.ipv6Domain }}<span class="text-success">/geo</span></pre>
|
||||
</div>
|
||||
<div>
|
||||
<p class="jn-comment text-xs font-mono mb-1.5 text-muted-foreground">{{ t('curl.get6and4') }}</p>
|
||||
<pre
|
||||
class="jn-curl bg-black text-neutral-100 rounded-md p-3 text-xs font-mono overflow-x-auto">curl {{ curl.ipv64Domain }}<span class="text-success">/geo</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -164,28 +285,31 @@ import { useMainStore } from '@/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import changelogData from '@/data/changelog.json';
|
||||
import { trackEvent } from '@/utils/analytics';
|
||||
import { docsUrl } from '@/utils/docs-url.js';
|
||||
import { formatIsoDate } from '@/utils/time-utils';
|
||||
import { fallbackChain } from '@/utils/locale-registry.js';
|
||||
import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet';
|
||||
import { JnTooltip } from '@/components/ui/tooltip';
|
||||
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Compass, ExternalLink, CircleFadingArrowUp, CirclePlus, BugOff, Smile } from '@lucide/vue';
|
||||
import { Compass, ExternalLink, CircleFadingArrowUp, CirclePlus, BugOff, Smile, Terminal, Feather } from '@lucide/vue';
|
||||
import brandIcon from './svgicons/Brand.vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRouteActive } from '@/composables/use-route-active.js';
|
||||
|
||||
const { t, tm, rt, locale } = useI18n();
|
||||
|
||||
const store = useMainStore();
|
||||
const isMobile = computed(() => store.isMobile);
|
||||
const configs = computed(() => store.configs);
|
||||
const route = useRoute();
|
||||
const isInHomePage = computed(() => route.path === '/');
|
||||
const curl = computed(() => store.curl);
|
||||
const curlDomainsHadSet = computed(() => store.curlDomainsHadSet);
|
||||
const tabs = ['about', 'changelog', 'acknowledgement'];
|
||||
const content = ref('about');
|
||||
|
||||
const headingClass = 'mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground';
|
||||
const linkClass = 'inline-flex items-center gap-1 text-sm text-muted-foreground no-underline transition-colors hover:text-foreground cursor-pointer';
|
||||
// Static data from JSON — reverse once via computed so the template stays tidy.
|
||||
const changelogReversed = computed(() => changelogData.slice().reverse());
|
||||
|
||||
@@ -266,6 +390,12 @@ const openAboutTab = (tab, trackLabel) => {
|
||||
trackEvent('Footer', 'FooterClick', trackLabel);
|
||||
};
|
||||
|
||||
const isCurlOpen = ref(false);
|
||||
const openCurl = () => {
|
||||
isCurlOpen.value = true;
|
||||
trackEvent('Footer', 'FooterClick', 'Curl');
|
||||
};
|
||||
|
||||
watch(content, () => {
|
||||
nextTick(() => {
|
||||
if (sheetBody.value) sheetBody.value.scrollTop = 0;
|
||||
@@ -273,3 +403,20 @@ watch(content, () => {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.jn-curl::before {
|
||||
content: '$ ';
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 500;
|
||||
margin-right: 0.25rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.jn-comment::before {
|
||||
content: '// ';
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,6 @@
|
||||
<QueryIP ref="queryIPRef" />
|
||||
</FloatingDock>
|
||||
<HelpModal ref="helpModalRef" />
|
||||
<Additional />
|
||||
<Footer />
|
||||
</template>
|
||||
|
||||
@@ -28,7 +27,7 @@
|
||||
//
|
||||
// Components — the test sections and the always-visible chrome load
|
||||
// synchronously; everything the first paint can't show (dialogs, the
|
||||
// below-fold Additional/Footer) is an async component so its code stays
|
||||
// below-fold Footer) is an async component so its code stays
|
||||
// out of the route chunk and out of the mount's critical path. Their
|
||||
// template refs are null until the chunk lands — consumers (use-shortcuts)
|
||||
// must optional-chain.
|
||||
@@ -45,7 +44,6 @@ import FloatingDock from './widgets/FloatingDock.vue';
|
||||
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
|
||||
// Async (off-critical-path) components
|
||||
const Additional = defineAsyncComponent(() => import('./Additional.vue'));
|
||||
const Footer = defineAsyncComponent(() => import('./Footer.vue'));
|
||||
const Achievements = defineAsyncComponent(() => import('./Achievements.vue'));
|
||||
const QueryIP = defineAsyncComponent(() => import('./widgets/QueryIP.vue'));
|
||||
|
||||
@@ -384,11 +384,9 @@
|
||||
"Note2_1": "You can use",
|
||||
"Note2_2": "command, get the IP address of your machine in terminal",
|
||||
"Note3": "is an optional path, adding it will obtain the geographical information of the IP",
|
||||
"Note4": "is your IPCheck.ing API key",
|
||||
"getIPv4": "Get local machine's IPv4 address",
|
||||
"getIPv6": "Get local machine's IPv6 address",
|
||||
"get6and4": "Get the preferred network exit IP address of the local machine",
|
||||
"notAvailable": "This feature is not available yet"
|
||||
"get6and4": "Get the preferred network exit IP address of the local machine"
|
||||
},
|
||||
"browserinfo": {
|
||||
"Title": "Browser Information",
|
||||
@@ -2077,13 +2075,25 @@
|
||||
"helpModal": {
|
||||
"Title": "Keyboard Shortcuts"
|
||||
},
|
||||
"footer": {
|
||||
"Tagline": "A free, open-source, all-in-one privacy toolbox.",
|
||||
"Project": "Project",
|
||||
"Support": "Docs & Support",
|
||||
"DeveloperDocs": "Developer Docs",
|
||||
"SelfHosting": "Self-hosting Guide",
|
||||
"ReportIssue": "Report an Issue",
|
||||
"License": "MIT License",
|
||||
"Clients": "Clients",
|
||||
"Raycast": "Raycast Extension",
|
||||
"Lite": "Lite Edition"
|
||||
},
|
||||
"about": {
|
||||
"Title": "About",
|
||||
"project": [
|
||||
"IPCheck.ing is a free all-in-one IP toolbox that helps you check your own IP information, test website availability, examine DNS exit information, and run speed tests, global latency tests, MTR tests and more.",
|
||||
"This is an open-source project — click the GitHub icon at the bottom of the page to browse the source code.",
|
||||
"If you are unsure how a feature works, open the \"Help Center\" link at the bottom of the page. The documentation is thorough: beyond describing each feature, it also explains networking concepts and how to troubleshoot network problems.",
|
||||
"If you would like to take part in the open-source project, click the \"Contribute Code\" button at the bottom of the page to see how."
|
||||
"If you would like to take part in the open-source project, open the \"Developer Docs\" link at the bottom of the page to see how."
|
||||
],
|
||||
"founderTitle": "About the Founder",
|
||||
"founder": [
|
||||
@@ -2104,7 +2114,6 @@
|
||||
"Sponsor": "Sponsor Us",
|
||||
"Privacy": "Privacy",
|
||||
"HelpCenter": "Help Center",
|
||||
"Contribute": "Contribute Code",
|
||||
"Close": "Close"
|
||||
},
|
||||
"acknowledgement": {
|
||||
|
||||
@@ -384,11 +384,9 @@
|
||||
"Note2_1": "Tu peux, dans le terminal, via",
|
||||
"Note2_2": "commande, obtenir l'adresse IP de cette machine",
|
||||
"Note3": "est un chemin optionnel, son ajout permettra d'obtenir les informations géographiques de l'IP",
|
||||
"Note4": "est votre clé API IPCheck.ing",
|
||||
"getIPv4": "Obtenir l'adresse IPv4 de la machine locale",
|
||||
"getIPv6": "Obtenir l'adresse IPv6 de la machine locale",
|
||||
"get6and4": "Obtenir l'adresse IP de sortie de réseau préférée de la machine locale",
|
||||
"notAvailable": "Cette fonctionnalité n'est pas encore disponible"
|
||||
"get6and4": "Obtenir l'adresse IP de sortie de réseau préférée de la machine locale"
|
||||
},
|
||||
"browserinfo": {
|
||||
"Title": "Informations du Navigateur",
|
||||
@@ -2077,13 +2075,25 @@
|
||||
"helpModal": {
|
||||
"Title": "Raccourcis"
|
||||
},
|
||||
"footer": {
|
||||
"Tagline": "Une boîte à outils de confidentialité tout-en-un, gratuite et open source.",
|
||||
"Project": "Projet",
|
||||
"Support": "Documentation et support",
|
||||
"DeveloperDocs": "Documentation développeur",
|
||||
"SelfHosting": "Guide d'auto-hébergement",
|
||||
"ReportIssue": "Signaler un problème",
|
||||
"License": "Licence MIT",
|
||||
"Clients": "Clients",
|
||||
"Raycast": "Extension Raycast",
|
||||
"Lite": "Édition Lite"
|
||||
},
|
||||
"about": {
|
||||
"Title": "À propos",
|
||||
"project": [
|
||||
"IPCheck.ing est une boîte à outils IP tout-en-un et gratuite : elle vous aide à consulter vos informations IP, à tester la disponibilité des sites web, à examiner les informations de sortie DNS, à mesurer votre débit, à tester la latence mondiale, à lancer des tests MTR, et bien plus encore.",
|
||||
"Il s'agit d'un projet open source : cliquez sur l'icône GitHub en bas de la page pour consulter le code source.",
|
||||
"Si vous ne comprenez pas comment utiliser une fonctionnalité, ouvrez le « Centre d'aide » en bas de la page. Notre documentation est très détaillée : en plus de présenter chaque fonctionnalité, elle vous aide à comprendre les différents concepts réseau et à diagnostiquer vos problèmes de connexion.",
|
||||
"Si vous souhaitez participer au projet open source, cliquez sur le bouton « Contribuer au code » en bas de la page pour découvrir comment faire."
|
||||
"Si vous souhaitez participer au projet open source, ouvrez le lien « Documentation développeur » en bas de la page pour découvrir comment faire."
|
||||
],
|
||||
"founderTitle": "À propos du fondateur",
|
||||
"founder": [
|
||||
@@ -2104,7 +2114,6 @@
|
||||
"Sponsor": "Nous soutenir",
|
||||
"Privacy": "Confidentialité",
|
||||
"HelpCenter": "Centre d'aide",
|
||||
"Contribute": "Contribuer au code",
|
||||
"Close": "Fermer"
|
||||
},
|
||||
"acknowledgement": {
|
||||
|
||||
@@ -384,11 +384,9 @@
|
||||
"Note2_1": "Você pode usar o comando",
|
||||
"Note2_2": "para obter o IP da sua máquina no terminal",
|
||||
"Note3": "é um caminho opcional; adicioná-lo obtém as informações geográficas do IP",
|
||||
"Note4": "é sua chave de API do IPCheck.ing",
|
||||
"getIPv4": "Obter o endereço IPv4 da máquina local",
|
||||
"getIPv6": "Obter o endereço IPv6 da máquina local",
|
||||
"get6and4": "Obter o IP de saída de rede preferencial da máquina local",
|
||||
"notAvailable": "Este recurso ainda não está disponível"
|
||||
"get6and4": "Obter o IP de saída de rede preferencial da máquina local"
|
||||
},
|
||||
"browserinfo": {
|
||||
"Title": "Informações do navegador",
|
||||
@@ -2077,13 +2075,25 @@
|
||||
"helpModal": {
|
||||
"Title": "Atalhos de teclado"
|
||||
},
|
||||
"footer": {
|
||||
"Tagline": "Uma caixa de ferramentas de privacidade tudo em um, gratuita e de código aberto.",
|
||||
"Project": "Projeto",
|
||||
"Support": "Documentação e suporte",
|
||||
"DeveloperDocs": "Documentação para desenvolvedores",
|
||||
"SelfHosting": "Guia de auto-hospedagem",
|
||||
"ReportIssue": "Relatar um problema",
|
||||
"License": "Licença MIT",
|
||||
"Clients": "Clientes",
|
||||
"Raycast": "Extensão para Raycast",
|
||||
"Lite": "Edição Lite"
|
||||
},
|
||||
"about": {
|
||||
"Title": "Sobre",
|
||||
"project": [
|
||||
"IPCheck.ing é uma caixa de ferramentas de IP gratuita e tudo em um que ajuda você a verificar suas próprias informações de IP, testar disponibilidade de sites, examinar informações de saída DNS e executar testes de velocidade, latência global, MTR e muito mais.",
|
||||
"Este é um projeto de código aberto — clique no ícone do GitHub no rodapé para navegar pelo código-fonte.",
|
||||
"Se você não tiver certeza de como um recurso funciona, abra o link \"Central de ajuda\" no rodapé. A documentação é completa: além de descrever cada recurso, ela também explica conceitos de rede e como solucionar problemas de rede.",
|
||||
"Se quiser participar do projeto de código aberto, clique no botão \"Contribuir com código\" no rodapé para ver como."
|
||||
"Se quiser participar do projeto de código aberto, abra o link \"Documentação para desenvolvedores\" no rodapé para ver como."
|
||||
],
|
||||
"founderTitle": "Sobre o fundador",
|
||||
"founder": [
|
||||
@@ -2104,7 +2114,6 @@
|
||||
"Sponsor": "Patrocine-nos",
|
||||
"Privacy": "Privacidade",
|
||||
"HelpCenter": "Central de ajuda",
|
||||
"Contribute": "Contribuir com código",
|
||||
"Close": "Fechar"
|
||||
},
|
||||
"acknowledgement": {
|
||||
|
||||
@@ -384,11 +384,9 @@
|
||||
"Note2_1": "Можно использовать",
|
||||
"Note2_2": "команду, чтобы получить IP-адрес этого устройства в терминале",
|
||||
"Note3": "— необязательный путь; добавьте его, чтобы получить географические сведения об IP",
|
||||
"Note4": "— ваш ключ API IPCheck.ing",
|
||||
"getIPv4": "Получить IPv4-адрес этого устройства",
|
||||
"getIPv6": "Получить IPv6-адрес этого устройства",
|
||||
"get6and4": "Получить предпочтительный внешний IP-адрес этого устройства",
|
||||
"notAvailable": "Эта функция пока недоступна"
|
||||
"get6and4": "Получить предпочтительный внешний IP-адрес этого устройства"
|
||||
},
|
||||
"browserinfo": {
|
||||
"Title": "Сведения о браузере",
|
||||
@@ -2077,13 +2075,25 @@
|
||||
"helpModal": {
|
||||
"Title": "Сочетания клавиш"
|
||||
},
|
||||
"footer": {
|
||||
"Tagline": "Бесплатный универсальный набор инструментов для приватности с открытым исходным кодом.",
|
||||
"Project": "Проект",
|
||||
"Support": "Документация и поддержка",
|
||||
"DeveloperDocs": "Документация для разработчиков",
|
||||
"SelfHosting": "Руководство по самостоятельному размещению",
|
||||
"ReportIssue": "Сообщить о проблеме",
|
||||
"License": "Лицензия MIT",
|
||||
"Clients": "Клиенты",
|
||||
"Raycast": "Расширение для Raycast",
|
||||
"Lite": "Версия Lite"
|
||||
},
|
||||
"about": {
|
||||
"Title": "О проекте",
|
||||
"project": [
|
||||
"IPCheck.ing — бесплатный универсальный набор IP-инструментов: он помогает посмотреть сведения о своём IP, проверить доступность сайтов и выход DNS, измерить скорость соединения, задержку до серверов по всему миру, выполнить MTR-тест и многое другое.",
|
||||
"Это проект с открытым исходным кодом — нажмите значок GitHub внизу страницы, чтобы посмотреть исходный код.",
|
||||
"Если вам непонятно, как пользоваться той или иной функцией, откройте «Справочный центр» внизу страницы. Документация очень подробная: помимо описания функций, она помогает разобраться в сетевых понятиях и в том, как диагностировать проблемы с сетью.",
|
||||
"Если вы хотите поучаствовать в проекте с открытым исходным кодом, нажмите внизу страницы кнопку «Стать разработчиком» и посмотрите, как это сделать."
|
||||
"Если вы хотите поучаствовать в проекте с открытым исходным кодом, откройте внизу страницы ссылку «Документация для разработчиков» и посмотрите, как это сделать."
|
||||
],
|
||||
"founderTitle": "Об основателе проекта",
|
||||
"founder": [
|
||||
@@ -2104,7 +2114,6 @@
|
||||
"Sponsor": "Поддержать",
|
||||
"Privacy": "Приватность",
|
||||
"HelpCenter": "Справочный центр",
|
||||
"Contribute": "Стать разработчиком",
|
||||
"Close": "Закрыть"
|
||||
},
|
||||
"acknowledgement": {
|
||||
|
||||
@@ -384,11 +384,9 @@
|
||||
"Note2_1": "你可以在終端裡,透過",
|
||||
"Note2_2": "命令取得本機的 IP 位址",
|
||||
"Note3": "是可選路徑,新增後可取得到 IP 的地理資訊",
|
||||
"Note4": "請使用你申請的 IPCheck.ing API 金鑰",
|
||||
"getIPv4": "取得本機 IPv4 位址",
|
||||
"getIPv6": "取得本機 IPv6 位址",
|
||||
"get6and4": "取得本機優先網路出口的 IP 位址",
|
||||
"notAvailable": "此功能暫未開放,敬請期待"
|
||||
"get6and4": "取得本機優先網路出口的 IP 位址"
|
||||
},
|
||||
"browserinfo": {
|
||||
"Title": "瀏覽器資訊",
|
||||
@@ -2077,13 +2075,25 @@
|
||||
"helpModal": {
|
||||
"Title": "鍵盤快速鍵"
|
||||
},
|
||||
"footer": {
|
||||
"Tagline": "免費、開源、一站式的隱私工具箱",
|
||||
"Project": "專案",
|
||||
"Support": "文件與支援",
|
||||
"DeveloperDocs": "開發者文件",
|
||||
"SelfHosting": "自行架設指南",
|
||||
"ReportIssue": "回報問題",
|
||||
"License": "MIT 授權條款",
|
||||
"Clients": "用戶端",
|
||||
"Raycast": "Raycast 擴充功能",
|
||||
"Lite": "Lite 版"
|
||||
},
|
||||
"about": {
|
||||
"Title": "關於",
|
||||
"project": [
|
||||
"IPCheck.ing 是一個免費的多合一 IP 工具箱,可以協助你檢視自己的 IP 資訊,檢測網站的可用性,檢視 DNS 出口資訊,進行網速測試,全球延遲測試,MTR 測試等。",
|
||||
"這是一個開源專案,你可以點選頁面底部的 GitHub 圖示檢視原始碼。",
|
||||
"如果你對於產品中功能使用不理解,可以透過底部的「說明中心」取得協助,我們已經寫了非常詳細的協助文件,除了功能介紹,也能協助你理解各種網路概念和如何排查網路問題。",
|
||||
"如果你有興趣參與到開源專案中,可以點選頁面底部的「貢獻程式碼」按鈕,檢視如何貢獻程式碼。"
|
||||
"如果你有興趣參與到開源專案中,可以點選頁面底部的「開發者文件」,檢視如何貢獻程式碼。"
|
||||
],
|
||||
"founderTitle": "關於專案發起人",
|
||||
"founder": [
|
||||
@@ -2104,7 +2114,6 @@
|
||||
"Sponsor": "贊助我們",
|
||||
"Privacy": "隱私政策",
|
||||
"HelpCenter": "說明中心",
|
||||
"Contribute": "貢獻程式碼",
|
||||
"Close": "關閉"
|
||||
},
|
||||
"acknowledgement": {
|
||||
|
||||
@@ -384,11 +384,9 @@
|
||||
"Note2_1": "你可以在终端里,通过",
|
||||
"Note2_2": "命令获取本机的 IP 地址",
|
||||
"Note3": "是可选路径,添加后可获取到 IP 的地理信息",
|
||||
"Note4": "请使用你申请的 IPCheck.ing API 密钥",
|
||||
"getIPv4": "获取本机 IPv4 地址",
|
||||
"getIPv6": "获取本机 IPv6 地址",
|
||||
"get6and4": "获取本机优先网络出口的 IP 地址",
|
||||
"notAvailable": "此功能暂未开放,敬请期待"
|
||||
"get6and4": "获取本机优先网络出口的 IP 地址"
|
||||
},
|
||||
"browserinfo": {
|
||||
"Title": "浏览器信息",
|
||||
@@ -2077,13 +2075,25 @@
|
||||
"helpModal": {
|
||||
"Title": "快捷键"
|
||||
},
|
||||
"footer": {
|
||||
"Tagline": "免费、开源、一站式的隐私工具箱",
|
||||
"Project": "项目",
|
||||
"Support": "文档与支持",
|
||||
"DeveloperDocs": "开发者文档",
|
||||
"SelfHosting": "自托管指南",
|
||||
"ReportIssue": "报告问题",
|
||||
"License": "MIT 许可证",
|
||||
"Clients": "客户端",
|
||||
"Raycast": "Raycast 扩展",
|
||||
"Lite": "Lite 版"
|
||||
},
|
||||
"about": {
|
||||
"Title": "关于",
|
||||
"project": [
|
||||
"IPCheck.ing 是一个免费的多合一 IP 工具箱,可以帮助你查看自己的 IP 信息,检测网站的可用性,查看 DNS 出口信息,进行网速测试,全球延迟测试,MTR 测试等。",
|
||||
"这是一个开源项目,你可以点击页面底部的 GitHub 图标查看源代码。",
|
||||
"如果你对于产品中功能使用不理解,可以通过底部的「帮助中心」获取帮助,我们已经写了非常详细的帮助文档,除了功能介绍,也能帮助你理解各种网络概念和如何排查网络问题。",
|
||||
"如果你有兴趣参与到开源项目中,可以点击页面底部的「贡献代码」按钮,查看如何贡献代码。"
|
||||
"如果你有兴趣参与到开源项目中,可以点击页面底部的「开发者文档」,查看如何贡献代码。"
|
||||
],
|
||||
"founderTitle": "关于项目发起人",
|
||||
"founder": [
|
||||
@@ -2104,7 +2114,6 @@
|
||||
"Sponsor": "赞助我们",
|
||||
"Privacy": "隐私政策",
|
||||
"HelpCenter": "帮助中心",
|
||||
"Contribute": "贡献代码",
|
||||
"Close": "关闭"
|
||||
},
|
||||
"acknowledgement": {
|
||||
|
||||
@@ -218,17 +218,6 @@
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 100pt;
|
||||
padding-top: 20pt;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
footer {
|
||||
height: 140pt;
|
||||
}
|
||||
}
|
||||
|
||||
.body-dark-mode {
|
||||
background-color: rgb(19, 21, 21) !important;
|
||||
color: #e3e3e3 !important;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Builds a docs.ipcheck.ing link: English lives at the section root, every
|
||||
// other locale under `/<section>/<locale>/`, with `path` appended after that.
|
||||
|
||||
const DOCS_ORIGIN = 'https://docs.ipcheck.ing';
|
||||
|
||||
const trimSlashes = (value) => String(value ?? '').replace(/^\/+|\/+$/g, '');
|
||||
|
||||
export const docsUrl = (section, locale, path = '') => {
|
||||
const base = `${DOCS_ORIGIN}/${trimSlashes(section)}/`;
|
||||
const lang = !locale || locale === 'en' ? '' : `${trimSlashes(locale)}/`;
|
||||
return base + lang + String(path ?? '').replace(/^\/+/, '');
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,36 @@
|
||||
// docsUrl() — locale-aware links into docs.ipcheck.ing.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import { docsUrl } from '../frontend/utils/docs-url.js';
|
||||
|
||||
describe('docsUrl()', () => {
|
||||
it('puts English at the section root', () => {
|
||||
assert.equal(docsUrl('knowledge-base', 'en'), 'https://docs.ipcheck.ing/knowledge-base/');
|
||||
assert.equal(docsUrl('developer', 'en'), 'https://docs.ipcheck.ing/developer/');
|
||||
});
|
||||
|
||||
it('puts any other locale under the section', () => {
|
||||
assert.equal(docsUrl('knowledge-base', 'zh'), 'https://docs.ipcheck.ing/knowledge-base/zh/');
|
||||
assert.equal(docsUrl('developer', 'pt-BR'), 'https://docs.ipcheck.ing/developer/pt-BR/');
|
||||
});
|
||||
|
||||
it('appends the path after the locale segment', () => {
|
||||
assert.equal(docsUrl('developer', 'en', 'getting-started/'), 'https://docs.ipcheck.ing/developer/getting-started/');
|
||||
assert.equal(docsUrl('developer', 'zh', 'getting-started/'), 'https://docs.ipcheck.ing/developer/zh/getting-started/');
|
||||
});
|
||||
|
||||
it('never produces a double slash', () => {
|
||||
const urls = [
|
||||
docsUrl('/developer/', 'zh', '/getting-started/'),
|
||||
docsUrl('developer', 'en', '/getting-started/'),
|
||||
docsUrl('developer/', 'fr'),
|
||||
];
|
||||
for (const url of urls) assert.doesNotMatch(url.replace('https://', ''), /\/\//);
|
||||
assert.equal(urls[0], 'https://docs.ipcheck.ing/developer/zh/getting-started/');
|
||||
});
|
||||
|
||||
it('treats a missing locale as English', () => {
|
||||
assert.equal(docsUrl('developer', undefined), 'https://docs.ipcheck.ing/developer/');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user