This commit is contained in:
50
frontend/vue-app/src/layout/AuthLayout.vue
Normal file
50
frontend/vue-app/src/layout/AuthLayout.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="layout-root">
|
||||
<header class="topbar">
|
||||
<div class="back-btn-container">
|
||||
<button v-show="showBack" class="back-btn" @click="handleBack" tabindex="0">← Back</button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
</header>
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const handleBack = () => {
|
||||
// route to the auth landing page instead of using browser history
|
||||
router.push({ name: 'AuthLanding' }).catch(() => {
|
||||
// fallback to a safe path if named route isn't available
|
||||
window.location.href = '/auth'
|
||||
})
|
||||
}
|
||||
|
||||
// hide back button specifically on the Auth landing route
|
||||
const showBack = computed(
|
||||
() =>
|
||||
route.name !== 'AuthLanding' && route.name !== 'VerifySignup' && route.name !== 'ResetPassword',
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Only keep styles unique to ChildLayout */
|
||||
|
||||
.topbar > .spacer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
49
frontend/vue-app/src/layout/ChildLayout.vue
Normal file
49
frontend/vue-app/src/layout/ChildLayout.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="layout-root">
|
||||
<header class="topbar">
|
||||
<div class="back-btn-container">
|
||||
<button v-show="showBack" class="back-btn" @click="handleBack" tabindex="0">← Back</button>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="login-btn-container">
|
||||
<LoginButton />
|
||||
</div>
|
||||
</header>
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import LoginButton from '../components/shared/LoginButton.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const handleBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/child')
|
||||
}
|
||||
}
|
||||
|
||||
const showBack = computed(() => route.path !== '/child')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Only keep styles unique to ChildLayout */
|
||||
|
||||
.topbar > .spacer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
212
frontend/vue-app/src/layout/ParentLayout.vue
Normal file
212
frontend/vue-app/src/layout/ParentLayout.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import LoginButton from '../components/shared/LoginButton.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const handleBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/child')
|
||||
}
|
||||
}
|
||||
|
||||
const showBack = computed(
|
||||
() =>
|
||||
!(
|
||||
route.path === '/parent' ||
|
||||
route.name === 'TaskView' ||
|
||||
route.name === 'RewardView' ||
|
||||
route.name === 'NotificationView'
|
||||
),
|
||||
)
|
||||
|
||||
// Version fetching
|
||||
const appVersion = ref('')
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/version')
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
appVersion.value = data.version || ''
|
||||
}
|
||||
} catch (e) {
|
||||
appVersion.value = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-root">
|
||||
<header class="topbar">
|
||||
<div class="back-btn-container">
|
||||
<button v-show="showBack" class="back-btn" @click="handleBack" tabindex="0">← Back</button>
|
||||
</div>
|
||||
<nav class="view-selector">
|
||||
<button
|
||||
:class="{
|
||||
active: [
|
||||
'ParentChildrenListView',
|
||||
'ParentView',
|
||||
'ChildEditView',
|
||||
'CreateChild',
|
||||
'TaskAssignView',
|
||||
'RewardAssignView',
|
||||
].includes(String(route.name)),
|
||||
}"
|
||||
@click="router.push({ name: 'ParentChildrenListView' })"
|
||||
aria-label="Children"
|
||||
title="Children"
|
||||
>
|
||||
<!-- Children Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="8" cy="10" r="3" />
|
||||
<circle cx="16" cy="10" r="3" />
|
||||
<path d="M2 20c0-2.5 3-4.5 6-4.5s6 2 6 4.5" />
|
||||
<path d="M10 20c0-2 2-3.5 6-3.5s6 1.5 6 3.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: ['TaskView', 'EditTask', 'CreateTask'].includes(String(route.name)) }"
|
||||
@click="router.push({ name: 'TaskView' })"
|
||||
aria-label="Tasks"
|
||||
title="Tasks"
|
||||
>
|
||||
<!-- Book Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M20 22V6a2 2 0 0 0-2-2H6.5A2.5 2.5 0 0 0 4 6.5v13" />
|
||||
<path d="M16 2v4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
:class="{
|
||||
active: ['RewardView', 'EditReward', 'CreateReward'].includes(String(route.name)),
|
||||
}"
|
||||
@click="router.push({ name: 'RewardView' })"
|
||||
aria-label="Rewards"
|
||||
title="Rewards"
|
||||
>
|
||||
<!-- Trophy Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 21h8" />
|
||||
<path d="M12 17v4" />
|
||||
<path d="M17 17a5 5 0 0 0 5-5V7h-4" />
|
||||
<path d="M7 17a5 5 0 0 1-5-5V7h4" />
|
||||
<rect x="7" y="2" width="10" height="15" rx="5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: ['NotificationView'].includes(String(route.name)) }"
|
||||
@click="router.push({ name: 'NotificationView' })"
|
||||
aria-label="Notifications"
|
||||
title="Notifications"
|
||||
>
|
||||
<!-- Notification/Bell Icon -->
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 16v-5a6 6 0 1 0-12 0v5" />
|
||||
<path d="M2 16h20" />
|
||||
<path d="M8 20a4 4 0 0 0 8 0" />
|
||||
<circle cx="19" cy="7" r="2" fill="#ef4444" stroke="none" />
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
<LoginButton class="login-btn-container" />
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<div v-if="appVersion" class="app-version">v{{ appVersion }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Only keep styles unique to ParentLayout */
|
||||
|
||||
.view-selector {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex: 2 1 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.view-selector button {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--button-bg);
|
||||
color: var(--button-text);
|
||||
border: 0;
|
||||
border-radius: 8px 8px 0 0;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.18s,
|
||||
color 0.18s;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.08);
|
||||
}
|
||||
|
||||
.view-selector button.active {
|
||||
background: var(--button-active-bg);
|
||||
color: var(--button-active-text);
|
||||
}
|
||||
|
||||
.view-selector button.active svg {
|
||||
stroke: var(--button-active-text);
|
||||
}
|
||||
|
||||
.view-selector button:hover:not(.active) {
|
||||
background: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.view-selector button {
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user