Refactored frontend directory
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m46s

This commit is contained in:
2026-04-25 00:40:15 -04:00
parent db846f4e31
commit 127378797c
263 changed files with 88 additions and 79 deletions

View File

@@ -0,0 +1,182 @@
<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">
<img :src="feature.screenshot" :alt="feature.screenshotLabel" class="screenshot-img" />
</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',
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',
screenshot: '/images/feature00.png',
},
{
tag: 'Rewards',
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',
screenshot: '/images/feature01.png',
},
{
tag: 'Child View',
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',
screenshot: '/images/feature02.png',
},
{
tag: 'Parent Controls',
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',
screenshot: '/images/feature03.png',
},
]
</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-img {
width: 100%;
height: auto;
display: block;
border-radius: 16px;
border: 1.5px solid var(--landing-placeholder-border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
object-fit: cover;
}
.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,123 @@
<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" @click="scrollToTop" />
<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' })
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
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));
cursor: pointer;
}
.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' })
})
})