Compare commits

...

3 commits

Author SHA1 Message Date
c7d443f236 docs: update README for Gradle at root, add convenience task docs
Update all references to match the new repo-root Gradle layout
after moving the wrapper out of backend/.

- Quick Start: add ./gradlew up alternative and hint at ./gradlew check
- Spring profiles: ./gradlew bootRun → ./gradlew :backend:bootRun
- Development section: add All-in-one subsection with check/up/down/reset
- Backend dev: cd backend && ./gradlew bootRun → ./gradlew :backend:bootRun
- Development vs Production table: ./gradlew bootRun → ./gradlew :backend:bootRun
- Project Structure tree: add gradlew, gradle/, settings.gradle, build.gradle
- Remove ARCHITECTURE.md reference (file never existed)
- Add Database reset section with ./gradlew reset

Also add .gradle/ and build/ to .gitignore with gradle-wrapper.jar
exception (was staged but not committed with previous refactor).
2026-05-01 18:44:05 +02:00
d70196112d refactor: move Gradle wrapper to repo root, add convenience tasks
Move gradlew, gradle/wrapper, and settings.gradle from backend/ to
the repo root so build commands run from the top-level directory.
This follows the standard multi-project Gradle layout where the build
tool lives alongside docker-compose.yml and all submodules.

- Move gradlew + gradle/wrapper/* from backend/ to repo root
- Move settings.gradle to root with rootProject.name and include 'backend'
- Create root build.gradle with convenience tasks: check, up, down, reset
- check task chains frontend lint → frontend test → backend check
- Update docker-compose.yml backend volume from ./backend:/app to .:/app
- Update backend.Dockerfile entrypoint to ./gradlew :backend:bootRun
- Update AGENTS.md: document ./gradlew check, up, down, reset
- Delete backend/settings.gradle (now at root)
- Add .gradle/ and build/ to .gitignore
- Add !gradle/wrapper/gradle-wrapper.jar exception (blocked by *.jar rule)

All 38 frontend tests and 33 backend tests pass via ./gradlew check.
2026-05-01 18:40:18 +02:00
4c6094446b feat: add app shell with header, footer, and compose flow
Add AppHeader and AppFooter to give the site a consistent chrome
around the core page content. Add ComposePage stub reachable via
"Skicka ett brev till ägaren" CTA on HomePage after vehicle lookup
succeeds. Add stub pages for about, contact, and privacy.

- Create AppHeader.vue with logo link (BilHälsning) and Hem nav link
- Create AppFooter.vue with 4 links: Om oss, Kontakt, Integritetspolicy, Villkor
- Create ComposePage.vue stub that reads plate from route query params
- Create AboutPage.vue and ContactPage.vue stub pages
- Add 4 new routes: /compose, /om, /kontakt, /integritetspolicy
- Update App.vue to render AppHeader + <main> + AppFooter around RouterView
- Add home__cta RouterLink button to HomePage, visible only when vehicle
  lookup succeeds, linking to /compose?plate=<plate>
- Remove BilHälsning h1 from HomePage (moved to header)
- Add 17 new tests: AppHeader (2), AppFooter (1), ComposePage (3),
  AboutPage (1), ContactPage (1), HomePage rewrite (6), App update (2)
- Update App.spec.ts to verify header/footer components render
2026-05-01 18:19:53 +02:00
25 changed files with 548 additions and 25 deletions

5
.gitignore vendored
View file

@ -10,6 +10,7 @@ target/
*.jar
*.war
!.mvn/wrapper/maven-wrapper.jar
!gradle/wrapper/gradle-wrapper.jar
# Environment
.env
@ -39,6 +40,10 @@ Thumbs.db
docker-compose.override.yml
certs/
# Gradle
.gradle/
build/
# Java
*.hprof

View file

@ -24,11 +24,23 @@ PostgreSQL 16. Deployed via Docker Compose.
Always run these after making changes to verify nothing is broken.
Gradle lives at repo root. All commands below run from the repo root unless noted.
### Quick start (everything)
```bash
cp .env.example .env # first time only, then fill in keys
docker compose up -d # starts postgres, backend, frontend
./gradlew up # same as above (Gradle wrapper)
```
### All-in-one
```bash
./gradlew check # frontend lint → frontend test → backend test → integration test
./gradlew up # docker compose up -d
./gradlew down # docker compose down
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
```
### Frontend (Vue.js 3 + Vite)
@ -45,10 +57,8 @@ npm run test # vitest
### Backend (Spring Boot 4 + Java 21)
```bash
cd backend
./gradlew bootRun # dev server on :8080
./gradlew test # JUnit 5 + Mockito
./gradlew check # full verification including integration tests
./gradlew :backend:bootRun # dev server on :8080
./gradlew :backend:test # JUnit 5 + Mockito (backend only)
```
### Stripe webhooks (local testing)
@ -80,7 +90,8 @@ bilhej/
│ │ ├── router/ # Vue Router config
│ │ └── assets/ # Static files, CSS
│ └── ...
├── backend/ # Spring Boot 4 (Java 21)
├── backend/ # Spring Boot 4 (Java 21) — Gradle subproject
│ ├── build.gradle # Spring Boot plugin, Java deps, test config
│ ├── src/main/java/se/bilhalsning/
│ │ ├── config/ # @Configuration classes
│ │ ├── controller/ # REST controllers
@ -96,7 +107,7 @@ bilhej/
│ ├── application-docker.yml # docker profile (PostgreSQL)
│ └── db/migration/ # Flyway migrations
├── docker/ # Dockerfiles
│ ├── backend.Dockerfile # dev: JDK 21 + gradle bootRun
│ ├── backend.Dockerfile # dev: JDK 21 + gradle :backend:bootRun
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle build → JRE Alpine, non-root)
│ ├── frontend.Dockerfile # dev: Node 24 + vite dev server
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node build → nginx)
@ -104,6 +115,11 @@ bilhej/
│ └── entrypoint.sh # prod: self-signed cert generation
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
├── docker-compose.prod.yml # prod: multi-stage images, no source mounts, restart always
├── gradlew # Gradle wrapper (repo root)
├── gradle/
│ └── wrapper/
├── settings.gradle # rootProject.name + include 'backend'
├── build.gradle # convenience tasks: check, up, down, reset
├── .env.example
├── AGENTS.md # This file
├── README.md

View file

@ -34,7 +34,7 @@ The user enters a registration number, composes a letter (from a template or fre
git clone <repo-url> bilhej
cd bilhej
cp .env.example .env # fill in your keys
docker compose up -d
docker compose up -d # or: ./gradlew up
```
The app will be available at:
@ -72,7 +72,7 @@ No CORS configuration needed in development — the browser never calls the back
**Spring profiles:**
| Profile | Datasource | Use |
|---------|-----------|-----|
| default | H2 in-memory | Local IDE dev (`./gradlew bootRun`) |
| default | H2 in-memory | Local IDE dev (`./gradlew :backend:bootRun`) |
| `docker` | PostgreSQL via `docker-compose.yml` | Docker Compose dev |
| `prod` | PostgreSQL (production config) | Deploy (`docker-compose.prod.yml`) |
@ -130,17 +130,21 @@ bilhej/
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
├── docker-compose.prod.yml # prod: multi-stage builds, no source mounts, restart: unless-stopped
├── docker/
│ ├── backend.Dockerfile # dev: JDK + gradle bootRun
│ ├── backend.Dockerfile # dev: JDK + gradle :backend:bootRun
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle → JRE Alpine, non-root)
│ ├── frontend.Dockerfile # dev: Node + vite dev server
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node → nginx)
│ ├── nginx.conf # prod: SPA fallback + /api proxy
│ └── entrypoint.sh # prod: self-signed cert generation
├── gradlew # Gradle wrapper (run from repo root)
├── gradle/
│ └── wrapper/
├── settings.gradle # rootProject.name + include 'backend'
├── build.gradle # convenience tasks: check, up, down, reset
├── .env.example
├── README.md
├── REQUIREMENTS.md
├── CODING_GUIDELINES.md
└── ARCHITECTURE.md
└── CODING_GUIDELINES.md
```
---
@ -149,7 +153,7 @@ bilhej/
| Aspect | `docker compose up -d` | `docker compose -f docker-compose.prod.yml up -d` |
|--------|------------------------|---------------------------------------------------|
| Backend | `./gradlew bootRun` (compiles on change) | Multi-stage build → `java -jar app.jar` |
| Backend | `./gradlew :backend:bootRun` (compiles on change) | Multi-stage build → `java -jar app.jar` |
| Backend image | `eclipse-temurin:21-jdk` (~400 MB) | `eclipse-temurin:21-jre-alpine` (~200 MB) |
| Backend user | root | `bilhej` (non-root) |
| Frontend | Vite dev server (HMR, `--host 0.0.0.0`) | nginx serving static `dist/` |
@ -163,19 +167,27 @@ bilhej/
## Development
### All-in-one (from repo root)
```bash
./gradlew check # lint → frontend test → backend test → integration test
./gradlew up # docker compose up -d
./gradlew down # docker compose down
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
```
### Frontend (dev server with HMR)
```bash
cd frontend
npm install
npm run dev
npm install # first time only
npm run dev # :3000 with HMR
```
### Backend (IDE or CLI)
```bash
cd backend
./gradlew bootRun
./gradlew :backend:bootRun # :8080, profile: default (H2)
```
### Stripe Webhooks (local testing)
@ -184,10 +196,15 @@ cd backend
stripe listen --forward-to localhost:8080/api/webhooks/stripe
```
### Database reset
```bash
./gradlew reset # wipes DB volume and restarts containers
```
---
## Related Documents
- [REQUIREMENTS.md](./REQUIREMENTS.md) — Full product requirements and business model
- [CODING_GUIDELINES.md](./CODING_GUIDELINES.md) — Code conventions and standards
- [ARCHITECTURE.md](./ARCHITECTURE.md) — Detailed architecture and data flow

38
build.gradle Normal file
View file

@ -0,0 +1,38 @@
plugins {
id 'base'
}
tasks.register('frontendLint', Exec) {
description = 'Run ESLint in the frontend directory'
workingDir = file("${rootProject.projectDir}/frontend")
commandLine 'npm', 'run', 'lint'
}
tasks.register('frontendTest', Exec) {
description = 'Run Vitest in the frontend directory'
dependsOn frontendLint
workingDir = file("${rootProject.projectDir}/frontend")
commandLine 'npm', 'run', 'test'
}
tasks.named('check').configure {
dependsOn frontendLint, frontendTest
}
tasks.register('up', Exec) {
description = 'Start all services via Docker Compose'
workingDir = rootProject.projectDir
commandLine 'docker', 'compose', 'up', '-d'
}
tasks.register('down', Exec) {
description = 'Stop all Docker Compose services'
workingDir = rootProject.projectDir
commandLine 'docker', 'compose', 'down'
}
tasks.register('reset', Exec) {
description = 'Wipe database and restart all services'
workingDir = rootProject.projectDir
commandLine 'bash', '-c', 'docker compose down -v && docker compose up -d'
}

View file

@ -36,7 +36,7 @@ services:
postgres:
condition: service_healthy
volumes:
- ./backend:/app
- .:/app
- gradle-cache:/root/.gradle
frontend:

View file

@ -1,3 +1,3 @@
FROM eclipse-temurin:21-jdk
WORKDIR /app
ENTRYPOINT ["./gradlew", "bootRun", "--no-daemon"]
ENTRYPOINT ["./gradlew", ":backend:bootRun", "--no-daemon"]

View file

@ -1,7 +1,19 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import AppHeader from '@/components/AppHeader.vue'
import AppFooter from '@/components/AppFooter.vue'
</script>
<template>
<RouterView />
<AppHeader />
<main class="app__main">
<RouterView />
</main>
<AppFooter />
</template>
<style>
.app__main {
min-height: calc(100vh - 12rem);
}
</style>

View file

@ -0,0 +1,10 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import AboutPage from '@/pages/AboutPage.vue'
describe('AboutPage', () => {
it('renders heading', () => {
const wrapper = mount(AboutPage)
expect(wrapper.text()).toContain('Om BilHälsning')
})
})

View file

@ -1,9 +1,23 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import App from '@/App.vue'
import AppHeader from '@/components/AppHeader.vue'
import AppFooter from '@/components/AppFooter.vue'
import router from '@/router'
describe('App', () => {
it('renders AppHeader and AppFooter', async () => {
router.push('/')
await router.isReady()
const wrapper = mount(App, {
global: {
plugins: [router],
},
})
expect(wrapper.findComponent(AppHeader).exists()).toBe(true)
expect(wrapper.findComponent(AppFooter).exists()).toBe(true)
})
it('renders RouterView with HomePage content', async () => {
router.push('/')
await router.isReady()
@ -12,6 +26,6 @@ describe('App', () => {
plugins: [router],
},
})
expect(wrapper.text()).toContain('BilHälsning')
expect(wrapper.text()).toContain('Skicka ett brev till en fordonsägare')
})
})

View file

@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import AppFooter from '@/components/AppFooter.vue'
function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/om',
name: 'about',
component: { template: '<div>About</div>' },
},
{
path: '/kontakt',
name: 'contact',
component: { template: '<div>Contact</div>' },
},
{
path: '/integritetspolicy',
name: 'privacy',
component: { template: '<div>Privacy</div>' },
},
{
path: '/villkor',
name: 'terms',
component: { template: '<div>Terms</div>' },
},
],
})
}
describe('AppFooter', () => {
it('renders all four links', () => {
const router = createTestRouter()
const wrapper = mount(AppFooter, {
global: { plugins: [router] },
})
const links = wrapper.findAll('a')
expect(links[0].text()).toBe('Om oss')
expect(links[0].attributes('href')).toBe('/om')
expect(links[1].text()).toBe('Kontakt')
expect(links[1].attributes('href')).toBe('/kontakt')
expect(links[2].text()).toBe('Integritetspolicy')
expect(links[2].attributes('href')).toBe('/integritetspolicy')
expect(links[3].text()).toBe('Villkor')
expect(links[3].attributes('href')).toBe('/villkor')
})
})

View file

@ -0,0 +1,33 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import AppHeader from '@/components/AppHeader.vue'
function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
],
})
}
describe('AppHeader', () => {
it('renders the logo text', () => {
const router = createTestRouter()
const wrapper = mount(AppHeader, {
global: { plugins: [router] },
})
expect(wrapper.text()).toContain('BilHälsning')
})
it('has a link to home', () => {
const router = createTestRouter()
const wrapper = mount(AppHeader, {
global: { plugins: [router] },
})
const links = wrapper.findAll('a')
const homeLink = links.find((a) => a.attributes('href') === '/')
expect(homeLink).toBeTruthy()
})
})

View file

@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import ComposePage from '@/pages/ComposePage.vue'
function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [{ path: '/compose', name: 'compose', component: ComposePage }],
})
}
describe('ComposePage', () => {
it('renders heading', async () => {
const router = createTestRouter()
router.push('/compose')
await router.isReady()
const wrapper = mount(ComposePage, {
global: { plugins: [router] },
})
expect(wrapper.text()).toContain('Skriv ditt brev')
})
it('displays plate from query param', async () => {
const router = createTestRouter()
router.push({ path: '/compose', query: { plate: 'ABC123' } })
await router.isReady()
const wrapper = mount(ComposePage, {
global: { plugins: [router] },
})
expect(wrapper.text()).toContain('ABC123')
})
it('does not show plate when no query param', async () => {
const router = createTestRouter()
router.push('/compose')
await router.isReady()
const wrapper = mount(ComposePage, {
global: { plugins: [router] },
})
expect(wrapper.find('.compose__plate').exists()).toBe(false)
})
})

View file

@ -0,0 +1,10 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ContactPage from '@/pages/ContactPage.vue'
describe('ContactPage', () => {
it('renders heading', () => {
const wrapper = mount(ContactPage)
expect(wrapper.text()).toContain('Kontakta oss')
})
})

View file

@ -1,10 +1,85 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import HomePage from '@/pages/HomePage.vue'
import ComposePage from '@/pages/ComposePage.vue'
function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: HomePage },
{ path: '/compose', name: 'compose', component: ComposePage },
],
})
}
function mountHome(router: ReturnType<typeof createTestRouter>) {
return mount(HomePage, {
global: { plugins: [router] },
})
}
describe('HomePage', () => {
it('mounts successfully', () => {
const wrapper = mount(HomePage)
expect(wrapper.text()).toContain('BilHälsning')
it('renders subtitle', () => {
const router = createTestRouter()
const wrapper = mountHome(router)
expect(wrapper.text()).toContain('Skicka ett brev till en fordonsägare')
})
it('does not show CTA button initially', () => {
const router = createTestRouter()
const wrapper = mountHome(router)
expect(wrapper.find('.home__cta').exists()).toBe(false)
})
it('does not show CTA while loading', async () => {
const router = createTestRouter()
const wrapper = mountHome(router)
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
await plateInput.vm.$emit('lookup', 'ABC123')
await wrapper.vm.$nextTick()
expect(wrapper.find('.home__cta').exists()).toBe(false)
})
it('does not show CTA after not-found', async () => {
const router = createTestRouter()
const wrapper = mountHome(router)
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
await plateInput.vm.$emit('lookup', 'UNKNOWN')
await new Promise((resolve) => setTimeout(resolve, 500))
expect(wrapper.find('.home__cta').exists()).toBe(false)
})
it('shows CTA button when vehicle data present', async () => {
const router = createTestRouter()
const wrapper = mountHome(router)
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
await plateInput.vm.$emit('update:modelValue', 'ABC123')
await plateInput.vm.$emit('lookup', 'ABC123')
await new Promise((resolve) => setTimeout(resolve, 500))
const cta = wrapper.find('.home__cta')
expect(cta.exists()).toBe(true)
expect(cta.text()).toBe('Skicka ett brev till ägaren')
})
it('CTA links to compose page with plate query param', async () => {
const router = createTestRouter()
const wrapper = mountHome(router)
const plateInput = wrapper.findComponent({ name: 'PlateInput' })
await plateInput.vm.$emit('update:modelValue', 'ABC123')
await plateInput.vm.$emit('lookup', 'ABC123')
await new Promise((resolve) => setTimeout(resolve, 500))
const cta = wrapper.find('.home__cta')
const href = cta.attributes('href')
expect(href).toBe('/compose?plate=ABC123')
})
})

View file

@ -0,0 +1,42 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<template>
<footer class="app-footer">
<nav class="app-footer__links">
<RouterLink to="/om" class="app-footer__link">Om oss</RouterLink>
<RouterLink to="/kontakt" class="app-footer__link">Kontakt</RouterLink>
<RouterLink to="/integritetspolicy" class="app-footer__link"
>Integritetspolicy</RouterLink
>
<RouterLink to="/villkor" class="app-footer__link">Villkor</RouterLink>
</nav>
</footer>
</template>
<style scoped>
.app-footer {
background: #f7fafc;
border-top: 1px solid #e2e8f0;
padding: 1.5rem;
text-align: center;
}
.app-footer__links {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.app-footer__link {
color: #718096;
text-decoration: none;
font-size: 0.8125rem;
}
.app-footer__link:hover {
color: #1a202c;
}
</style>

View file

@ -0,0 +1,45 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<template>
<header class="app-header">
<RouterLink to="/" class="app-header__logo">BilHälsning</RouterLink>
<nav class="app-header__nav">
<RouterLink to="/" class="app-header__link">Hem</RouterLink>
</nav>
</header>
</template>
<style scoped>
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e2e8f0;
background: #fff;
}
.app-header__logo {
font-size: 1.25rem;
font-weight: 700;
color: #1a202c;
text-decoration: none;
}
.app-header__nav {
display: flex;
gap: 1rem;
}
.app-header__link {
color: #4a5568;
text-decoration: none;
font-size: 0.875rem;
}
.app-header__link:hover {
color: #1a202c;
}
</style>

View file

@ -0,0 +1,19 @@
<script setup lang="ts"></script>
<template>
<div class="about">
<h1>Om BilHälsning</h1>
<p>
BilHälsning är en tjänst som låter dig skicka fysiska brev till
fordonsägare via registreringsnummer.
</p>
</div>
</template>
<style scoped>
.about {
max-width: 28rem;
margin: 3rem auto 0;
padding: 0 1rem;
}
</style>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const plate = (route.query.plate as string) || ''
</script>
<template>
<div class="compose">
<h1>Skriv ditt brev</h1>
<p v-if="plate" class="compose__plate">Registreringsnummer: {{ plate }}</p>
</div>
</template>
<style scoped>
.compose {
max-width: 28rem;
margin: 3rem auto 0;
padding: 0 1rem;
}
.compose__plate {
color: #4a5568;
font-size: 0.875rem;
}
</style>

View file

@ -0,0 +1,19 @@
<script setup lang="ts"></script>
<template>
<div class="contact">
<h1>Kontakta oss</h1>
<p>
Har du frågor eller feedback? Hör av dig till oss
<a href="mailto:hej@bilhalsning.se">hej@bilhalsning.se</a>.
</p>
</div>
</template>
<style scoped>
.contact {
max-width: 28rem;
margin: 3rem auto 0;
padding: 0 1rem;
}
</style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import PlateInput from '@/components/PlateInput.vue'
import VehicleInfo from '@/components/VehicleInfo.vue'
import type { VehicleInfo as VehicleData } from '@/components/VehicleInfo.vue'
@ -36,7 +37,6 @@ function handleLookup(lookedUpPlate: string) {
<template>
<div class="home">
<h1>BilHälsning</h1>
<p class="home__subtitle">Skicka ett brev till en fordonsägare</p>
<PlateInput v-model="plate" @lookup="handleLookup" />
@ -47,6 +47,14 @@ function handleLookup(lookedUpPlate: string) {
:not-found="notFound"
:plate="plate"
/>
<RouterLink
v-if="vehicle"
:to="{ name: 'compose', query: { plate } }"
class="home__cta"
>
Skicka ett brev till ägaren
</RouterLink>
</div>
</template>
@ -61,4 +69,21 @@ function handleLookup(lookedUpPlate: string) {
color: #718096;
margin: 0 0 1.5rem 0;
}
.home__cta {
display: block;
margin-top: 1.5rem;
padding: 0.875rem 1.5rem;
background: #38a169;
color: #fff;
text-align: center;
text-decoration: none;
font-weight: 600;
border-radius: 0.5rem;
font-size: 1rem;
}
.home__cta:hover {
background: #2f855a;
}
</style>

View file

@ -1,5 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomePage from '@/pages/HomePage.vue'
import ComposePage from '@/pages/ComposePage.vue'
import AboutPage from '@/pages/AboutPage.vue'
import ContactPage from '@/pages/ContactPage.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -9,6 +12,21 @@ const router = createRouter({
name: 'home',
component: HomePage,
},
{
path: '/compose',
name: 'compose',
component: ComposePage,
},
{
path: '/om',
name: 'about',
component: AboutPage,
},
{
path: '/kontakt',
name: 'contact',
component: ContactPage,
},
],
})

View file

View file

@ -1 +1,3 @@
rootProject.name = 'bilhej'
include 'backend'