feat: change codebase structure #1
Merged
diegovester
merged 1 commits from feat-change-codebase-structure into main 2 months ago
24 changed files with 981 additions and 161 deletions
@ -1,85 +1,27 @@ |
|||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import { RouterLink, RouterView } from 'vue-router' |
/** |
||||||
import HelloWorld from './components/HelloWorld.vue' |
* App.vue - Root application component |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* This file should remain minimal. It only contains: |
||||||
|
* - AppHeader (site navigation) |
||||||
|
* - RouterView (page content) |
||||||
|
* - AppFooter (site footer) |
||||||
|
* |
||||||
|
* To change site content, edit files in /src/content/ |
||||||
|
* To change layout, edit components in /src/components/layout/ |
||||||
|
* To change pages, edit files in /src/views/ |
||||||
|
*/ |
||||||
|
import AppHeader from "@/components/layout/AppHeader.vue"; |
||||||
|
import AppFooter from "@/components/layout/AppFooter.vue"; |
||||||
</script> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<header> |
<AppHeader /> |
||||||
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> |
|
||||||
|
|
||||||
<div class="wrapper"> |
|
||||||
<HelloWorld msg="You did it!" /> |
|
||||||
|
|
||||||
<nav> |
|
||||||
<RouterLink to="/">Home</RouterLink> |
|
||||||
<RouterLink to="/about">About</RouterLink> |
|
||||||
</nav> |
|
||||||
</div> |
|
||||||
</header> |
|
||||||
|
|
||||||
<RouterView /> |
<RouterView /> |
||||||
|
<AppFooter /> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<style scoped> |
<style scoped> |
||||||
header { |
/* App-level styles kept minimal - see /src/styles/ for global styles */ |
||||||
line-height: 1.5; |
|
||||||
max-height: 100vh; |
|
||||||
} |
|
||||||
|
|
||||||
.logo { |
|
||||||
display: block; |
|
||||||
margin: 0 auto 2rem; |
|
||||||
} |
|
||||||
|
|
||||||
nav { |
|
||||||
width: 100%; |
|
||||||
font-size: 12px; |
|
||||||
text-align: center; |
|
||||||
margin-top: 2rem; |
|
||||||
} |
|
||||||
|
|
||||||
nav a.router-link-exact-active { |
|
||||||
color: var(--color-text); |
|
||||||
} |
|
||||||
|
|
||||||
nav a.router-link-exact-active:hover { |
|
||||||
background-color: transparent; |
|
||||||
} |
|
||||||
|
|
||||||
nav a { |
|
||||||
display: inline-block; |
|
||||||
padding: 0 1rem; |
|
||||||
border-left: 1px solid var(--color-border); |
|
||||||
} |
|
||||||
|
|
||||||
nav a:first-of-type { |
|
||||||
border: 0; |
|
||||||
} |
|
||||||
|
|
||||||
@media (min-width: 1024px) { |
|
||||||
header { |
|
||||||
display: flex; |
|
||||||
place-items: center; |
|
||||||
padding-right: calc(var(--section-gap) / 2); |
|
||||||
} |
|
||||||
|
|
||||||
.logo { |
|
||||||
margin: 0 2rem 0 0; |
|
||||||
} |
|
||||||
|
|
||||||
header .wrapper { |
|
||||||
display: flex; |
|
||||||
place-items: flex-start; |
|
||||||
flex-wrap: wrap; |
|
||||||
} |
|
||||||
|
|
||||||
nav { |
|
||||||
text-align: left; |
|
||||||
margin-left: -1rem; |
|
||||||
font-size: 1rem; |
|
||||||
|
|
||||||
padding: 1rem 0; |
|
||||||
margin-top: 1rem; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
</style> |
||||||
|
|||||||
@ -1,11 +1,11 @@ |
|||||||
import { describe, it, expect } from 'vitest' |
import { describe, it, expect } from "vitest"; |
||||||
|
|
||||||
import { mount } from '@vue/test-utils' |
import { mount } from "@vue/test-utils"; |
||||||
import HelloWorld from '../HelloWorld.vue' |
import HelloWorld from "../HelloWorld.vue"; |
||||||
|
|
||||||
describe('HelloWorld', () => { |
describe("HelloWorld", () => { |
||||||
it('renders properly', () => { |
it("renders properly", () => { |
||||||
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) |
const wrapper = mount(HelloWorld, { props: { msg: "Hello Vitest" } }); |
||||||
expect(wrapper.text()).toContain('Hello Vitest') |
expect(wrapper.text()).toContain("Hello Vitest"); |
||||||
}) |
}); |
||||||
}) |
}); |
||||||
|
|||||||
@ -0,0 +1,100 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
/** |
||||||
|
* AppFooter - Site footer with copyright and links |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* - "change copyright" -> update the copyright text |
||||||
|
* - "add social link" -> add to social links section |
||||||
|
* - "change footer text" -> update the footer content |
||||||
|
*/ |
||||||
|
import { hero, contact } from "@/content/site-data"; |
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear(); |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<footer class="app-footer"> |
||||||
|
<div class="footer-container"> |
||||||
|
<div class="footer-brand"> |
||||||
|
<span class="brand-name">{{ hero.title.value }}</span> |
||||||
|
<p class="tagline">{{ hero.tagline.value }}</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="footer-links"> |
||||||
|
<a |
||||||
|
v-if="contact.etsyLink.value" |
||||||
|
:href="contact.etsyLink.value" |
||||||
|
target="_blank" |
||||||
|
rel="noopener noreferrer" |
||||||
|
class="etsy-link" |
||||||
|
> |
||||||
|
Visit Shop on Etsy |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="footer-copyright"> |
||||||
|
<p>© {{ currentYear }} {{ hero.title.value }}. Made with love.</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</footer> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.app-footer { |
||||||
|
background-color: var(--color-background-mute); |
||||||
|
border-top: 1px solid var(--color-border); |
||||||
|
padding: var(--space-xl) var(--space-lg); |
||||||
|
margin-top: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.footer-container { |
||||||
|
max-width: var(--max-width); |
||||||
|
margin: 0 auto; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.footer-brand { |
||||||
|
margin-bottom: var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
.brand-name { |
||||||
|
font-family: var(--font-heading); |
||||||
|
font-size: var(--font-size-lg); |
||||||
|
font-weight: 600; |
||||||
|
color: var(--color-primary-dark); |
||||||
|
} |
||||||
|
|
||||||
|
.tagline { |
||||||
|
color: var(--color-text-light); |
||||||
|
font-size: var(--font-size-sm); |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.footer-links { |
||||||
|
margin-bottom: var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
.etsy-link { |
||||||
|
display: inline-block; |
||||||
|
padding: var(--space-sm) var(--space-lg); |
||||||
|
background-color: var(--color-secondary); |
||||||
|
color: white; |
||||||
|
border-radius: var(--border-radius); |
||||||
|
font-weight: 600; |
||||||
|
transition: background-color var(--transition-fast); |
||||||
|
} |
||||||
|
|
||||||
|
.etsy-link:hover { |
||||||
|
background-color: var(--color-secondary-dark); |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.footer-copyright { |
||||||
|
color: var(--color-text-light); |
||||||
|
font-size: var(--font-size-sm); |
||||||
|
} |
||||||
|
|
||||||
|
.footer-copyright p { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
/** |
||||||
|
* AppHeader - Site header with logo and navigation |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* - "change the logo" -> update the logo image or text |
||||||
|
* - "add a nav link" -> add to the navigation links array |
||||||
|
* - "remove a nav link" -> remove from navigation links |
||||||
|
* - "change header color" -> modify the header background style |
||||||
|
*/ |
||||||
|
import { RouterLink } from "vue-router"; |
||||||
|
import { hero } from "@/content/site-data"; |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<header class="app-header"> |
||||||
|
<div class="header-container"> |
||||||
|
<RouterLink to="/" class="logo"> |
||||||
|
{{ hero.title.value }} |
||||||
|
</RouterLink> |
||||||
|
|
||||||
|
<nav class="main-nav"> |
||||||
|
<RouterLink to="/">Home</RouterLink> |
||||||
|
<RouterLink to="/about">About</RouterLink> |
||||||
|
</nav> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.app-header { |
||||||
|
background-color: var(--color-background-soft); |
||||||
|
border-bottom: 1px solid var(--color-border); |
||||||
|
padding: var(--space-md) var(--space-lg); |
||||||
|
position: sticky; |
||||||
|
top: 0; |
||||||
|
z-index: 100; |
||||||
|
} |
||||||
|
|
||||||
|
.header-container { |
||||||
|
max-width: var(--max-width); |
||||||
|
margin: 0 auto; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.logo { |
||||||
|
font-family: var(--font-heading); |
||||||
|
font-size: var(--font-size-xl); |
||||||
|
font-weight: 700; |
||||||
|
color: var(--color-primary-dark); |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
.logo:hover { |
||||||
|
color: var(--color-primary); |
||||||
|
} |
||||||
|
|
||||||
|
.main-nav { |
||||||
|
display: flex; |
||||||
|
gap: var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
.main-nav a { |
||||||
|
font-weight: 500; |
||||||
|
color: var(--color-text); |
||||||
|
padding: var(--space-xs) var(--space-sm); |
||||||
|
border-radius: var(--border-radius); |
||||||
|
transition: all var(--transition-fast); |
||||||
|
} |
||||||
|
|
||||||
|
.main-nav a:hover { |
||||||
|
color: var(--color-primary-dark); |
||||||
|
background-color: var(--color-primary-light); |
||||||
|
} |
||||||
|
|
||||||
|
.main-nav a.router-link-active { |
||||||
|
color: var(--color-primary-dark); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
/** |
||||||
|
* HeroSection - Main hero/landing section of the homepage |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* Content is loaded from /src/content/site-data.js |
||||||
|
* |
||||||
|
* When user says: |
||||||
|
* - "change the title" -> edit hero.title.value in site-data.js |
||||||
|
* - "change the subtitle" or "tagline" -> edit hero.tagline.value in site-data.js |
||||||
|
* - "change the description" -> edit hero.description.value in site-data.js |
||||||
|
* - "change hero colors" -> modify the styles below or variables.css |
||||||
|
*/ |
||||||
|
import { hero } from "@/content/site-data"; |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<section class="hero-section"> |
||||||
|
<div class="hero-container"> |
||||||
|
<h1 class="hero-title">{{ hero.title.value }}</h1> |
||||||
|
<p class="hero-tagline">{{ hero.tagline.value }}</p> |
||||||
|
<p class="hero-description">{{ hero.description.value }}</p> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.hero-section { |
||||||
|
padding: var(--space-2xl) var(--space-lg); |
||||||
|
text-align: center; |
||||||
|
background: linear-gradient( |
||||||
|
135deg, |
||||||
|
var(--color-background-soft) 0%, |
||||||
|
var(--color-primary-light) 50%, |
||||||
|
var(--color-secondary-light) 100% |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
.hero-container { |
||||||
|
max-width: var(--content-width); |
||||||
|
margin: 0 auto; |
||||||
|
} |
||||||
|
|
||||||
|
.hero-title { |
||||||
|
font-size: clamp(2.5rem, 8vw, 4rem); |
||||||
|
color: var(--color-primary-dark); |
||||||
|
margin-bottom: var(--space-md); |
||||||
|
letter-spacing: -0.02em; |
||||||
|
} |
||||||
|
|
||||||
|
.hero-tagline { |
||||||
|
font-size: var(--font-size-xl); |
||||||
|
color: var(--color-secondary-dark); |
||||||
|
font-weight: 500; |
||||||
|
margin-bottom: var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
.hero-description { |
||||||
|
font-size: var(--font-size-lg); |
||||||
|
color: var(--color-text); |
||||||
|
max-width: 600px; |
||||||
|
margin: 0 auto; |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
/** |
||||||
|
* Plushie Catalog - lukastitch.corneruniverse.com |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* This file contains the list of plushies displayed in the gallery. |
||||||
|
* |
||||||
|
* Common user requests and how to handle them: |
||||||
|
* - "add a plushie" -> add a new object to the plushies array |
||||||
|
* - "remove a plushie" -> remove the object with matching name/id |
||||||
|
* - "mark as sold" -> set available: false |
||||||
|
* - "mark as available" -> set available: true |
||||||
|
* - "change plushie description" -> update the description field |
||||||
|
* - "update etsy link" -> update the etsyLink field |
||||||
|
*/ |
||||||
|
|
||||||
|
export interface Plushie { |
||||||
|
id: string; |
||||||
|
name: string; |
||||||
|
image: string; |
||||||
|
description: string; |
||||||
|
size: string; |
||||||
|
available: boolean; |
||||||
|
etsyLink: string | null; |
||||||
|
tags?: string[]; // Optional: helps with search
|
||||||
|
} |
||||||
|
|
||||||
|
export const plushies: Plushie[] = [ |
||||||
|
// Example entry - replace with real plushies:
|
||||||
|
// {
|
||||||
|
// id: 'cozy-bear',
|
||||||
|
// name: 'Cozy Bear',
|
||||||
|
// image: '/images/plushies/cozy-bear.jpg',
|
||||||
|
// description: 'A huggable friend for cold nights.',
|
||||||
|
// size: '12 inches',
|
||||||
|
// available: false,
|
||||||
|
// etsyLink: null,
|
||||||
|
// tags: ['bear', 'cozy', 'huggable']
|
||||||
|
// }
|
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Find a plushie by name (case-insensitive partial match) |
||||||
|
*/ |
||||||
|
export function findPlushie(searchName: string): Plushie | undefined { |
||||||
|
const normalized = searchName.toLowerCase().trim(); |
||||||
|
return plushies.find( |
||||||
|
(p) => |
||||||
|
p.name.toLowerCase().includes(normalized) || |
||||||
|
p.id.toLowerCase().includes(normalized), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get available plushies only |
||||||
|
*/ |
||||||
|
export function getAvailablePlushies(): Plushie[] { |
||||||
|
return plushies.filter((p) => p.available); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get sold plushies only |
||||||
|
*/ |
||||||
|
export function getSoldPlushies(): Plushie[] { |
||||||
|
return plushies.filter((p) => !p.available); |
||||||
|
} |
||||||
@ -0,0 +1,200 @@ |
|||||||
|
/** |
||||||
|
* Site Content Data - lukastitch.corneruniverse.com |
||||||
|
* |
||||||
|
* This file contains all editable site content in one place. |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* Each content field has a "tags" array containing words and phrases that |
||||||
|
* humans might use to refer to this content. When a user says something like |
||||||
|
* "change the title" or "update the main heading", match their language to |
||||||
|
* the tags to find the correct field to modify. |
||||||
|
* |
||||||
|
* Tag matching examples: |
||||||
|
* - "change the title" -> look for tags containing "title" |
||||||
|
* - "update the subtitle" -> look for tags containing "subtitle" |
||||||
|
* - "change the tagline" -> look for tags containing "tagline" |
||||||
|
* - "update the description" -> look for tags containing "description" |
||||||
|
*/ |
||||||
|
|
||||||
|
interface ContentField { |
||||||
|
value: string | null; |
||||||
|
tags: string[]; |
||||||
|
} |
||||||
|
|
||||||
|
interface HeroContent { |
||||||
|
title: ContentField; |
||||||
|
tagline: ContentField; |
||||||
|
description: ContentField; |
||||||
|
} |
||||||
|
|
||||||
|
interface AboutContent { |
||||||
|
heading: ContentField; |
||||||
|
text: ContentField; |
||||||
|
image: ContentField; |
||||||
|
} |
||||||
|
|
||||||
|
interface ContactContent { |
||||||
|
heading: ContentField; |
||||||
|
text: ContentField; |
||||||
|
etsyLink: ContentField; |
||||||
|
email: ContentField; |
||||||
|
} |
||||||
|
|
||||||
|
export const hero: HeroContent = { |
||||||
|
title: { |
||||||
|
value: "Luka Stitch", |
||||||
|
tags: [ |
||||||
|
"title", |
||||||
|
"main title", |
||||||
|
"heading", |
||||||
|
"main heading", |
||||||
|
"name", |
||||||
|
"brand name", |
||||||
|
"site name", |
||||||
|
"website name", |
||||||
|
"big text", |
||||||
|
"header text", |
||||||
|
], |
||||||
|
}, |
||||||
|
tagline: { |
||||||
|
value: "Handmade plushies with love", |
||||||
|
tags: [ |
||||||
|
"tagline", |
||||||
|
"subtitle", |
||||||
|
"slogan", |
||||||
|
"motto", |
||||||
|
"subheading", |
||||||
|
"secondary text", |
||||||
|
"smaller text under title", |
||||||
|
], |
||||||
|
}, |
||||||
|
description: { |
||||||
|
value: "Each creation is one-of-a-kind, crafted with care.", |
||||||
|
tags: [ |
||||||
|
"description", |
||||||
|
"intro", |
||||||
|
"introduction", |
||||||
|
"about text", |
||||||
|
"body text", |
||||||
|
"paragraph", |
||||||
|
"blurb", |
||||||
|
], |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export const about: AboutContent = { |
||||||
|
heading: { |
||||||
|
value: "About", |
||||||
|
tags: ["about title", "about heading", "about section title"], |
||||||
|
}, |
||||||
|
text: { |
||||||
|
value: |
||||||
|
"Hi! I'm the maker behind Luka Stitch. I create handmade plushies, each one crafted with love and attention to detail. Every plushie is unique and made to bring joy.", |
||||||
|
tags: [ |
||||||
|
"about text", |
||||||
|
"about description", |
||||||
|
"about paragraph", |
||||||
|
"bio", |
||||||
|
"biography", |
||||||
|
"about me", |
||||||
|
"maker story", |
||||||
|
], |
||||||
|
}, |
||||||
|
image: { |
||||||
|
value: "/images/about-photo.jpg", |
||||||
|
tags: ["about image", "about photo", "maker photo", "profile photo"], |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export const contact: ContactContent = { |
||||||
|
heading: { |
||||||
|
value: "Get in Touch", |
||||||
|
tags: ["contact title", "contact heading", "get in touch title"], |
||||||
|
}, |
||||||
|
text: { |
||||||
|
value: "Interested in a custom plushie? I'd love to hear from you!", |
||||||
|
tags: [ |
||||||
|
"contact text", |
||||||
|
"contact description", |
||||||
|
"contact message", |
||||||
|
"reach out text", |
||||||
|
], |
||||||
|
}, |
||||||
|
etsyLink: { |
||||||
|
value: "https://etsy.com/shop/lukastitch", |
||||||
|
tags: ["etsy", "etsy link", "shop link", "store link", "buy link"], |
||||||
|
}, |
||||||
|
email: { |
||||||
|
value: null, |
||||||
|
tags: ["email", "email address", "contact email"], |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
type SectionName = "hero" | "about" | "contact"; |
||||||
|
|
||||||
|
interface TagSearchResult { |
||||||
|
section: SectionName; |
||||||
|
field: string; |
||||||
|
value: string | null; |
||||||
|
tags: string[]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper function to find content by tag |
||||||
|
* Usage: findByTag("title") returns { section: "hero", field: "title", value: "Luka Stitch" } |
||||||
|
*/ |
||||||
|
export function findByTag(searchTag: string): TagSearchResult | null { |
||||||
|
const normalizedSearch = searchTag.toLowerCase().trim(); |
||||||
|
const sections = { hero, about, contact } as const; |
||||||
|
|
||||||
|
for (const sectionName of Object.keys(sections) as SectionName[]) { |
||||||
|
const section = sections[sectionName]; |
||||||
|
for (const [fieldName, field] of Object.entries(section)) { |
||||||
|
const contentField = field as ContentField; |
||||||
|
if ( |
||||||
|
contentField.tags && |
||||||
|
contentField.tags.some( |
||||||
|
(tag: string) => |
||||||
|
tag.toLowerCase().includes(normalizedSearch) || |
||||||
|
normalizedSearch.includes(tag.toLowerCase()), |
||||||
|
) |
||||||
|
) { |
||||||
|
return { |
||||||
|
section: sectionName, |
||||||
|
field: fieldName, |
||||||
|
value: contentField.value, |
||||||
|
tags: contentField.tags, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
interface ContentItem { |
||||||
|
path: string; |
||||||
|
value: string | null; |
||||||
|
tags: string[]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get all content as a flat list with their tags |
||||||
|
* Useful for AI tools to understand available content |
||||||
|
*/ |
||||||
|
export function getAllContent(): ContentItem[] { |
||||||
|
const sections = { hero, about, contact } as const; |
||||||
|
const content: ContentItem[] = []; |
||||||
|
|
||||||
|
for (const sectionName of Object.keys(sections) as SectionName[]) { |
||||||
|
const section = sections[sectionName]; |
||||||
|
for (const [fieldName, field] of Object.entries(section)) { |
||||||
|
const contentField = field as ContentField; |
||||||
|
content.push({ |
||||||
|
path: `${sectionName}.${fieldName}`, |
||||||
|
value: contentField.value, |
||||||
|
tags: contentField.tags || [], |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
return content; |
||||||
|
} |
||||||
@ -1,14 +1,14 @@ |
|||||||
import './assets/main.css' |
import "./styles/global.css"; |
||||||
|
|
||||||
import { createApp } from 'vue' |
import { createApp } from "vue"; |
||||||
import { createPinia } from 'pinia' |
import { createPinia } from "pinia"; |
||||||
|
|
||||||
import App from './App.vue' |
import App from "./App.vue"; |
||||||
import router from './router' |
import router from "./router"; |
||||||
|
|
||||||
const app = createApp(App) |
const app = createApp(App); |
||||||
|
|
||||||
app.use(createPinia()) |
app.use(createPinia()); |
||||||
app.use(router) |
app.use(router); |
||||||
|
|
||||||
app.mount('#app') |
app.mount("#app"); |
||||||
|
|||||||
@ -1,23 +1,23 @@ |
|||||||
import { createRouter, createWebHistory } from 'vue-router' |
import { createRouter, createWebHistory } from "vue-router"; |
||||||
import HomeView from '../views/HomeView.vue' |
import HomeView from "../views/HomeView.vue"; |
||||||
|
|
||||||
const router = createRouter({ |
const router = createRouter({ |
||||||
history: createWebHistory(import.meta.env.BASE_URL), |
history: createWebHistory(import.meta.env.BASE_URL), |
||||||
routes: [ |
routes: [ |
||||||
{ |
{ |
||||||
path: '/', |
path: "/", |
||||||
name: 'home', |
name: "home", |
||||||
component: HomeView, |
component: HomeView, |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
path: '/about', |
path: "/about", |
||||||
name: 'about', |
name: "about", |
||||||
// route level code-splitting
|
// route level code-splitting
|
||||||
// this generates a separate chunk (About.[hash].js) for this route
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/AboutView.vue'), |
component: () => import("../views/AboutView.vue"), |
||||||
}, |
}, |
||||||
], |
], |
||||||
}) |
}); |
||||||
|
|
||||||
export default router |
export default router; |
||||||
|
|||||||
@ -1,12 +1,12 @@ |
|||||||
import { ref, computed } from 'vue' |
import { ref, computed } from "vue"; |
||||||
import { defineStore } from 'pinia' |
import { defineStore } from "pinia"; |
||||||
|
|
||||||
export const useCounterStore = defineStore('counter', () => { |
export const useCounterStore = defineStore("counter", () => { |
||||||
const count = ref(0) |
const count = ref(0); |
||||||
const doubleCount = computed(() => count.value * 2) |
const doubleCount = computed(() => count.value * 2); |
||||||
function increment() { |
function increment() { |
||||||
count.value++ |
count.value++; |
||||||
} |
} |
||||||
|
|
||||||
return { count, doubleCount, increment } |
return { count, doubleCount, increment }; |
||||||
}) |
}); |
||||||
|
|||||||
@ -0,0 +1,118 @@ |
|||||||
|
/** |
||||||
|
* Global Styles - lukastitch.corneruniverse.com |
||||||
|
* |
||||||
|
* Base styles and resets that apply site-wide. |
||||||
|
* Uses variables from variables.css for theming. |
||||||
|
*/ |
||||||
|
|
||||||
|
@import "./variables.css"; |
||||||
|
|
||||||
|
/* Import Google Fonts */ |
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700&family=Quicksand:wght@500;600;700&display=swap"); |
||||||
|
|
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
html { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
min-height: 100vh; |
||||||
|
font-family: var(--font-body); |
||||||
|
font-size: var(--font-size-base); |
||||||
|
line-height: 1.6; |
||||||
|
color: var(--color-text); |
||||||
|
background-color: var(--color-background); |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
} |
||||||
|
|
||||||
|
#app { |
||||||
|
min-height: 100vh; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
/* Typography */ |
||||||
|
h1, |
||||||
|
h2, |
||||||
|
h3, |
||||||
|
h4, |
||||||
|
h5, |
||||||
|
h6 { |
||||||
|
font-family: var(--font-heading); |
||||||
|
color: var(--color-heading); |
||||||
|
line-height: 1.3; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: var(--font-size-3xl); |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: var(--font-size-2xl); |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: var(--font-size-xl); |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-bottom: var(--space-md); |
||||||
|
} |
||||||
|
|
||||||
|
/* Links */ |
||||||
|
a { |
||||||
|
color: var(--color-accent); |
||||||
|
text-decoration: none; |
||||||
|
transition: color var(--transition-fast); |
||||||
|
} |
||||||
|
|
||||||
|
a:hover { |
||||||
|
color: var(--color-accent-hover); |
||||||
|
} |
||||||
|
|
||||||
|
/* Images */ |
||||||
|
img { |
||||||
|
max-width: 100%; |
||||||
|
height: auto; |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
/* Buttons */ |
||||||
|
button { |
||||||
|
font-family: inherit; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
/* Section wrapper utility */ |
||||||
|
.section { |
||||||
|
padding: var(--space-xl) var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
max-width: var(--max-width); |
||||||
|
margin: 0 auto; |
||||||
|
padding: 0 var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
/* Focus states for accessibility */ |
||||||
|
:focus-visible { |
||||||
|
outline: 2px solid var(--color-primary); |
||||||
|
outline-offset: 2px; |
||||||
|
} |
||||||
|
|
||||||
|
/* Selection color */ |
||||||
|
::selection { |
||||||
|
background-color: var(--color-primary-light); |
||||||
|
color: var(--color-heading); |
||||||
|
} |
||||||
@ -0,0 +1,132 @@ |
|||||||
|
/** |
||||||
|
* Design System Variables - lukastitch.corneruniverse.com |
||||||
|
* |
||||||
|
* Color Palette: Pastel pink, purple, and grey |
||||||
|
* Mood: Warm, cozy, handcrafted feel |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* When user mentions colors: |
||||||
|
* - "primary color" or "main color" -> --color-primary (pastel pink) |
||||||
|
* - "secondary color" or "accent" -> --color-secondary (pastel purple) |
||||||
|
* - "background" -> --color-background |
||||||
|
* - "make it more pink/purple/grey" -> adjust the relevant color values |
||||||
|
*/ |
||||||
|
|
||||||
|
:root { |
||||||
|
/* ============================================ |
||||||
|
PASTEL COLOR PALETTE |
||||||
|
Pink, Purple, and Grey as requested |
||||||
|
============================================ */ |
||||||
|
|
||||||
|
/* Primary: Soft Pastel Pink */ |
||||||
|
--color-primary: #f4a5c0; |
||||||
|
--color-primary-light: #fcd5e5; |
||||||
|
--color-primary-dark: #e87aa0; |
||||||
|
|
||||||
|
/* Secondary: Soft Pastel Purple */ |
||||||
|
--color-secondary: #c9a5f4; |
||||||
|
--color-secondary-light: #e5d5fc; |
||||||
|
--color-secondary-dark: #a07ae8; |
||||||
|
|
||||||
|
/* Neutral: Warm Grey */ |
||||||
|
--color-grey: #9e9e9e; |
||||||
|
--color-grey-light: #e0e0e0; |
||||||
|
--color-grey-dark: #6e6e6e; |
||||||
|
|
||||||
|
/* Background Colors */ |
||||||
|
--color-background: #fefcfd; |
||||||
|
--color-background-soft: #fdf5f8; |
||||||
|
--color-background-mute: #f8eef2; |
||||||
|
|
||||||
|
/* Text Colors */ |
||||||
|
--color-text: #4a4a4a; |
||||||
|
--color-text-light: #6e6e6e; |
||||||
|
--color-heading: #3d3d3d; |
||||||
|
|
||||||
|
/* Border Colors */ |
||||||
|
--color-border: #e8dde2; |
||||||
|
--color-border-hover: #d4c4cc; |
||||||
|
|
||||||
|
/* Accent for links and interactive elements */ |
||||||
|
--color-accent: #c9a5f4; |
||||||
|
--color-accent-hover: #a07ae8; |
||||||
|
|
||||||
|
/* Status Colors */ |
||||||
|
--color-available: #a5d6a7; |
||||||
|
--color-sold: #ef9a9a; |
||||||
|
|
||||||
|
/* ============================================ |
||||||
|
TYPOGRAPHY |
||||||
|
============================================ */ |
||||||
|
|
||||||
|
--font-heading: "Quicksand", "Nunito", sans-serif; |
||||||
|
--font-body: |
||||||
|
"Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
||||||
|
|
||||||
|
--font-size-xs: 0.75rem; |
||||||
|
--font-size-sm: 0.875rem; |
||||||
|
--font-size-base: 1rem; |
||||||
|
--font-size-lg: 1.25rem; |
||||||
|
--font-size-xl: 1.5rem; |
||||||
|
--font-size-2xl: 2rem; |
||||||
|
--font-size-3xl: 2.5rem; |
||||||
|
|
||||||
|
/* ============================================ |
||||||
|
SPACING |
||||||
|
============================================ */ |
||||||
|
|
||||||
|
--space-xs: 0.25rem; |
||||||
|
--space-sm: 0.5rem; |
||||||
|
--space-md: 1rem; |
||||||
|
--space-lg: 2rem; |
||||||
|
--space-xl: 4rem; |
||||||
|
--space-2xl: 6rem; |
||||||
|
|
||||||
|
--section-gap: 4rem; |
||||||
|
|
||||||
|
/* ============================================ |
||||||
|
LAYOUT |
||||||
|
============================================ */ |
||||||
|
|
||||||
|
--max-width: 1200px; |
||||||
|
--content-width: 800px; |
||||||
|
--gallery-gap: 1.5rem; |
||||||
|
--card-radius: 12px; |
||||||
|
--border-radius: 8px; |
||||||
|
|
||||||
|
/* ============================================ |
||||||
|
SHADOWS |
||||||
|
============================================ */ |
||||||
|
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); |
||||||
|
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); |
||||||
|
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); |
||||||
|
|
||||||
|
/* ============================================ |
||||||
|
TRANSITIONS |
||||||
|
============================================ */ |
||||||
|
|
||||||
|
--transition-fast: 0.15s ease; |
||||||
|
--transition-base: 0.3s ease; |
||||||
|
--transition-slow: 0.5s ease; |
||||||
|
} |
||||||
|
|
||||||
|
/* Dark mode adjustments - keeping the pastel feel but adjusted for dark backgrounds */ |
||||||
|
@media (prefers-color-scheme: dark) { |
||||||
|
:root { |
||||||
|
--color-background: #2d2a2c; |
||||||
|
--color-background-soft: #363133; |
||||||
|
--color-background-mute: #403b3d; |
||||||
|
|
||||||
|
--color-text: #e8e4e6; |
||||||
|
--color-text-light: #b8b4b6; |
||||||
|
--color-heading: #f4f0f2; |
||||||
|
|
||||||
|
--color-border: #4a4547; |
||||||
|
--color-border-hover: #5a5557; |
||||||
|
|
||||||
|
/* Pastels stay similar but slightly more saturated for visibility */ |
||||||
|
--color-primary: #f4a5c0; |
||||||
|
--color-secondary: #c9a5f4; |
||||||
|
} |
||||||
|
} |
||||||
@ -1,15 +1,53 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
/** |
||||||
|
* AboutView - About page |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* To change the about content, edit /src/content/site-data.js |
||||||
|
* Look for the "about" section with fields: heading, text, image |
||||||
|
*/ |
||||||
|
import { about } from "@/content/site-data"; |
||||||
|
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<div class="about"> |
<main class="about-page"> |
||||||
<h1>This is an about page</h1> |
<section class="about-section"> |
||||||
|
<div class="about-container"> |
||||||
|
<h1>{{ about.heading.value }}</h1> |
||||||
|
<p class="about-text">{{ about.text.value }}</p> |
||||||
</div> |
</div> |
||||||
|
</section> |
||||||
|
</main> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<style> |
<style scoped> |
||||||
@media (min-width: 1024px) { |
.about-page { |
||||||
.about { |
flex: 1; |
||||||
min-height: 100vh; |
} |
||||||
display: flex; |
|
||||||
align-items: center; |
.about-section { |
||||||
} |
padding: var(--space-2xl) var(--space-lg); |
||||||
|
background: linear-gradient( |
||||||
|
180deg, |
||||||
|
var(--color-background-soft) 0%, |
||||||
|
var(--color-background) 100% |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
.about-container { |
||||||
|
max-width: var(--content-width); |
||||||
|
margin: 0 auto; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.about-container h1 { |
||||||
|
color: var(--color-primary-dark); |
||||||
|
margin-bottom: var(--space-lg); |
||||||
|
} |
||||||
|
|
||||||
|
.about-text { |
||||||
|
font-size: var(--font-size-lg); |
||||||
|
color: var(--color-text); |
||||||
|
line-height: 1.8; |
||||||
} |
} |
||||||
</style> |
</style> |
||||||
|
|||||||
@ -1,9 +1,25 @@ |
|||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import TheWelcome from '../components/TheWelcome.vue' |
/** |
||||||
|
* HomeView - Homepage |
||||||
|
* |
||||||
|
* AI CODING TOOL INSTRUCTIONS: |
||||||
|
* This is the main landing page. It contains: |
||||||
|
* - HeroSection: The main title, tagline, and description |
||||||
|
* |
||||||
|
* To change the title/subtitle/description, edit /src/content/site-data.js |
||||||
|
* To add new sections, import and add them below HeroSection |
||||||
|
*/ |
||||||
|
import HeroSection from "@/components/sections/HeroSection.vue"; |
||||||
</script> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<main> |
<main> |
||||||
<TheWelcome /> |
<HeroSection /> |
||||||
</main> |
</main> |
||||||
</template> |
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
main { |
||||||
|
flex: 1; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|||||||
Loading…
Reference in new issue