Files
lynq-chat/packages/frontend/src/Layout.vue
T

278 lines
5.6 KiB
Vue
Executable File

<template>
<Progress
v-show="routerStatus.isLoad"
class="router-progress"
:size="6"
/>
<div class="modals-container" />
<main class="layout">
<div class="left-menu">
<RouterLink
to="/"
:class='$route.path === "/"
? "isActive"
: ""'
title="ホーム"
>
<img :src='"icon" in serverInfo
? serverInfo.icon
: "/assets/lynqchat.svg"'
/>
</RouterLink>
<RouterLink
v-for="community of communitys"
:to="`/community/${community.id}`"
:class='$route.path.startsWith(`/community/${community.id}`)
? "isActive"
: ""'
:title="community.name"
>
<img
v-if="community.icon"
:src="community.icon"
/>
<Icon
v-else
icon="material-symbols:groups-rounded"
/>
</RouterLink>
</div>
<div class="content-main">
<div class="content-header">
{{
title
?? (serverInfo.success
? serverInfo.name
: null)
?? "LynqChat"
}}
</div>
<div
class="route-main"
:class='isFullRoute
? "full-route"
: ""'
>
<RouterView />
</div>
</div>
</main>
</template>
<style scoped>
main.layout {
display: flex;
width: 100dvw;
height: 100dvh;
padding: 0.5rem;
gap: 0.5rem;
box-sizing: border-box;
}
.left-menu {
display: flex;
flex-shrink: 0;
flex-direction: column;
padding: 0.5rem 0;
height: 100%;
overflow: scroll;
gap: 0.5rem;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
.left-menu a {
display: block;
position: relative;
z-index: 0;
border-radius: 0.5rem;
width: 3rem;
height: 3rem;
padding: 0.5rem;
transition: background-color 200ms ease-out;
}
.left-menu a * {
font-size: 3rem;
padding: 0.25rem;
border-radius: 0.5rem;
width: 3rem;
height: 3rem;
color: var(--text-color);
display: block;
overflow: hidden;
object-fit: contain;
box-sizing: border-box;
}
.left-menu a::before {
content: "";
display: block;
position: absolute;
top: 50%;
left: 0.3rem;
transform: translateY(-50%);
width: 0.25rem;
height: 70%;
border-radius: 0.5rem;
background-color: transparent;
transition: all 200ms ease-out;
}
.left-menu a.isActive::before {
background-color: var(--text-color);
}
.left-menu a:hover,
.left-menu a.isActive {
background-color: var(--border-color);
}
.content-main {
border: 1px solid var(--border-color);
background-color: var(--route-bg-color);
border-radius: 0.5rem;
overflow: hidden;
flex-grow: 1;
}
.content-header {
padding: 1rem 1.25rem;
font-size: 1.3rem;
font-weight: bold;
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
border-bottom: 1px solid var(--border-color);
box-shadow: 0px 1px 10px var(--border-color);
user-select: none;
-webkit-user-select: none;
}
.route-main {
display: flex;
flex-direction: column;
padding: 1.25rem;
padding-bottom: 0;
overflow: scroll;
height: 100%;
}
.content-main .route-main.full-route {
padding: 0;
overflow: hidden;
}
.router-progress {
position: fixed;
inset: 0.5rem 0.5rem 0 auto;
z-index: 9999;
}
.modals-container {
z-index: 9998;
position: relative;
}
@media (max-width: 40rem) {
.layout {
padding: 0 !important;
}
.left-menu {
display: none;
}
.content-main {
border: none;
border-radius: 0;
}
}
</style>
<script lang="ts" setup>
import { RouterView, RouterLink, useRouter, useRoute } from "vue-router";
import routerStatus, { title } from "@/lib/router";
import { Icon } from "@iconify/vue";
import Progress from "@/components/Progress.vue";
import { communitys, serverInfo } from "@/lib/account";
import { computed, onBeforeUnmount, onMounted, watch } from "vue";
import { createModal } from "@/lib/modal";
import ErrorModal from "@/components/Modal/Error.vue";
const router = useRouter();
const route = useRoute();
const isFullRoute = computed(() => route.meta.isFullRoute === true);
watch(route, (to, from) => {
if (
to.matched[0]?.path === from.matched[0]?.path &&
from.meta.canReloadTitle === false
) {
routerStatus.isLoad = false;
return false;
}
if (typeof route.meta.title === "string")
title.value = route.meta.title;
});
if (!serverInfo.value.success) {
throw new Error("サーバー情報の取得に失敗しました。");
}
if (!serverInfo.value.isInitialized) {
router.replace("/setup/initialization");
}
if (!serverInfo.value.isFirstAdminExists) {
router.replace("/setup/create-admin");
}
function handleError(event: ErrorEvent | PromiseRejectionEvent) {
let content = event instanceof PromiseRejectionEvent
? event.reason
: event;
if (content instanceof Error) {
content = content.message;
}
createModal({
component: ErrorModal,
props: {
error: content ?? "不明なエラー",
canClose: false,
},
});
}
onMounted(async () => {
window.addEventListener("error", handleError);
window.addEventListener("unhandledrejection", handleError);
if ("serviceWorker" in navigator) {
const swFile = import.meta.env.MODE === "production"
? "/sw.js"
: "/dev-sw.js?dev-sw";
navigator.serviceWorker.register(swFile, {
type: import.meta.env.MODE === "production"
? "classic"
: "module",
scope: "/",
});
}
});
onBeforeUnmount(() => {
window.removeEventListener("error", handleError);
window.removeEventListener("unhandledrejection", handleError);
});
</script>