init
This commit is contained in:
348
client/src/App.vue
Normal file
348
client/src/App.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template lang="pug">
|
||||
v-app.zen-app
|
||||
v-navigation-drawer(
|
||||
v-model="drawer"
|
||||
temporary
|
||||
location="right"
|
||||
color="#1e1e24"
|
||||
class="border-none"
|
||||
width="280"
|
||||
)
|
||||
.d-flex.flex-column.h-100.pa-4
|
||||
.d-flex.align-center.mb-6.px-2
|
||||
img.mr-3(:src="logo" height="32" width="32" alt="Logo")
|
||||
span.text-h6.font-weight-bold {{ $t('nav.menu') }}
|
||||
|
||||
v-list.pa-0(nav bg-color="transparent")
|
||||
v-list-item.mb-2(
|
||||
to="/"
|
||||
rounded="lg"
|
||||
:active="$route.path === '/'"
|
||||
)
|
||||
template(v-slot:prepend)
|
||||
v-icon(color="#00cec9" icon="mdi-view-dashboard")
|
||||
v-list-item-title.font-weight-bold {{ $t('nav.dashboard') }}
|
||||
|
||||
v-list-item.mb-2(
|
||||
to="/collection"
|
||||
rounded="lg"
|
||||
:active="$route.path === '/collection'"
|
||||
)
|
||||
template(v-slot:prepend)
|
||||
v-icon(color="#00cec9" icon="mdi-bookshelf")
|
||||
v-list-item-title.font-weight-bold {{ $t('nav.collection') }}
|
||||
|
||||
v-divider.my-4.border-subtle
|
||||
|
||||
.d-flex.flex-column.gap-2
|
||||
v-btn.justify-start.px-4(
|
||||
variant="text"
|
||||
block
|
||||
color="grey-lighten-1"
|
||||
@click="showSettings = true; drawer = false"
|
||||
)
|
||||
v-icon(start icon="mdi-cog")
|
||||
| {{ $t('nav.settings') }}
|
||||
|
||||
v-btn.justify-start.px-4(
|
||||
variant="text"
|
||||
block
|
||||
color="grey-lighten-1"
|
||||
:loading="syncing"
|
||||
@click="manualSync"
|
||||
)
|
||||
v-icon(start icon="mdi-sync")
|
||||
| {{ $t('nav.sync') }}
|
||||
|
||||
v-btn.justify-start.px-4.text-red-lighten-2(
|
||||
variant="text"
|
||||
block
|
||||
@click="handleLogout"
|
||||
)
|
||||
v-icon(start icon="mdi-logout")
|
||||
| {{ $t('nav.logout') }}
|
||||
|
||||
v-app-bar.px-2.app-bar-blur(
|
||||
flat
|
||||
color="rgba(30, 30, 36, 0.8)"
|
||||
border="b"
|
||||
)
|
||||
v-app-bar-title.font-weight-bold.text-h6(style="min-width: fit-content;")
|
||||
.d-flex.align-center.cursor-pointer.logo-hover(@click="$router.push('/')")
|
||||
img(:src="logo" height="32" width="32" alt="Zen Kanji Logo")
|
||||
span.ml-3.tracking-tight Zen Kanji
|
||||
|
||||
v-spacer
|
||||
|
||||
template(v-if="store.token")
|
||||
.d-none.d-md-flex.align-center
|
||||
v-btn.mx-1(
|
||||
to="/"
|
||||
:active="false"
|
||||
variant="text"
|
||||
:color="$route.path === '/' ? '#00cec9' : 'grey'"
|
||||
) {{ $t('nav.dashboard') }}
|
||||
|
||||
v-btn.mx-1(
|
||||
to="/collection"
|
||||
:active="false"
|
||||
variant="text"
|
||||
:color="$route.path === '/collection' ? '#00cec9' : 'grey'"
|
||||
) {{ $t('nav.collection') }}
|
||||
|
||||
v-divider.mx-2.my-auto(vertical length="20" color="grey-darken-2")
|
||||
|
||||
v-tooltip(:text="$t('nav.settings')" location="bottom")
|
||||
template(v-slot:activator="{ props }")
|
||||
v-btn(
|
||||
v-bind="props"
|
||||
icon="mdi-cog"
|
||||
variant="text"
|
||||
color="grey-lighten-1"
|
||||
@click="showSettings = true"
|
||||
)
|
||||
|
||||
v-tooltip(:text="$t('nav.sync')" location="bottom")
|
||||
template(v-slot:activator="{ props }")
|
||||
v-btn(
|
||||
v-bind="props"
|
||||
icon="mdi-sync"
|
||||
variant="text"
|
||||
:loading="syncing"
|
||||
color="grey-lighten-1"
|
||||
@click="manualSync"
|
||||
)
|
||||
|
||||
v-tooltip(:text="$t('nav.logout')" location="bottom")
|
||||
template(v-slot:activator="{ props }")
|
||||
v-btn(
|
||||
v-bind="props"
|
||||
icon="mdi-logout"
|
||||
variant="text"
|
||||
color="grey-darken-1"
|
||||
@click="handleLogout"
|
||||
)
|
||||
|
||||
.d-flex.d-md-none
|
||||
v-btn(
|
||||
icon="mdi-menu"
|
||||
variant="text"
|
||||
color="grey-lighten-1"
|
||||
@click="drawer = !drawer"
|
||||
)
|
||||
|
||||
v-main
|
||||
.fill-height.d-flex.align-center.justify-center.px-4(v-if="!store.token")
|
||||
v-card.pa-6.rounded-lg.elevation-10.text-center.border-subtle(
|
||||
color="#1e1e24"
|
||||
width="100%"
|
||||
max-width="400"
|
||||
)
|
||||
img.mb-4(:src="logo" height="64" width="64")
|
||||
|
||||
h1.text-h5.font-weight-bold.mb-2 {{ $t('hero.welcome') }}
|
||||
p.text-grey-lighten-1.text-body-2.mb-6 {{ $t('login.instruction') }}
|
||||
|
||||
v-text-field.mb-2(
|
||||
v-model="inputKey"
|
||||
:placeholder="$t('login.placeholder')"
|
||||
variant="solo-filled"
|
||||
bg-color="#2f3542"
|
||||
color="white"
|
||||
hide-details
|
||||
density="comfortable"
|
||||
@keyup.enter="handleLogin"
|
||||
)
|
||||
|
||||
v-alert.mb-4.text-left.text-caption(
|
||||
v-if="errorMsg"
|
||||
type="error"
|
||||
density="compact"
|
||||
variant="tonal"
|
||||
) {{ errorMsg }}
|
||||
|
||||
v-btn.text-black.font-weight-bold.mt-4(
|
||||
block
|
||||
color="#00cec9"
|
||||
height="44"
|
||||
:loading="loggingIn"
|
||||
@click="handleLogin"
|
||||
) {{ $t('login.button') }}
|
||||
|
||||
router-view(v-else)
|
||||
|
||||
v-dialog(v-model="showSettings" max-width="340")
|
||||
v-card.rounded-xl.pa-4.border-subtle(color="#1e1e24")
|
||||
v-card-title.text-center.font-weight-bold {{ $t('settings.title') }}
|
||||
|
||||
v-card-text.pt-4
|
||||
.text-caption.text-grey.mb-2 {{ $t('settings.batchSize') }}
|
||||
v-slider(
|
||||
v-model="tempBatchSize"
|
||||
:min="5"
|
||||
:max="100"
|
||||
:step="5"
|
||||
thumb-label
|
||||
color="#00cec9"
|
||||
track-color="grey-darken-3"
|
||||
)
|
||||
.text-center.text-h5.font-weight-bold.text-teal-accent-3.mb-6
|
||||
| {{ tempBatchSize }} {{ $t('settings.items') }}
|
||||
|
||||
.text-caption.text-grey.mb-2 Drawing Tolerance
|
||||
v-slider(
|
||||
v-model="tempDrawingAccuracy"
|
||||
:min="5"
|
||||
:max="20"
|
||||
:step="1"
|
||||
thumb-label
|
||||
color="#00cec9"
|
||||
track-color="grey-darken-3"
|
||||
)
|
||||
.d-flex.justify-space-between.text-caption.text-grey-lighten-1.mb-6.px-1
|
||||
span Strict (5)
|
||||
span.font-weight-bold.text-body-1(color="#00cec9") {{ tempDrawingAccuracy }}
|
||||
span Loose (20)
|
||||
|
||||
.text-caption.text-grey.mb-2 {{ $t('settings.language') }}
|
||||
v-btn-toggle.d-flex.w-100.border-subtle(
|
||||
v-model="$i18n.locale"
|
||||
mandatory
|
||||
rounded="lg"
|
||||
color="#00cec9"
|
||||
)
|
||||
v-btn.flex-grow-1(value="en") EN
|
||||
v-btn.flex-grow-1(value="de") DE
|
||||
v-btn.flex-grow-1(value="ja") JA
|
||||
|
||||
v-card-actions.mt-2
|
||||
v-btn.text-white(
|
||||
block
|
||||
color="#2f3542"
|
||||
@click="saveSettings"
|
||||
) {{ $t('settings.save') }}
|
||||
|
||||
v-dialog(v-model="showLogoutDialog" max-width="320")
|
||||
v-card.rounded-xl.pa-4.border-subtle(color="#1e1e24")
|
||||
.d-flex.flex-column.align-center.text-center.pt-2
|
||||
v-avatar.mb-3(color="red-darken-4" size="48")
|
||||
v-icon(icon="mdi-logout" size="24" color="red-lighten-1")
|
||||
|
||||
h3.text-h6.font-weight-bold.mb-2 {{ $t('nav.logout') }}?
|
||||
|
||||
p.text-body-2.text-grey-lighten-1.px-2.mb-4
|
||||
| {{ $t('alerts.logoutConfirm') }}
|
||||
|
||||
v-card-actions.d-flex.gap-2.pa-0.mt-2
|
||||
v-btn.flex-grow-1.text-capitalize(
|
||||
variant="tonal"
|
||||
color="grey"
|
||||
height="40"
|
||||
@click="showLogoutDialog = false"
|
||||
) {{ $t('common.cancel') }}
|
||||
|
||||
v-btn.flex-grow-1.text-capitalize(
|
||||
color="red-accent-2"
|
||||
variant="flat"
|
||||
height="40"
|
||||
@click="confirmLogout"
|
||||
) {{ $t('nav.logout') }}
|
||||
|
||||
v-snackbar(v-model="snackbar.show" :color="snackbar.color" timeout="3000")
|
||||
| {{ snackbar.text }}
|
||||
template(v-slot:actions)
|
||||
v-btn(variant="text" @click="snackbar.show = false") {{ $t('common.close') }}
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useAppStore } from '@/stores/appStore';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import logo from '@/assets/icon.svg';
|
||||
|
||||
const drawer = ref(false);
|
||||
const { locale, t } = useI18n();
|
||||
const store = useAppStore();
|
||||
|
||||
const inputKey = ref('');
|
||||
const loggingIn = ref(false);
|
||||
const syncing = ref(false);
|
||||
const errorMsg = ref('');
|
||||
const showSettings = ref(false);
|
||||
const showLogoutDialog = ref(false);
|
||||
const snackbar = ref({ show: false, text: '', color: 'success' });
|
||||
|
||||
const tempBatchSize = ref(store.batchSize);
|
||||
const tempDrawingAccuracy = ref(store.drawingAccuracy);
|
||||
|
||||
onMounted(() => {
|
||||
if (store.token) {
|
||||
store.fetchStats();
|
||||
}
|
||||
});
|
||||
|
||||
watch(showSettings, (isOpen) => {
|
||||
if (isOpen) {
|
||||
tempBatchSize.value = store.batchSize;
|
||||
tempDrawingAccuracy.value = store.drawingAccuracy;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleLogin() {
|
||||
if (!inputKey.value) return;
|
||||
loggingIn.value = true;
|
||||
errorMsg.value = '';
|
||||
|
||||
try {
|
||||
const result = await store.login(inputKey.value.trim());
|
||||
if (result.user && !result.user.lastSync) {
|
||||
manualSync();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
errorMsg.value = e.message || t('login.failed');
|
||||
} finally {
|
||||
loggingIn.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function manualSync() {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const result = await store.sync();
|
||||
snackbar.value = { show: true, text: t('alerts.syncSuccess', { count: result.count }), color: 'success' };
|
||||
await store.fetchQueue();
|
||||
await store.fetchStats();
|
||||
await store.fetchCollection();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snackbar.value = { show: true, text: t('alerts.syncFailed'), color: 'error' };
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
store.saveSettings({
|
||||
batchSize: tempBatchSize.value,
|
||||
drawingAccuracy: tempDrawingAccuracy.value
|
||||
});
|
||||
|
||||
localStorage.setItem('zen_locale', locale.value);
|
||||
|
||||
showSettings.value = false;
|
||||
store.fetchQueue();
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
showLogoutDialog.value = true;
|
||||
drawer.value = false;
|
||||
}
|
||||
function confirmLogout() {
|
||||
store.logout();
|
||||
inputKey.value = '';
|
||||
showLogoutDialog.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" src="@/styles/pages/_app.scss"></style>
|
||||
Reference in New Issue
Block a user