diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue new file mode 100644 index 000000000..a32de01d9 --- /dev/null +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -0,0 +1,224 @@ +<template> +<div v-if="meta" :class="$style.root"> + <div :class="$style.main"> + <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/> + <button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button> + <div :class="$style.mainFg"> + <h1 :class="$style.mainTitle"> + <!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に --> + <!-- <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> --> + <span>{{ instanceName }}</span> + </h1> + <div :class="$style.mainAbout"> + <!-- eslint-disable-next-line vue/no-v-html --> + <div v-html="meta.description || i18n.ts.headlineMisskey"></div> + </div> + <div v-if="instance.disableRegistration" :class="$style.mainWarn"> + <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> + </div> + <div class="_gaps_s" :class="$style.mainActions"> + <MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton> + <MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton> + <MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton> + </div> + </div> + </div> + <div v-if="stats" :class="$style.stats"> + <div :class="$style.statsItem"> + <div :class="$style.statsItemLabel">{{ i18n.ts.users }}</div> + <div :class="$style.statsItemCount"><MkNumber :value="stats.originalUsersCount"/></div> + </div> + <div :class="$style.statsItem"> + <div :class="$style.statsItemLabel">{{ i18n.ts.notes }}</div> + <div :class="$style.statsItemCount"><MkNumber :value="stats.originalNotesCount"/></div> + </div> + </div> + <div v-if="instance.policies.ltlAvailable" :class="$style.tl"> + <div :class="$style.tlHeader">{{ i18n.ts.letsLookAtTimeline }}</div> + <div :class="$style.tlBody"> + <MkTimeline src="local"/> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import { Instance } from 'misskey-js/built/entities'; +import XTimeline from './welcome.timeline.vue'; +import XSigninDialog from '@/components/MkSigninDialog.vue'; +import XSignupDialog from '@/components/MkSignupDialog.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkTimeline from '@/components/MkTimeline.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import { instanceName } from '@/config'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { instance } from '@/instance'; +import number from '@/filters/number'; +import MkNumber from '@/components/MkNumber.vue'; + +let meta = $ref<Instance>(); +let stats = $ref(null); + +os.api('meta', { detail: true }).then(_meta => { + meta = _meta; +}); + +os.api('stats', { +}).then((res) => { + stats = res; +}); + +function signin() { + os.popup(XSigninDialog, { + autoSet: true, + }, {}, 'closed'); +} + +function signup() { + os.popup(XSignupDialog, { + autoSet: true, + }, {}, 'closed'); +} + +function showMenu(ev) { + os.popupMenu([{ + text: i18n.ts.instanceInfo, + icon: 'ti ti-info-circle', + action: () => { + os.pageWindow('/about'); + }, + }, { + text: i18n.ts.aboutMisskey, + icon: 'ti ti-info-circle', + action: () => { + os.pageWindow('/about-misskey'); + }, + }, null, { + text: i18n.ts.help, + icon: 'ti ti-help-circle', + action: () => { + window.open('https://misskey-hub.net/help.md', '_blank'); + }, + }], ev.currentTarget ?? ev.target); +} + +function exploreOtherServers() { + // TODO: 言語をよしなに + window.open('https://join.misskey.page/ja-JP/instances', '_blank'); +} +</script> + +<style lang="scss" module> +.root { + position: relative; + display: flex; + flex-direction: column; + gap: 16px; + padding: 32px 0 0 0; +} + +.main { + position: relative; + background: var(--panel); + border-radius: var(--radius); + box-shadow: 0 12px 32px rgb(0 0 0 / 25%); + text-align: center; +} + +.mainIcon { + width: 85px; + margin-top: -47px; + border-radius: 100%; + vertical-align: bottom; +} + +.mainMenu { + position: absolute; + top: 16px; + right: 16px; + width: 32px; + height: 32px; + border-radius: 8px; + font-size: 18px; +} + +.mainFg { + position: relative; + z-index: 1; +} + +.mainTitle { + display: block; + margin: 0; + padding: 16px 32px 24px 32px; + font-size: 1.4em; +} + +.mainLogo { + vertical-align: bottom; + max-height: 120px; + max-width: min(100%, 300px); +} + +.mainAbout { + padding: 0 32px; +} + +.mainWarn { + padding: 32px 32px 0 32px; +} + +.mainActions { + padding: 32px; +} + +.mainAction { + line-height: 28px; +} + +.stats { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 16px; +} + +.statsItem { + position: relative; + background: var(--panel); + border-radius: var(--radius); + overflow: clip; + box-shadow: 0 12px 32px rgb(0 0 0 / 25%); + padding: 16px; +} + +.statsItemLabel { + color: var(--fgTransparentWeak); + font-size: 0.9em; +} + +.statsItemCount { + font-weight: bold; + font-size: 1.2em; + color: var(--accent); +} + +.tl { + position: relative; + background: var(--panel); + border-radius: var(--radius); + overflow: clip; + box-shadow: 0 12px 32px rgb(0 0 0 / 25%); +} + +.tlHeader { + padding: 12px 16px; + border-bottom: solid 1px var(--divider); +} + +.tlBody { + height: 350px; + overflow: auto; +} +</style> diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 4e716b9b3..929152bd5 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -13,35 +13,7 @@ <MkEmoji :normal="true" :no-style="true" emoji="🍮"/> </div> <div class="contents"> - <div class="main"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> - <button class="_button _acrylic menu" @click="showMenu"><i class="ti ti-dots"></i></button> - <div class="fg"> - <h1> - <!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に --> - <!-- <img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> --> - <span class="text">{{ instanceName }}</span> - </h1> - <div class="about"> - <!-- eslint-disable-next-line vue/no-v-html --> - <div class="desc" v-html="meta.description || i18n.ts.headlineMisskey"></div> - </div> - <div v-if="instance.disableRegistration" class="warn"> - <MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> - </div> - <div class="action _gaps_s"> - <MkButton full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton> - <MkButton full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton> - <MkButton full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton> - </div> - </div> - </div> - <div v-if="instance.policies.ltlAvailable" class="tl"> - <div class="title">{{ i18n.ts.letsLookAtTimeline }}</div> - <div class="body"> - <MkTimeline src="local"/> - </div> - </div> + <MkVisitorDashboard/> </div> <div v-if="instances && instances.length > 0" class="federation"> <MarqueeText :duration="40"> @@ -60,16 +32,15 @@ import { } from 'vue'; import { Instance } from 'misskey-js/built/entities'; import XTimeline from './welcome.timeline.vue'; import MarqueeText from '@/components/MkMarquee.vue'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import MkButton from '@/components/MkButton.vue'; import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue'; -import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instanceName } from '@/config'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; +import number from '@/filters/number'; +import MkNumber from '@/components/MkNumber.vue'; +import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; let meta = $ref<Instance>(); let instances = $ref<any[]>(); @@ -84,45 +55,6 @@ os.apiGet('federation/instances', { }).then(_instances => { instances = _instances; }); - -function signin() { - os.popup(XSigninDialog, { - autoSet: true, - }, {}, 'closed'); -} - -function signup() { - os.popup(XSignupDialog, { - autoSet: true, - }, {}, 'closed'); -} - -function showMenu(ev) { - os.popupMenu([{ - text: i18n.ts.instanceInfo, - icon: 'ti ti-info-circle', - action: () => { - os.pageWindow('/about'); - }, - }, { - text: i18n.ts.aboutMisskey, - icon: 'ti ti-info-circle', - action: () => { - os.pageWindow('/about-misskey'); - }, - }, null, { - text: i18n.ts.help, - icon: 'ti ti-help-circle', - action: () => { - window.open('https://misskey-hub.net/help.md', '_blank'); - }, - }], ev.currentTarget ?? ev.target); -} - -function exploreOtherServers() { - // TODO: 言語をよしなに - window.open('https://join.misskey.page/ja-JP/instances', '_blank'); -} </script> <style lang="scss" scoped> @@ -202,89 +134,11 @@ function exploreOtherServers() { position: relative; width: min(430px, calc(100% - 32px)); margin-left: 128px; - padding: 150px 0 100px 0; + padding: 100px 0 100px 0; @media (max-width: 1200px) { margin: auto; } - - > .main { - position: relative; - background: var(--panel); - border-radius: var(--radius); - box-shadow: 0 12px 32px rgb(0 0 0 / 25%); - text-align: center; - - > .icon { - width: 85px; - margin-top: -47px; - border-radius: 100%; - vertical-align: bottom; - } - - > .menu { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - border-radius: 8px; - font-size: 18px; - } - - > .fg { - position: relative; - z-index: 1; - - > h1 { - display: block; - margin: 0; - padding: 16px 32px 24px 32px; - font-size: 1.4em; - - > .logo { - vertical-align: bottom; - max-height: 120px; - max-width: min(100%, 300px); - } - } - - > .about { - padding: 0 32px; - } - - > .warn { - padding: 32px 32px 0 32px; - } - - > .action { - padding: 32px; - - > * { - line-height: 28px; - } - } - } - } - - > .tl { - position: relative; - background: var(--panel); - border-radius: var(--radius); - overflow: clip; - box-shadow: 0 12px 32px rgb(0 0 0 / 25%); - margin-top: 16px; - - > .title { - padding: 12px 16px; - border-bottom: solid 1px var(--divider); - } - - > .body { - height: 350px; - overflow: auto; - } - } } > .federation { diff --git a/packages/frontend/src/ui/visitor/header.vue b/packages/frontend/src/ui/visitor.header.vue similarity index 100% rename from packages/frontend/src/ui/visitor/header.vue rename to packages/frontend/src/ui/visitor.header.vue diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 6c96440eb..46520353a 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -1,19 +1,283 @@ <template> -<DesignB/> +<div class="mk-app"> + <a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> + + <div v-if="!narrow && !root" class="side"> + <div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div> + <div class="dashboard"> + <MkVisitorDashboard/> + </div> + </div> + + <div class="main"> + <XKanban v-if="narrow && !root" class="banner" :powered-by="root"/> + + <div class="contents"> + <XHeader v-if="!root" class="header"/> + <main v-if="!root" style="container-type: inline-size;"> + <RouterView/> + </main> + <main v-else> + <RouterView/> + </main> + <div v-if="!root" class="powered-by"> + <b><MkA to="/">{{ host }}</MkA></b> + <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> + </div> + </div> + </div> + + <Transition :name="'tray-back'"> + <div + v-if="showMenu" + class="menu-back _modalBg" + @click="showMenu = false" + @touchstart.passive="showMenu = false" + ></div> + </Transition> + + <Transition :name="'tray'"> + <div v-if="showMenu" class="menu"> + <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA> + <MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ i18n.ts.timeline }}</MkA> + <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA> + <MkA to="/announcements" class="link" active-class="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA> + <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA> + <div class="divider"></div> + <MkA to="/pages" class="link" active-class="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA> + <MkA to="/play" class="link" active-class="active"><i class="ti ti-player-play icon"></i>Play</MkA> + <MkA to="/gallery" class="link" active-class="active"><i class="ti ti-icons icon"></i>{{ i18n.ts.gallery }}</MkA> + <div class="action"> + <button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button> + <button class="_button" @click="signin()">{{ i18n.ts.login }}</button> + </div> + </div> + </Transition> +</div> <XCommon/> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -//import DesignA from './visitor/a.vue'; -import DesignB from './visitor/b.vue'; +<script lang="ts" setup> +import { ComputedRef, onMounted, provide } from 'vue'; import XCommon from './_common_/common.vue'; +import XHeader from './visitor.header.vue'; +import { host, instanceName } from '@/config'; +import * as os from '@/os'; +import { instance } from '@/instance'; +import XSigninDialog from '@/components/MkSigninDialog.vue'; +import XSignupDialog from '@/components/MkSignupDialog.vue'; +import { ColdDeviceStorage, defaultStore } from '@/store'; +import { mainRouter } from '@/router'; +import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; +import { i18n } from '@/i18n'; +import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue'; -export default defineComponent({ - components: { - XCommon, - //DesignA, - DesignB, - }, +const DESKTOP_THRESHOLD = 1100; + +let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); + +provide('router', mainRouter); +provideMetadataReceiver((info) => { + pageMetadata = info; + if (pageMetadata.value) { + document.title = `${pageMetadata.value.title} | ${instanceName}`; + } +}); + +const announcements = { + endpoint: 'announcements', + limit: 10, +}; + +const isTimelineAvailable = $ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable); + +let showMenu = $ref(false); +let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); +let narrow = $ref(window.innerWidth < 1280); +let meta = $ref(); + +const keymap = $computed(() => { + return { + 'd': () => { + if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': () => { + mainRouter.push('/search'); + }, + }; +}); + +const root = $computed(() => mainRouter.currentRoute.value.name === 'index'); + +os.api('meta', { detail: true }).then(res => { + meta = res; +}); + +function signin() { + os.popup(XSigninDialog, { + autoSet: true, + }, {}, 'closed'); +} + +function signup() { + os.popup(XSignupDialog, { + autoSet: true, + }, {}, 'closed'); +} + +onMounted(() => { + if (!isDesktop) { + window.addEventListener('resize', () => { + if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop = true; + }, { passive: true }); + } +}); + +defineExpose({ + showMenu: $$(showMenu), }); </script> + +<style> +.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}} +</style> + +<style lang="scss" scoped> +.tray-enter-active, +.tray-leave-active { + opacity: 1; + transform: translateX(0); + transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.tray-enter-from, +.tray-leave-active { + opacity: 0; + transform: translateX(-240px); +} + +.tray-back-enter-active, +.tray-back-leave-active { + opacity: 1; + transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); +} +.tray-back-enter-from, +.tray-back-leave-active { + opacity: 0; +} + +.mk-app { + display: flex; + min-height: 100vh; + + > .side { + position: sticky; + top: 0; + left: 0; + width: 500px; + height: 100vh; + background: var(--accent); + + > .banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + aspect-ratio: 1.5; + background-position: center; + background-size: cover; + -webkit-mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent); + mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent); + } + + > .dashboard { + position: relative; + padding: 32px; + box-sizing: border-box; + max-height: 100%; + overflow: auto; + } + } + + > .main { + flex: 1; + min-width: 0; + + > .banner { + } + + > .contents { + position: relative; + z-index: 1; + + > .powered-by { + padding: 28px; + font-size: 14px; + text-align: center; + border-top: 1px solid var(--divider); + + > small { + display: block; + margin-top: 8px; + opacity: 0.5; + } + } + } + } + + > .menu-back { + position: fixed; + z-index: 1001; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + } + + > .menu { + position: fixed; + z-index: 1001; + top: 0; + left: 0; + width: 240px; + height: 100vh; + background: var(--panel); + + > .link { + display: block; + padding: 16px; + + > .icon { + margin-right: 1em; + } + } + + > .divider { + margin: 8px auto; + width: calc(100% - 32px); + border-top: solid 0.5px var(--divider); + } + + > .action { + padding: 16px; + + > button { + display: block; + width: 100%; + padding: 10px; + box-sizing: border-box; + text-align: center; + border-radius: 999px; + + &._button { + background: var(--panel); + } + + &:first-child { + margin-bottom: 16px; + } + } + } + } +} +</style> diff --git a/packages/frontend/src/ui/visitor/a.vue b/packages/frontend/src/ui/visitor/a.vue deleted file mode 100644 index 476103607..000000000 --- a/packages/frontend/src/ui/visitor/a.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<div class="mk-app"> - <div v-if="mainRouter.currentRoute?.name === 'index'" class="banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> - <div> - <h1 v-if="meta"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1> - <div v-if="meta" class="about"> - <!-- eslint-disable-next-line vue/no-v-html --> - <div class="desc" v-html="meta.description || i18n.ts.introMisskey"></div> - </div> - <div class="action"> - <button class="_button primary" @click="signup()">{{ i18n.ts.signup }}</button> - <button class="_button" @click="signin()">{{ i18n.ts.login }}</button> - </div> - </div> - </div> - <div v-else class="banner-mini" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> - <div> - <h1 v-if="meta"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1> - </div> - </div> - - <div class="main"> - <div ref="contents" class="contents" :class="{ wallpaper }"> - <header v-show="mainRouter.currentRoute?.name !== 'index'" ref="header" class="header"> - <XHeader :info="pageInfo"/> - </header> - <main ref="main" style="container-type: inline-size;"> - <RouterView/> - </main> - <div class="powered-by"> - <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> - </div> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import XHeader from './header.vue'; -import { host, instanceName } from '@/config'; -import * as os from '@/os'; -import MkButton from '@/components/MkButton.vue'; -import { defaultStore, ColdDeviceStorage } from '@/store'; -import { mainRouter } from '@/router'; -import { instance } from '@/instance'; -import { i18n } from '@/i18n'; - -const DESKTOP_THRESHOLD = 1100; - -export default defineComponent({ - components: { - XHeader, - MkButton, - }, - - data() { - return { - host, - instanceName, - pageInfo: null, - meta: null, - narrow: window.innerWidth < 1280, - announcements: { - endpoint: 'announcements', - limit: 10, - }, - mainRouter, - isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, - defaultStore, - instance, - i18n, - }; - }, - - computed: { - keymap(): any { - return { - 'd': () => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; - this.defaultStore.set('darkMode', !this.defaultStore.state.darkMode); - }, - 's': () => { - mainRouter.push('/search'); - }, - 'h|/': this.help, - }; - }, - }, - - created() { - document.documentElement.style.overflowY = 'scroll'; - - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - }, - - mounted() { - if (!this.isDesktop) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true; - }, { passive: true }); - } - }, - - methods: { - // @ThatOneCalculator: Are these methods even used? - // I can't find references to them anywhere else in the code... - - // setParallax(el) { - // new simpleParallax(el); - // }, - - changePage(page) { - if (page == null) return; - // eslint-disable-next-line no-undef - if (page[symbols.PAGE_INFO]) { - // eslint-disable-next-line no-undef - this.pageInfo = page[symbols.PAGE_INFO]; - } - }, - - top() { - window.scroll({ top: 0, behavior: 'smooth' }); - }, - - help() { - window.open('https://misskey-hub.net/docs/keyboard-shortcut.md', '_blank'); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.mk-app { - min-height: 100vh; - - > .banner { - position: relative; - width: 100%; - text-align: center; - background-position: center; - background-size: cover; - - > div { - height: 100%; - background: rgba(0, 0, 0, 0.3); - - * { - color: #fff; - } - - > h1 { - margin: 0; - padding: 96px 32px 0 32px; - text-shadow: 0 0 8px black; - - > .logo { - vertical-align: bottom; - max-height: 150px; - } - } - - > .about { - padding: 32px; - max-width: 580px; - margin: 0 auto; - box-sizing: border-box; - text-shadow: 0 0 8px black; - } - - > .action { - padding-bottom: 64px; - - > button { - display: inline-block; - padding: 10px 20px; - box-sizing: border-box; - text-align: center; - border-radius: 999px; - background: var(--panel); - color: var(--fg); - - &.primary { - background: var(--accent); - color: #fff; - } - - &:first-child { - margin-right: 16px; - } - } - } - } - } - - > .banner-mini { - position: relative; - width: 100%; - text-align: center; - background-position: center; - background-size: cover; - - > div { - position: relative; - z-index: 1; - height: 100%; - background: rgba(0, 0, 0, 0.3); - - * { - color: #fff !important; - } - - > header { - - } - - > h1 { - margin: 0; - padding: 32px; - text-shadow: 0 0 8px black; - - > .logo { - vertical-align: bottom; - max-height: 100px; - } - } - } - } - - > .main { - > .contents { - position: relative; - z-index: 1; - - > .header { - position: sticky; - top: 0; - left: 0; - z-index: 1000; - } - - > .powered-by { - padding: 28px; - font-size: 14px; - text-align: center; - border-top: 1px solid var(--divider); - - > small { - display: block; - margin-top: 8px; - opacity: 0.5; - } - } - } - } -} -</style> - -<style lang="scss"> -</style> diff --git a/packages/frontend/src/ui/visitor/b.vue b/packages/frontend/src/ui/visitor/b.vue deleted file mode 100644 index 5287a670c..000000000 --- a/packages/frontend/src/ui/visitor/b.vue +++ /dev/null @@ -1,266 +0,0 @@ -<template> -<div class="mk-app"> - <a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> - - <div v-if="!narrow && !root" class="side"> - <XKanban class="kanban" full/> - </div> - - <div class="main"> - <XKanban v-if="narrow && !root" class="banner" :powered-by="root"/> - - <div class="contents"> - <XHeader v-if="!root" class="header"/> - <main v-if="!root" style="container-type: inline-size;"> - <RouterView/> - </main> - <main v-else> - <RouterView/> - </main> - <div v-if="!root" class="powered-by"> - <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> - </div> - </div> - </div> - - <Transition :name="'tray-back'"> - <div - v-if="showMenu" - class="menu-back _modalBg" - @click="showMenu = false" - @touchstart.passive="showMenu = false" - ></div> - </Transition> - - <Transition :name="'tray'"> - <div v-if="showMenu" class="menu"> - <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA> - <MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA> - <MkA to="/announcements" class="link" active-class="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA> - <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA> - <div class="divider"></div> - <MkA to="/pages" class="link" active-class="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA> - <MkA to="/play" class="link" active-class="active"><i class="ti ti-player-play icon"></i>Play</MkA> - <MkA to="/gallery" class="link" active-class="active"><i class="ti ti-icons icon"></i>{{ i18n.ts.gallery }}</MkA> - <div class="action"> - <button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button> - <button class="_button" @click="signin()">{{ i18n.ts.login }}</button> - </div> - </div> - </Transition> -</div> -</template> - -<script lang="ts" setup> -import { ComputedRef, onMounted, provide } from 'vue'; -import XHeader from './header.vue'; -import XKanban from './kanban.vue'; -import { host, instanceName } from '@/config'; -import * as os from '@/os'; -import { instance } from '@/instance'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import { ColdDeviceStorage, defaultStore } from '@/store'; -import { mainRouter } from '@/router'; -import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; -import { i18n } from '@/i18n'; - -const DESKTOP_THRESHOLD = 1100; - -let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); - -provide('router', mainRouter); -provideMetadataReceiver((info) => { - pageMetadata = info; - if (pageMetadata.value) { - document.title = `${pageMetadata.value.title} | ${instanceName}`; - } -}); - -const announcements = { - endpoint: 'announcements', - limit: 10, -}; - -const isTimelineAvailable = $ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable); - -let showMenu = $ref(false); -let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); -let narrow = $ref(window.innerWidth < 1280); -let meta = $ref(); - -const keymap = $computed(() => { - return { - 'd': () => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) return; - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': () => { - mainRouter.push('/search'); - }, - }; -}); - -const root = $computed(() => mainRouter.currentRoute.value.name === 'index'); - -os.api('meta', { detail: true }).then(res => { - meta = res; -}); - -function signin() { - os.popup(XSigninDialog, { - autoSet: true, - }, {}, 'closed'); -} - -function signup() { - os.popup(XSignupDialog, { - autoSet: true, - }, {}, 'closed'); -} - -onMounted(() => { - if (!isDesktop) { - window.addEventListener('resize', () => { - if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop = true; - }, { passive: true }); - } -}); - -defineExpose({ - showMenu: $$(showMenu), -}); -</script> - -<style> -.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}} -</style> - -<style lang="scss" scoped> -.tray-enter-active, -.tray-leave-active { - opacity: 1; - transform: translateX(0); - transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-enter-from, -.tray-leave-active { - opacity: 0; - transform: translateX(-240px); -} - -.tray-back-enter-active, -.tray-back-leave-active { - opacity: 1; - transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); -} -.tray-back-enter-from, -.tray-back-leave-active { - opacity: 0; -} - -.mk-app { - display: flex; - min-height: 100vh; - background-position: center; - background-size: cover; - background-attachment: fixed; - - > .side { - width: 500px; - height: 100vh; - - > .kanban { - position: fixed; - top: 0; - left: 0; - width: 500px; - height: 100vh; - overflow: auto; - } - } - - > .main { - flex: 1; - min-width: 0; - - > .banner { - } - - > .contents { - position: relative; - z-index: 1; - - > .powered-by { - padding: 28px; - font-size: 14px; - text-align: center; - border-top: 1px solid var(--divider); - - > small { - display: block; - margin-top: 8px; - opacity: 0.5; - } - } - } - } - - > .menu-back { - position: fixed; - z-index: 1001; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - } - - > .menu { - position: fixed; - z-index: 1001; - top: 0; - left: 0; - width: 240px; - height: 100vh; - background: var(--panel); - - > .link { - display: block; - padding: 16px; - - > .icon { - margin-right: 1em; - } - } - - > .divider { - margin: 8px auto; - width: calc(100% - 32px); - border-top: solid 0.5px var(--divider); - } - - > .action { - padding: 16px; - - > button { - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border-radius: 999px; - - &._button { - background: var(--panel); - } - - &:first-child { - margin-bottom: 16px; - } - } - } - } -} -</style> diff --git a/packages/frontend/src/ui/visitor/kanban.vue b/packages/frontend/src/ui/visitor/kanban.vue deleted file mode 100644 index ce7fcfe94..000000000 --- a/packages/frontend/src/ui/visitor/kanban.vue +++ /dev/null @@ -1,261 +0,0 @@ -<!-- eslint-disable vue/no-v-html --> -<template> -<div class="rwqkcmrc" :style="{ backgroundImage: transparent ? 'none' : `url(${ instance.backgroundImageUrl })` }"> - <div class="back" :class="{ transparent }"></div> - <div class="contents"> - <div class="wrapper"> - <h1 v-if="meta" :class="{ full }"> - <MkA to="/" class="link"><img v-if="meta.logoImageUrl" class="logo" :src="meta.logoImageUrl" alt="logo"><span v-else class="text">{{ instanceName }}</span></MkA> - </h1> - <template v-if="full"> - <div v-if="meta" class="about"> - <div class="desc" v-html="meta.description || i18n.ts.introMisskey"></div> - </div> - <div class="action"> - <button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button> - <button class="_button" @click="signin()">{{ i18n.ts.login }}</button> - </div> - <div class="announcements panel"> - <header>{{ i18n.ts.announcements }}</header> - <MkPagination v-slot="{items}" :pagination="announcements" class="list"> - <section v-for="announcement in items" :key="announcement.id" class="item"> - <div class="title">{{ announcement.title }}</div> - <div class="content"> - <Mfm :text="announcement.text"/> - <img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt="announcement image"/> - </div> - </section> - </MkPagination> - </div> - <div v-if="poweredBy" class="powered-by"> - <b><MkA to="/">{{ host }}</MkA></b> - <small>Powered by <a href="https://github.com/misskey-dev/misskey" target="_blank">Misskey</a></small> - </div> - </template> - </div> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; -import { host, instanceName } from '@/config'; -import * as os from '@/os'; -import MkPagination from '@/components/MkPagination.vue'; -import XSigninDialog from '@/components/MkSigninDialog.vue'; -import XSignupDialog from '@/components/MkSignupDialog.vue'; -import MkButton from '@/components/MkButton.vue'; -import { instance } from '@/instance'; -import { i18n } from '@/i18n'; - -export default defineComponent({ - components: { - MkPagination, - MkButton, - }, - - props: { - full: { - type: Boolean, - required: false, - default: false, - }, - transparent: { - type: Boolean, - required: false, - default: false, - }, - poweredBy: { - type: Boolean, - required: false, - default: false, - }, - }, - - data() { - return { - host, - instanceName, - pageInfo: null, - meta: null, - narrow: window.innerWidth < 1280, - announcements: { - endpoint: 'announcements', - limit: 10, - }, - instance, - i18n, - }; - }, - - created() { - os.api('meta', { detail: true }).then(meta => { - this.meta = meta; - }); - }, - - methods: { - signin() { - os.popup(XSigninDialog, { - autoSet: true, - }, {}, 'closed'); - }, - - signup() { - os.popup(XSignupDialog, { - autoSet: true, - }, {}, 'closed'); - }, - }, -}); -</script> - -<style lang="scss" scoped> -.rwqkcmrc { - position: relative; - text-align: center; - background-position: center; - background-size: cover; - // TODO: パララックスにしたい - - > .back { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.3); - - &.transparent { - -webkit-backdrop-filter: var(--blur, blur(12px)); - backdrop-filter: var(--blur, blur(12px)); - } - } - - > .contents { - position: relative; - z-index: 1; - height: inherit; - overflow: auto; - - > .wrapper { - max-width: 380px; - padding: 0 16px; - box-sizing: border-box; - margin: 0 auto; - - > .panel { - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(8px)); - background: rgba(0, 0, 0, 0.5); - border-radius: var(--radius); - - &, * { - color: #fff !important; - } - } - - > h1 { - display: block; - margin: 0; - padding: 32px 0 32px 0; - color: #fff; - - &.full { - padding: 64px 0 0 0; - - > .link { - > ::v-deep(.logo) { - max-height: 130px; - } - } - } - - > .link { - display: block; - - > ::v-deep(.logo) { - vertical-align: bottom; - max-height: 100px; - } - } - } - - > .about { - display: block; - margin: 24px 0; - text-align: center; - box-sizing: border-box; - text-shadow: 0 0 8px black; - color: #fff; - } - - > .action { - > button { - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border-radius: 999px; - - &._button { - background: var(--panel); - } - - &:first-child { - margin-bottom: 16px; - } - } - } - - > .announcements { - margin: 32px 0; - text-align: left; - - > header { - padding: 12px 16px; - border-bottom: solid 1px rgba(255, 255, 255, 0.5); - } - - > .list { - max-height: 300px; - overflow: auto; - - > .item { - padding: 12px 16px; - - & + .item { - border-top: solid 1px rgba(255, 255, 255, 0.5); - } - - > .title { - font-weight: bold; - } - - > .content { - > img { - max-width: 100%; - } - } - } - } - } - - > .powered-by { - padding: 28px; - font-size: 14px; - text-align: center; - border-top: 1px solid rgba(255, 255, 255, 0.5); - color: #fff; - - > small { - display: block; - margin-top: 8px; - opacity: 0.5; - } - } - } - } -} -</style>