feat: add landing page components including hero, features, problem, and footer
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m23s

- Introduced LandingHero component with logo, tagline, and action buttons.
- Created LandingFeatures component to showcase chore system benefits.
- Developed LandingProblem component explaining the importance of a structured chore system.
- Implemented LandingFooter for navigation and copyright information.
- Added LandingPage to assemble all components and manage navigation.
- Included unit tests for LandingHero component to ensure functionality.
This commit is contained in:
2026-03-04 16:21:26 -05:00
parent 82ac820c67
commit c922e1180d
19 changed files with 965 additions and 43 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -183,4 +183,24 @@
--create-btn-border: #2563eb;
--create-btn-hover-bg: #2563eb;
--create-btn-hover-color: #fff;
/* Landing page styles */
--landing-overlay: rgba(0, 0, 0, 0.48);
--landing-glass-bg: rgba(255, 255, 255, 0.12);
--landing-glass-border: rgba(255, 255, 255, 0.25);
--landing-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
--landing-section-alt: #f4f6fb;
--landing-section-dark: #1a1830;
--landing-hero-text: #ffffff;
--landing-hero-subtext: rgba(255, 255, 255, 0.82);
--landing-problem-heading: #2d2a4a;
--landing-problem-body: #4a4870;
--landing-card-number: #667eea;
--landing-feature-heading: #ffffff;
--landing-feature-body: rgba(255, 255, 255, 0.88);
--landing-placeholder-bg: rgba(255, 255, 255, 0.08);
--landing-placeholder-border: rgba(255, 255, 255, 0.2);
--landing-footer-text: rgba(255, 255, 255, 0.7);
--landing-nav-bg: rgba(255, 255, 255, 0.06);
--landing-nav-border: rgba(255, 255, 255, 0.15);
}

View File

@@ -23,6 +23,7 @@ function handleUnauthorizedResponse(): void {
logoutUser()
if (typeof window === 'undefined') return
if (window.location.pathname.startsWith('/auth')) return
if (window.location.pathname === '/') return
if (unauthorizedRedirectHandler) {
unauthorizedRedirectHandler()
return

View File

@@ -577,6 +577,35 @@ onUnmounted(() => {
<div v-if="choreDueLabel(item)" class="due-label">{{ choreDueLabel(item) }}</div>
</template>
</ScrollingList>
<ScrollingList
title="Rewards"
ref="childRewardListRef"
:fetchBaseUrl="`/api/child/${child?.id}/reward-status`"
itemKey="reward_status"
imageField="image_id"
:isParentAuthenticated="false"
:readyItemId="readyItemId"
@item-ready="handleItemReady"
@trigger-item="triggerReward"
:getItemClass="
(item) => ({ reward: true, disabled: hasPendingRewards && !item.redeeming })
"
>
<template #item="{ item }: { item: RewardStatus }">
<div class="item-name">{{ item.name }}</div>
<img
v-if="item.image_url"
:src="item.image_url"
alt="Reward Image"
class="item-image"
/>
<div class="item-points">
<span v-if="item.redeeming" class="pending">PENDING</span>
<span v-if="item.points_needed <= 0" class="ready">REWARD READY</span>
<span v-else>{{ item.points_needed }} more points</span>
</div>
</template>
</ScrollingList>
<ScrollingList
title="Kindness Acts"
ref="childKindnessListRef"
@@ -631,35 +660,6 @@ onUnmounted(() => {
</div>
</template>
</ScrollingList>
<ScrollingList
title="Rewards"
ref="childRewardListRef"
:fetchBaseUrl="`/api/child/${child?.id}/reward-status`"
itemKey="reward_status"
imageField="image_id"
:isParentAuthenticated="false"
:readyItemId="readyItemId"
@item-ready="handleItemReady"
@trigger-item="triggerReward"
:getItemClass="
(item) => ({ reward: true, disabled: hasPendingRewards && !item.redeeming })
"
>
<template #item="{ item }: { item: RewardStatus }">
<div class="item-name">{{ item.name }}</div>
<img
v-if="item.image_url"
:src="item.image_url"
alt="Reward Image"
class="item-image"
/>
<div class="item-points">
<span v-if="item.redeeming" class="pending">PENDING</span>
<span v-if="item.points_needed <= 0" class="ready">REWARD READY</span>
<span v-else>{{ item.points_needed }} more points</span>
</div>
</template>
</ScrollingList>
</div>
</div>
</div>

View File

@@ -0,0 +1,201 @@
<template>
<section class="features-section">
<div class="container">
<div class="section-label">The Solution</div>
<h2 class="section-heading">A System That Actually Works</h2>
<p class="section-subheading">
Chorly gives your family a structured, fun, and rewarding system so chores become
something kids want to do.
</p>
<div class="features-list">
<div
v-for="(feature, index) in features"
:key="feature.title"
class="feature-row"
:class="{ 'feature-row-reverse': index % 2 !== 0 }"
>
<div class="feature-screenshot">
<div class="screenshot-placeholder">
<span class="placeholder-icon">{{ feature.icon }}</span>
<span class="placeholder-label">{{ feature.screenshotLabel }}</span>
</div>
</div>
<div class="feature-text">
<div class="feature-tag">{{ feature.tag }}</div>
<h3 class="feature-title">{{ feature.title }}</h3>
<p class="feature-body">{{ feature.body }}</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const features = [
{
tag: 'Points & Chores',
icon: '⭐',
title: 'Chores Worth Collecting',
body: 'Create chores with fully customizable point values. Kids earn points for every task they complete, turning daily responsibilities into something genuinely satisfying to collect.',
screenshotLabel: 'Chore Dashboard',
},
{
tag: 'Rewards',
icon: '🎁',
title: 'Goals Kids Actually Care About',
body: "Set up custom rewards that mean something to your child — screen time, a special outing, a toy they've been wanting. Saving up points teaches patience and goal-setting.",
screenshotLabel: 'Reward Store',
},
{
tag: 'Child View',
icon: '👧',
title: 'Kids Stay in the Loop',
body: 'A simple, beautiful child interface shows exactly which chores need to be done today and which rewards are within reach — no confusion, no excuses.',
screenshotLabel: 'Child Dashboard',
},
{
tag: 'Parent Controls',
icon: '🔒',
title: 'Powerful Parental Controls',
body: "Hidden behind a PIN, the parent panel lets you add chores, adjust rewards, review history, and manage everything — without interrupting your child's flow.",
screenshotLabel: 'Parent Panel',
},
]
</script>
<style scoped>
.features-section {
background: var(--header-bg);
padding: 5rem 1.5rem;
}
.container {
max-width: 1100px;
margin: 0 auto;
}
.section-label {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.78rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 0.6rem;
text-align: center;
}
.section-heading {
font-size: clamp(1.6rem, 3.5vw, 2.4rem);
font-weight: 800;
color: var(--landing-feature-heading);
text-align: center;
margin: 0 0 1rem;
line-height: 1.2;
}
.section-subheading {
font-size: clamp(0.95rem, 2vw, 1.1rem);
color: var(--landing-feature-body);
text-align: center;
max-width: 620px;
margin: 0 auto 4rem;
line-height: 1.65;
}
.features-list {
display: flex;
flex-direction: column;
gap: 4rem;
}
.feature-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
align-items: center;
}
.feature-row-reverse {
direction: rtl;
}
.feature-row-reverse > * {
direction: ltr;
}
.screenshot-placeholder {
background: var(--landing-placeholder-bg);
border: 1.5px solid var(--landing-placeholder-border);
border-radius: 16px;
aspect-ratio: 16 / 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.placeholder-icon {
font-size: 2.8rem;
line-height: 1;
}
.placeholder-label {
font-size: 0.85rem;
color: var(--landing-hero-subtext);
font-weight: 500;
letter-spacing: 0.04em;
}
.feature-tag {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.72rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.55);
margin-bottom: 0.5rem;
}
.feature-title {
font-size: clamp(1.2rem, 2.5vw, 1.55rem);
font-weight: 800;
color: var(--landing-feature-heading);
margin: 0 0 0.75rem;
line-height: 1.3;
}
.feature-body {
font-size: 0.97rem;
color: var(--landing-feature-body);
line-height: 1.7;
margin: 0;
}
/* Tablet */
@media (max-width: 800px) {
.feature-row,
.feature-row-reverse {
grid-template-columns: 1fr;
direction: ltr;
gap: 1.5rem;
}
.feature-row-reverse > * {
direction: ltr;
}
}
/* Mobile */
@media (max-width: 560px) {
.features-section {
padding: 3.5rem 1rem;
}
.features-list {
gap: 3rem;
}
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<footer class="landing-footer">
<div class="container">
<div class="footer-brand">
<img src="/images/chorly_logo.png" alt="Chorly" class="footer-logo" />
</div>
<div class="footer-links">
<button class="footer-link" @click="goToLogin">Sign In</button>
<span class="footer-divider">·</span>
<button class="footer-link" @click="goToSignup">Get Started</button>
</div>
<!-- Future: pricing section -->
<p class="footer-copy">© {{ year }} Chorly. All rights reserved.</p>
</div>
</footer>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const year = new Date().getFullYear()
function goToLogin() {
router.push({ name: 'Login' })
}
function goToSignup() {
router.push({ name: 'Signup' })
}
</script>
<style scoped>
.landing-footer {
background: var(--landing-section-dark);
padding: 3rem 1.5rem 2rem;
}
.container {
max-width: 1100px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.2rem;
text-align: center;
}
.footer-logo {
height: 80px;
opacity: 0.85;
filter: brightness(1.1);
}
.footer-links {
display: flex;
align-items: center;
gap: 0.6rem;
}
.footer-link {
background: none;
border: none;
cursor: pointer;
color: var(--landing-footer-text);
font-size: 0.9rem;
padding: 0;
transition: color 0.15s;
}
.footer-link:hover {
color: #fff;
}
.footer-divider {
color: var(--landing-footer-text);
opacity: 0.4;
}
.footer-copy {
font-size: 0.78rem;
color: var(--landing-footer-text);
opacity: 0.5;
margin: 0;
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<section class="hero">
<div class="hero-overlay" />
<div class="hero-content">
<img src="/images/full_logo.png" alt="Chorly Logo" class="hero-logo" />
<p class="hero-tagline">
Turn chores into achievements.<br />Raise kids who own their responsibilities.
</p>
<div class="hero-card">
<div class="hero-actions">
<button class="btn-hero btn-hero-signin" @click="goToLogin">Sign In</button>
<button class="btn-hero btn-hero-signup" @click="goToSignup">Get Started Free</button>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function goToLogin() {
router.push({ name: 'Login' })
}
function goToSignup() {
router.push({ name: 'Signup' })
}
</script>
<style scoped>
.hero {
position: relative;
min-height: 100svh;
display: flex;
align-items: center;
justify-content: center;
background-image: url('/images/hero.png');
background-size: cover;
background-position: center;
overflow: hidden;
}
.hero-overlay {
position: absolute;
inset: 0;
background: var(--landing-overlay);
z-index: 0;
}
.hero-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 2rem 1.5rem;
max-width: 600px;
width: 100%;
}
.hero-logo {
width: min(280px, 70vw);
margin-bottom: 1.5rem;
filter: drop-shadow(0 4px 16px rgba(0, 0, 0, 0.4));
}
.hero-tagline {
color: var(--landing-hero-text);
font-size: clamp(1.1rem, 2.5vw, 1.4rem);
line-height: 1.55;
margin: 0 0 2rem;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
font-weight: 400;
}
.hero-card {
background: var(--landing-glass-bg);
border: 1px solid var(--landing-glass-border);
box-shadow: var(--landing-glass-shadow);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 18px;
padding: 1.8rem 2.5rem;
width: 100%;
max-width: 440px;
}
.hero-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn-hero {
flex: 1;
min-width: 130px;
padding: 0.85rem 1.6rem;
border-radius: 10px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: none;
transition:
transform 0.15s,
box-shadow 0.15s,
background 0.18s;
}
.btn-hero:hover {
transform: translateY(-2px);
}
.btn-hero-signin {
background: rgba(255, 255, 255, 0.18);
color: var(--landing-hero-text);
border: 1.5px solid var(--landing-glass-border);
}
.btn-hero-signin:hover {
background: rgba(255, 255, 255, 0.28);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.btn-hero-signup {
background: var(--btn-primary);
color: #fff;
box-shadow: 0 4px 18px rgba(102, 126, 234, 0.45);
}
.btn-hero-signup:hover {
background: var(--btn-primary-hover);
box-shadow: 0 6px 22px rgba(102, 126, 234, 0.6);
}
/* Mobile */
@media (max-width: 480px) {
.hero-card {
padding: 1.4rem 1.2rem;
}
.hero-actions {
flex-direction: column;
}
.btn-hero {
width: 100%;
flex: none;
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="landing-page">
<!-- Sticky nav -->
<nav class="landing-nav" :class="{ 'landing-nav-scrolled': scrolled }">
<div class="nav-inner">
<img src="/images/c_logo.png" alt="Chorly" class="nav-logo" />
<button class="nav-signin" @click="goToLogin">Sign In</button>
</div>
</nav>
<LandingHero />
<LandingProblem />
<LandingFeatures />
<LandingFooter />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import LandingHero from '@/components/landing/LandingHero.vue'
import LandingProblem from '@/components/landing/LandingProblem.vue'
import LandingFeatures from '@/components/landing/LandingFeatures.vue'
import LandingFooter from '@/components/landing/LandingFooter.vue'
const router = useRouter()
const scrolled = ref(false)
function onScroll() {
scrolled.value = window.scrollY > 40
}
function goToLogin() {
router.push({ name: 'Login' })
}
onMounted(() => {
window.addEventListener('scroll', onScroll, { passive: true })
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
</script>
<style scoped>
.landing-page {
min-height: 100svh;
overflow-x: hidden;
}
/* ── Sticky nav ── */
.landing-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: 0.9rem 1.5rem;
transition:
background 0.25s,
box-shadow 0.25s,
border-bottom 0.25s;
}
.landing-nav-scrolled {
background: rgba(26, 24, 48, 0.82);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.3);
border-bottom: 1px solid var(--landing-nav-border);
}
.nav-inner {
max-width: 1100px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-logo {
height: 34px;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
}
.nav-signin {
background: var(--landing-glass-bg);
border: 1px solid var(--landing-glass-border);
color: #fff;
padding: 0.45rem 1.2rem;
border-radius: 8px;
font-size: 0.88rem;
font-weight: 600;
cursor: pointer;
transition:
background 0.15s,
box-shadow 0.15s;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.nav-signin:hover {
background: rgba(255, 255, 255, 0.22);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
/* Mobile */
@media (max-width: 480px) {
.landing-nav {
padding: 0.75rem 1rem;
}
.nav-logo {
height: 28px;
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<section class="problem-section">
<div class="container">
<div class="section-label">Why It Matters</div>
<h2 class="section-heading">Why Most Chore Systems Fail</h2>
<p class="section-subheading">
Nagging doesn't work. Vague expectations don't work. But a clear, consistent system changes
everything for your kids and for you.
</p>
<div class="benefits-grid">
<div v-for="benefit in benefits" :key="benefit.title" class="benefit-card">
<h3 class="benefit-title">{{ benefit.title }}</h3>
<p class="benefit-body">{{ benefit.body }}</p>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
const benefits = [
{
title: 'Develops Executive Function',
body: 'Managing a to-do list teaches kids how to prioritize tasks, manage their time, and follow multi-step directions — skills that translate directly to schoolwork and future careers.',
},
{
title: 'Fosters a "Can-Do" Attitude',
body: "Completing a task provides a tangible sense of mastery. This builds genuine self-esteem because it's based on actual competence rather than empty praise.",
},
{
title: 'Encourages Accountability',
body: 'A system moves chores from "Mom/Dad is nagging me" to "This is my responsibility." It teaches that their actions have a direct impact on the people they live with.',
},
{
title: 'Normalizes Life Skills',
body: 'By the time they head out on their own, "adulting" won\'t feel like a mountain to climb. Cooking, cleaning, and organizing will be second nature rather than a stressful learning curve.',
},
{
title: 'Promotes Family Belonging',
body: 'Contributing to the home makes a child feel like a teammate rather than a guest. It reinforces the idea that a family is a unit where everyone supports one another.',
},
]
</script>
<style scoped>
.problem-section {
background: var(--landing-section-alt);
padding: 5rem 1.5rem;
}
.container {
max-width: 1100px;
margin: 0 auto;
}
.section-label {
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.78rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 0.6rem;
text-align: center;
}
.section-heading {
font-size: clamp(1.6rem, 3.5vw, 2.4rem);
font-weight: 800;
color: var(--landing-problem-heading);
text-align: center;
margin: 0 0 1rem;
line-height: 1.2;
}
.section-subheading {
font-size: clamp(0.95rem, 2vw, 1.1rem);
color: var(--landing-problem-body);
text-align: center;
max-width: 640px;
margin: 0 auto 3.5rem;
line-height: 1.65;
}
.benefits-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.benefit-card {
background: var(--card-bg);
border-radius: 14px;
padding: 1.6rem 1.6rem 1.6rem 1.4rem;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.09);
border-left: 3px solid var(--primary);
transition:
transform 0.18s,
box-shadow 0.18s;
}
.benefit-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 28px rgba(102, 126, 234, 0.16);
}
.benefit-title {
font-size: 1rem;
font-weight: 700;
color: var(--landing-problem-heading);
margin: 0 0 0.6rem;
line-height: 1.3;
}
.benefit-body {
font-size: 0.9rem;
color: var(--landing-problem-body);
line-height: 1.65;
margin: 0;
}
/* Tablet */
@media (max-width: 900px) {
.benefits-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Mobile */
@media (max-width: 560px) {
.problem-section {
padding: 3.5rem 1rem;
}
.benefits-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,49 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import LandingHero from '../LandingHero.vue'
const { pushMock } = vi.hoisted(() => ({
pushMock: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRouter: vi.fn(() => ({ push: pushMock })),
}))
describe('LandingHero.vue', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders the logo image', () => {
const wrapper = mount(LandingHero)
const logo = wrapper.find('img.hero-logo')
expect(logo.exists()).toBe(true)
expect(logo.attributes('src')).toBe('/images/full_logo.png')
})
it('renders a tagline', () => {
const wrapper = mount(LandingHero)
expect(wrapper.find('.hero-tagline').exists()).toBe(true)
})
it('renders Sign In and Get Started buttons', () => {
const wrapper = mount(LandingHero)
const buttons = wrapper.findAll('button')
expect(buttons).toHaveLength(2)
expect(buttons[0].text()).toBe('Sign In')
expect(buttons[1].text()).toBe('Get Started Free')
})
it('navigates to Login route when Sign In is clicked', async () => {
const wrapper = mount(LandingHero)
await wrapper.find('.btn-hero-signin').trigger('click')
expect(pushMock).toHaveBeenCalledWith({ name: 'Login' })
})
it('navigates to Signup route when Get Started is clicked', async () => {
const wrapper = mount(LandingHero)
await wrapper.find('.btn-hero-signup').trigger('click')
expect(pushMock).toHaveBeenCalledWith({ name: 'Signup' })
})
})

View File

@@ -94,20 +94,20 @@ describe('router auth guard', () => {
expect(path).toBe('/auth/login')
})
// ── Unauthenticated users are redirected to /auth from protected routes ───
// ── Unauthenticated users are redirected to / from protected routes ────────
it('redirects unauthenticated user from /parent to /auth', async () => {
it('redirects unauthenticated user from /parent to /', async () => {
isUserLoggedInMock.value = false
const path = await navigate('/parent')
expect(path).toBe('/auth')
expect(path).toBe('/')
})
it('redirects unauthenticated user from /child to /auth', async () => {
it('redirects unauthenticated user from /child to /', async () => {
isUserLoggedInMock.value = false
const path = await navigate('/child')
expect(path).toBe('/auth')
expect(path).toBe('/')
})
// ── Authenticated users are routed to the correct section ─────────────────
@@ -152,4 +152,29 @@ describe('router auth guard', () => {
const path = await navigate('/parent/pin-setup')
expect(path).toBe('/parent/pin-setup')
})
// ── Landing page (/) ────────────────────────────────────────────────────────
it('allows unauthenticated user to access / (landing page)', async () => {
isUserLoggedInMock.value = false
const path = await navigate('/')
expect(path).toBe('/')
})
it('redirects logged-in child user from / to /child', async () => {
isUserLoggedInMock.value = true
isParentAuthenticatedMock.value = false
const path = await navigate('/')
expect(path).toBe('/child')
})
it('redirects logged-in parent user from / to /parent', async () => {
isUserLoggedInMock.value = true
isParentAuthenticatedMock.value = true
const path = await navigate('/')
expect(path).toBe('/parent')
})
})

View File

@@ -32,6 +32,7 @@ import {
enforceParentExpiry,
} from '../stores/auth'
import ParentPinSetup from '@/components/auth/ParentPinSetup.vue'
import LandingPage from '@/components/landing/LandingPage.vue'
const routes = [
{
@@ -235,7 +236,9 @@ const routes = [
},
{
path: '/',
redirect: '/child',
name: 'LandingPage',
component: LandingPage,
meta: { isPublic: true },
},
]
@@ -260,8 +263,8 @@ router.beforeEach(async (to, from, next) => {
})
}
// If already logged in and trying to access /auth, redirect to appropriate view
if (to.path.startsWith('/auth') && isUserLoggedIn.value) {
// If already logged in and trying to access /auth or landing, redirect to appropriate view
if ((to.path.startsWith('/auth') || to.path === '/') && isUserLoggedIn.value) {
if (isParentAuthenticated.value) {
return next('/parent')
} else {
@@ -269,14 +272,14 @@ router.beforeEach(async (to, from, next) => {
}
}
// Always allow /auth and /parent/pin-setup
if (to.path.startsWith('/auth') || to.name === 'ParentPinSetup') {
// Always allow /auth, landing page, and /parent/pin-setup
if (to.path.startsWith('/auth') || to.path === '/' || to.name === 'ParentPinSetup') {
return next()
}
// If not logged in, redirect to /auth
// If not logged in, redirect to landing page
if (!isUserLoggedIn.value) {
return next('/auth')
return next('/')
}
// If parent-authenticated, allow all /parent routes

View File

@@ -134,8 +134,8 @@ export function initAuthSync() {
const payload = JSON.parse(event.newValue)
if (payload?.type === 'logout') {
applyLoggedOutState()
if (!window.location.pathname.startsWith('/auth')) {
window.location.href = '/auth/login'
if (!window.location.pathname.startsWith('/auth') && window.location.pathname !== '/') {
window.location.href = '/'
}
} else if (payload?.type === 'parent_logout') {
applyParentLoggedOutState()