Unified Content Discovery

This commit is contained in:
Crimekillz 2024-05-01 17:08:18 +02:00
parent 6f57bce3aa
commit 77a9a49ca1
16 changed files with 367 additions and 362 deletions

View File

@ -278,6 +278,8 @@ uploadFromUrlRequested: "Upload angefordert"
uploadFromUrlMayTakeTime: "Es kann einige Zeit dauern, bis das Hochladen abgeschlossen uploadFromUrlMayTakeTime: "Es kann einige Zeit dauern, bis das Hochladen abgeschlossen
ist." ist."
explore: "Erkunden" explore: "Erkunden"
discover: "Entdecken"
reel: "Reel"
messageRead: "Gelesen" messageRead: "Gelesen"
noMoreHistory: "Es gibt keine weitere Historie" noMoreHistory: "Es gibt keine weitere Historie"
startMessaging: "Einen neuen Chat beginnen" startMessaging: "Einen neuen Chat beginnen"

View File

@ -302,6 +302,8 @@ uploadFromUrlDescription: "URL of the file you want to upload"
uploadFromUrlRequested: "Upload requested" uploadFromUrlRequested: "Upload requested"
uploadFromUrlMayTakeTime: "It may take some time until the upload is complete." uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
explore: "Explore" explore: "Explore"
discover: "Discover"
reel: "Reel"
messageRead: "Read" messageRead: "Read"
noMoreHistory: "There is no further history" noMoreHistory: "There is no further history"
startMessaging: "Start a new chat" startMessaging: "Start a new chat"

View File

@ -264,6 +264,8 @@ uploadFromUrlDescription: "アップロードしたいファイルのURL"
uploadFromUrlRequested: "アップロードをリクエストしました" uploadFromUrlRequested: "アップロードをリクエストしました"
uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。" uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
explore: "みつける" explore: "みつける"
discover: "Discover"
reel: "Reel"
messageRead: "既読" messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません" noMoreHistory: "これより過去の履歴はありません"
startMessaging: "チャットを開始" startMessaging: "チャットを開始"

View File

@ -210,7 +210,7 @@ import { reactive, computed } from "vue";
import XSettings from "@/pages/settings/profile.vue"; import XSettings from "@/pages/settings/profile.vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import XFeaturedUsers from "@/pages/explore.users.vue"; import XFeaturedUsers from "@/pages/discover.users.vue";
import XPostForm from "@/components/MkPostForm.vue"; import XPostForm from "@/components/MkPostForm.vue";
import MkSparkle from "@/components/MkSparkle.vue"; import MkSparkle from "@/components/MkSparkle.vue";
import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowButton.vue"; import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowButton.vue";

View File

@ -38,15 +38,10 @@ export const navbarItemDef = reactive({
indicated: computed(() => $i?.hasPendingReceivedFollowRequest), indicated: computed(() => $i?.hasPendingReceivedFollowRequest),
to: "/my/follow-requests", to: "/my/follow-requests",
}, },
explore: { discover: {
title: "explore", title: "discover",
icon: "ph-hash ph-bold ph-lg", icon: "ph-lightning ph-bold ph-lg",
to: "/explore", to: "/discover",
},
search: {
title: "search",
icon: "ph-magnifying-glass ph-bold ph-lg",
to: "/search",
}, },
lists: { lists: {
title: "lists", title: "lists",
@ -81,7 +76,7 @@ export const navbarItemDef = reactive({
}, },
gallery: { gallery: {
title: "gallery", title: "gallery",
icon: "ph-image-square ph-bold ph-lg", icon: "ph-film-strip ph-bold ph-lg",
to: "/gallery", to: "/gallery",
}, },
channels: { channels: {

View File

@ -0,0 +1,342 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
/></template>
<div class="lznhrdub">
<MkSpacer :content-max="1200">
<MkSearch :query="searchQuery" :hideFilters="!$i || tab !== 'featured'" @query="search"/>
<swiper
:round-lengths="true"
:touch-angle="25"
:threshold="10"
:centeredSlides="true"
:modules="[Virtual]"
:space-between="20"
:virtual="true"
:allow-touch-move="
defaultStore.state.swipeOnMobile &&
(deviceKind !== 'desktop' ||
defaultStore.state.swipeOnDesktop)
"
@swiper="setSwiperRef"
@slide-change="onSlideChange"
>
<swiper-slide>
<template v-if="searchQuery == null || searchQuery.trim().length < 1">
<XUsers />
</template>
<template v-else-if="tabs[swiperRef!.activeIndex] == 'users'">
<XUserList
ref="users"
class="_gap"
:pagination="usersSearchPagination"
/>
</template>
</swiper-slide>
<swiper-slide>
<template v-if="$i">
<template v-if="searchQuery == null || searchQuery.trim().length < 1">
<XFeatured />
</template>
<template v-else-if="tabs[swiperRef!.activeIndex] == 'notes'">
<XNotes ref="notes" :pagination="notesSearchPagination" />
</template>
</template>
<template v-else>
<XFeatured />
</template>
</swiper-slide>
<swiper-slide>
<div class="_content grwlizim featured">
<MkChannelList
key="featured"
:pagination="chanTrendPagination"
/>
</div>
</swiper-slide>
<swiper-slide>
<div class="rknalgpo">
<MkPagination
v-slot="{ items }"
:pagination="pagesTrendPagination"
>
<MkPagePreview
v-for="page in items"
:key="page.id"
class="ckltabjg"
:page="page"
/>
</MkPagination>
</div>
</swiper-slide>
<swiper-slide>
<MkFolder class="_gap">
<template #header
><i class="ph-clock ph-bold ph-lg"></i>
{{ i18n.ts.recentPosts }}</template
>
<MkPagination
v-slot="{ items }"
:pagination="recentReelsPagination"
:disable-auto-load="true"
>
<div class="vfpdbgtk">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</MkFolder>
<MkFolder class="_gap">
<template #header
><i class="ph-fire-simple ph-bold ph-lg"></i>
{{ i18n.ts.popularPosts }}</template
>
<MkPagination
v-slot="{ items }"
:pagination="popularReelsPagination"
:disable-auto-load="true"
>
<div class="vfpdbgtk">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</MkFolder>
</swiper-slide>
</swiper>
</MkSpacer>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch, onMounted, onActivated } from "vue";
import { Virtual } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/vue";
import MkChannelPreview from "@/components/MkChannelPreview.vue";
import MkChannelList from "@/components/MkChannelList.vue";
import MkPagePreview from "@/components/MkPagePreview.vue";
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
import MkPagination from "@/components/MkPagination.vue";
import XFeatured from "./discover.featured.vue";
import XUsers from "./discover.users.vue";
import { definePageMetadata } from "@/scripts/page-metadata";
import { deviceKind } from "@/scripts/device-kind";
import { i18n } from "@/i18n";
import { $i } from "@/account";
import MkSearch from "@/components/MkSearch.vue";
import XNotes from "@/components/MkNotes.vue";
import XUserList from "@/components/MkUserList.vue";
import { defaultStore } from "@/store";
import "swiper/scss";
import "swiper/scss/virtual";
import { useRouter } from "@/router.js";
const router = useRouter();
const tabs = ["users", "featured", "channels", "pages", "reel"];
let tab = $ref(tabs[0]);
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
const getUrlParams = () =>
window.location.search
.substring(1)
.split("&")
.reduce((result, query) => {
const [k, v] = query.split("=");
result[k] = decodeURIComponent(v?.replace('+', '%20'));
return result;
}, {});
let searchQuery = $ref<string>(getUrlParams()['q'] ?? "");
let channel = $ref<string|undefined>(getUrlParams()['channel'] ?? undefined);
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [
{
key: "users",
icon: "ph-users ph-bold ph-lg",
title: i18n.ts.users,
},
{
key: "featured",
icon: "ph-fire ph-bold ph-lg",
title: i18n.ts.notes,
},
{
key: "channels",
icon: "ph-megaphone-simple ph-bold ph-lg",
title: i18n.ts.channel,
},
{
key: "pages",
icon: "ph-file-text ph-bold ph-lg",
title: i18n.ts.pages,
},
{
key: "reel",
icon: "ph-film-strip ph-bold ph-lg",
title: i18n.ts.reel,
},
]);
const chanTrendPagination = {
endpoint: "channels/featured" as const,
limit: 10,
noPaging: false,
};
const pagesTrendPagination = {
endpoint: "pages/featured" as const,
limit: 10,
};
const recentReelsPagination = {
endpoint: "gallery/posts" as const,
limit: 6,
};
const popularReelsPagination = {
endpoint: "gallery/featured" as const,
limit: 5,
};
const notesSearchPagination = {
endpoint: "notes/search" as const,
limit: 10,
params: computed(() => ({
query: searchQuery,
channelId: channel,
})),
};
const usersSearchPagination = {
endpoint: "users/search" as const,
limit: 10,
params: computed(() => ({
query: searchQuery,
origin: "combined",
})),
};
definePageMetadata(
computed(() => ({
title: i18n.ts.discover,
icon: "ph-lightning ph-bold ph-lg",
})),
);
let swiperRef = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(tabs.indexOf(tab));
}
function onSlideChange() {
tab = tabs[swiperRef.activeIndex];
}
function syncSlide(index) {
swiperRef.slideTo(index);
}
onMounted(() => {
syncSlide(tabs.indexOf(swiperRef.activeIndex));
});
onActivated(() => {
searchQuery = getUrlParams()['q'];
channel = getUrlParams()['channel'] ?? undefined;
syncSlide(tabs.indexOf(tab));
});
async function search(query: string) {
const q = query.trim();
if (q.startsWith("@") && !q.includes(" ")) {
router.push(`/${q}`);
return;
}
if (q.startsWith("#")) {
router.push(`/tags/${encodeURIComponent(q.slice(1))}`);
return;
}
if (q.startsWith("http://") || q.startsWith("https://")) {
const promise = os.api("ap/show", {
uri: q,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === "User") {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === "Note") {
router.push(`/notes/${res.object.id}`);
}
return;
}
searchQuery = q;
router.push(`/discover?q=${encodeURIComponent(q)}`);
}
</script>
<style lang="scss" scoped>
.buttoncontainer {
display: grid;
justify-content: center;
margin-bottom: 1rem;
}
.vfpdbgtk {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: 0 var(--margin);
> .post {
}
}
.rknalgpo {
> .buttoncontainer {
display: grid;
justify-content: center;
margin-bottom: 1rem;
}
&.my .ckltabjg:first-child {
margin-top: 16px;
}
.ckltabjg:not(:last-child) {
margin-bottom: 8px;
}
@media (min-width: 500px) {
.ckltabjg:not(:last-child) {
margin-bottom: 16px;
}
}
}
</style>

View File

@ -1,96 +0,0 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
/></template>
<div class="lznhrdub">
<MkSpacer :content-max="1200">
<swiper
:round-lengths="true"
:touch-angle="25"
:threshold="10"
:centeredSlides="true"
:modules="[Virtual]"
:space-between="20"
:virtual="true"
:allow-touch-move="
defaultStore.state.swipeOnMobile &&
(deviceKind !== 'desktop' ||
defaultStore.state.swipeOnDesktop)
"
@swiper="setSwiperRef"
@slide-change="onSlideChange"
>
<swiper-slide>
<XUsers />
</swiper-slide>
<swiper-slide>
<XFeatured />
</swiper-slide>
</swiper>
</MkSpacer>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch, onMounted } from "vue";
import { Virtual } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/vue";
import XFeatured from "./explore.featured.vue";
import XUsers from "./explore.users.vue";
import { definePageMetadata } from "@/scripts/page-metadata";
import { deviceKind } from "@/scripts/device-kind";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import "swiper/scss";
import "swiper/scss/virtual";
const tabs = ["users", "featured"];
let tab = $ref(tabs[0]);
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [
{
key: "users",
icon: "ph-users ph-bold ph-lg",
title: i18n.ts.users,
},
{
key: "featured",
icon: "ph-lightning ph-bold ph-lg",
title: i18n.ts.featured,
},
]);
definePageMetadata(
computed(() => ({
title: i18n.ts.explore,
icon: "ph-hash ph-bold ph-lg",
})),
);
let swiperRef = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(tabs.indexOf(tab));
}
function onSlideChange() {
tab = tabs[swiperRef.activeIndex];
}
function syncSlide(index) {
swiperRef.slideTo(index);
}
onMounted(() => {
syncSlide(tabs.indexOf(swiperRef.activeIndex));
});
</script>

View File

@ -1,7 +1,7 @@
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header <template #header
><MkPageHeader :actions="headerActions" :tabs="headerTabs" ><MkPageHeader :actions="headerActions" :tabs="headerTabs" :display-back-button="true"
/></template> /></template>
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
<FormSuspense :p="init" class="_formRoot"> <FormSuspense :p="init" class="_formRoot">

View File

@ -1,7 +1,7 @@
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header <template #header
><MkPageHeader :actions="headerActions" :tabs="headerTabs" ><MkPageHeader :actions="headerActions" :tabs="headerTabs" :display-back-button="true"
/></template> /></template>
<MkSpacer :content-max="1000" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="1000" :margin-min="16" :margin-max="32">
<div class="_root"> <div class="_root">

View File

@ -1,228 +0,0 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
/></template>
<MkSpacer :content-max="800">
<MkSearch :query="searchQuery" :hideFilters="!$i || tab === 'users'" @query="search"/>
<swiper
:round-lengths="true"
:touch-angle="25"
:threshold="10"
:centeredSlides="true"
:modules="[Virtual]"
:space-between="20"
:virtual="true"
:allow-touch-move="
defaultStore.state.swipeOnMobile &&
(deviceKind !== 'desktop' ||
defaultStore.state.swipeOnDesktop)
"
@swiper="setSwiperRef"
@slide-change="onSlideChange"
>
<swiper-slide>
<template v-if="$i">
<template v-if="searchQuery == null || searchQuery.trim().length < 1">
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
<div class="_fullinfo" ref="notes">
<img
:src="instance.images.info"
class="_ghost"
alt="Info"
/>
<div>
{{ i18n.ts.searchEmptyQuery }}
</div>
</div>
</transition>
</template>
<template v-else-if="tabs[swiperRef!.activeIndex] == 'notes'">
<XNotes ref="notes" :pagination="notesPagination" />
</template>
</template>
<template v-else>
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
<div class="_fullinfo" ref="notes">
<img
:src="instance.images.info"
class="_ghost"
alt="Info"
/>
<div>
{{ i18n.ts.searchNotLoggedIn_1 }}<br>
{{ i18n.ts.searchNotLoggedIn_2 }}
</div>
</div>
</transition>
</template>
</swiper-slide>
<swiper-slide>
<template v-if="searchQuery == null || searchQuery.trim().length < 1">
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
<div class="_fullinfo" ref="notes">
<img
:src="instance.images.info"
class="_ghost"
alt="Info"
/>
<div>
{{ i18n.ts.searchEmptyQuery }}
</div>
</div>
</transition>
</template>
<template v-else-if="tabs[swiperRef!.activeIndex] == 'users'">
<XUserList
ref="users"
class="_gap"
:pagination="usersPagination"
/>
</template>
</swiper-slide>
</swiper>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch, onMounted, onActivated, ref } from "vue";
import { Virtual } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/vue";
import XNotes from "@/components/MkNotes.vue";
import XUserList from "@/components/MkUserList.vue";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind";
import { $i } from "@/account";
import "swiper/scss";
import "swiper/scss/virtual";
import {instance} from "@/instance";
import MkSearch from "@/components/MkSearch.vue";
import { useRouter } from "@/router.js";
import * as os from "@/os.js";
const router = useRouter();
const getUrlParams = () =>
window.location.search
.substring(1)
.split("&")
.reduce((result, query) => {
const [k, v] = query.split("=");
result[k] = decodeURIComponent(v?.replace('+', '%20'));
return result;
}, {});
let searchQuery = $ref<string>(getUrlParams()['q'] ?? "");
let channel = $ref<string|undefined>(getUrlParams()['channel'] ?? undefined);
const notesPagination = {
endpoint: "notes/search" as const,
limit: 10,
params: computed(() => ({
query: searchQuery,
channelId: channel,
})),
};
const usersPagination = {
endpoint: "users/search" as const,
limit: 10,
params: computed(() => ({
query: searchQuery,
origin: "combined",
})),
};
const tabs = ["notes", "users"];
let tab = $ref(tabs[0]);
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
const headerActions = $computed(() => []);
const headerTabs = $computed(() => [
{
key: "notes",
icon: "ph-magnifying-glass ph-bold ph-lg",
title: i18n.ts.notes,
},
{
key: "users",
icon: "ph-users ph-bold ph-lg",
title: i18n.ts.users,
},
]);
let swiperRef = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(tabs.indexOf(tab));
}
function onSlideChange() {
tab = tabs[swiperRef.activeIndex];
}
function syncSlide(index) {
swiperRef.slideTo(index);
}
onMounted(() => {
syncSlide(tabs.indexOf(tab));
});
onActivated(() => {
searchQuery = getUrlParams()['q'];
channel = getUrlParams()['channel'] ?? undefined;
syncSlide(tabs.indexOf(tab));
});
definePageMetadata(
computed(() => ({
title: i18n.ts.search,
icon: "ph-magnifying-glass ph-bold ph-lg",
})),
);
async function search(query: string) {
const q = query.trim();
if (q.startsWith("@") && !q.includes(" ")) {
router.push(`/${q}`);
return;
}
if (q.startsWith("#")) {
router.push(`/tags/${encodeURIComponent(q.slice(1))}`);
return;
}
if (q.startsWith("http://") || q.startsWith("https://")) {
const promise = os.api("ap/show", {
uri: q,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
if (res.type === "User") {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === "Note") {
router.push(`/notes/${res.object.id}`);
}
return;
}
searchQuery = q;
router.push(`/search?q=${encodeURIComponent(q)}`);
}
</script>

View File

@ -337,16 +337,12 @@ export const routes = [
loginRequired: true, loginRequired: true,
}, },
{ {
path: "/explore/tags/:tag", path: "/discover/tags/:tag",
component: page(() => import("./pages/explore.vue")), component: page(() => import("./pages/discover.vue")),
}, },
{ {
path: "/explore", path: "/discover",
component: page(() => import("./pages/explore.vue")), component: page(() => import("./pages/discover.vue")),
},
{
path: "/search",
component: page(() => import("./pages/search.vue")),
}, },
{ {
path: "/authorize-follow", path: "/authorize-follow",

View File

@ -15,8 +15,7 @@ const menuOptions = [
"-", "-",
"snippets", "snippets",
"antennas", "antennas",
"explore", "discover",
"search",
"-", "-",
"achievements", "achievements",
"drive" "drive"
@ -153,7 +152,6 @@ export const defaultStore = markRaw(
arg: null, arg: null,
}, },
}, },
overridedDeviceKind: { overridedDeviceKind: {
where: "device", where: "device",
default: null as null | "smartphone" | "tablet" | "desktop", default: null as null | "smartphone" | "tablet" | "desktop",

View File

@ -41,9 +41,9 @@
><i class="ph-house ph-bold ph-lg icon"></i ><i class="ph-house ph-bold ph-lg icon"></i
>{{ i18n.ts.home }}</MkA >{{ i18n.ts.home }}</MkA
> >
<MkA to="/explore" class="link" active-class="active" <MkA to="/discover" class="link" active-class="active"
><i class="ph-compass ph-bold ph-lg icon"></i ><i class="ph-lightning ph-bold ph-lg icon"></i
>{{ i18n.ts.explore }}</MkA >{{ i18n.ts.discover }}</MkA
> >
<MkA to="/pages" class="link" active-class="active" <MkA to="/pages" class="link" active-class="active"
><i class="ph-file-text ph-bold ph-lg icon"></i ><i class="ph-file-text ph-bold ph-lg icon"></i
@ -53,10 +53,6 @@
><i class="ph-image-square ph-bold ph-lg icon"></i ><i class="ph-image-square ph-bold ph-lg icon"></i
>{{ i18n.ts.gallery }}</MkA >{{ i18n.ts.gallery }}</MkA
> >
<MkA to="/search" class="link" active-class="active">
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
><span>{{ i18n.ts.search }}</span>
</MkA>
<MkA to="/settings" class="link" active-class="active"> <MkA to="/settings" class="link" active-class="active">
<i class="ph-gear-six ph-bold ph-lg icon"></i <i class="ph-gear-six ph-bold ph-lg icon"></i
><span>{{ i18n.ts.settings }}</span> ><span>{{ i18n.ts.settings }}</span>

View File

@ -14,9 +14,9 @@
><i class="ph-chats-circle ph-bold ph-lg icon"></i ><i class="ph-chats-circle ph-bold ph-lg icon"></i
>{{ i18n.ts.timeline }}</MkA >{{ i18n.ts.timeline }}</MkA
> --> > -->
<MkA to="/explore" class="link" active-class="active" <MkA to="/discover" class="link" active-class="active"
><i class="ph-compass ph-bold ph-lg icon"></i ><i class="ph-lightning ph-bold ph-lg icon"></i
>{{ i18n.ts.explore }}</MkA >{{ i18n.ts.discover }}</MkA
> >
<MkA to="/pages" class="link" active-class="active" <MkA to="/pages" class="link" active-class="active"
><i class="ph-file-text ph-bold ph-lg icon"></i ><i class="ph-file-text ph-bold ph-lg icon"></i
@ -26,10 +26,6 @@
><i class="ph-image-square ph-bold ph-lg icon"></i ><i class="ph-image-square ph-bold ph-lg icon"></i
>{{ i18n.ts.gallery }}</MkA >{{ i18n.ts.gallery }}</MkA
> >
<MkA to="/search" class="link" active-class="active">
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
><span>{{ i18n.ts.search }}</span>
</MkA>
<div v-if="info" class="page active link"> <div v-if="info" class="page active link">
<div class="title"> <div class="title">
<i v-if="info.icon" class="icon" :class="info.icon"></i> <i v-if="info.icon" class="icon" :class="info.icon"></i>