From 96508d63cd556815b4638df845497dc1e4bff108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20M=C3=B6rling?= Date: Thu, 14 May 2026 17:39:21 +0200 Subject: [PATCH] feat: add template picker modal to compose page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LetterTemplate.icon field and 7th template 'Mindre parkeringsskada' (đŸ…żïž) - Create TemplatePicker.vue component: modal overlay with 2-column card grid, emits 'select' and 'close' events, closes on overlay click - Add '✹ Visa mallar' pill button above textarea in ComposePage - Clicking button opens TemplatePicker modal, selecting a template fills textarea and closes modal - Style button as pill/badge with light blue background and icon - Add 7 Vitest tests for TemplatePicker (renders cards, emits events, close behavior, parking damage template) - Add 4 Vitest tests for ComposePage template picker integration - Add 2 Playwright E2E tests (opens picker, fills textarea and closes) --- frontend/e2e/compose.spec.ts | 35 ++++ frontend/src/__tests__/ComposePage.spec.ts | 39 +++++ frontend/src/__tests__/TemplatePicker.spec.ts | 59 +++++++ frontend/src/components/TemplatePicker.vue | 151 ++++++++++++++++++ frontend/src/data/templates.ts | 23 ++- frontend/src/pages/ComposePage.vue | 49 +++++- 6 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 frontend/src/__tests__/TemplatePicker.spec.ts create mode 100644 frontend/src/components/TemplatePicker.vue diff --git a/frontend/e2e/compose.spec.ts b/frontend/e2e/compose.spec.ts index d4a9e96..b789b84 100644 --- a/frontend/e2e/compose.spec.ts +++ b/frontend/e2e/compose.spec.ts @@ -84,4 +84,39 @@ test.describe('Compose flow', () => { ).toBeVisible() await expect(page.getByText('Transportstyrelsens fordonsregister')).toBeVisible() }) + + test('Visa mallar button opens template picker', async ({ page }) => { + await page.goto('/logga-in') + await page.getByLabel('E-postadress').fill('test@bilhalsning.se') + await page.getByLabel('Lösenord').fill('test1234') + await page.getByRole('button', { name: 'Logga in' }).click() + await page.waitForURL('/') + + await page.goto('/compose?plate=ABC123') + + await page.getByRole('button', { name: 'Visa mallar' }).click() + + await expect(page.getByRole('heading', { name: 'VĂ€lj en mall' })).toBeVisible() + await expect(page.getByText('Komplimang')).toBeVisible() + await expect(page.getByText('KöpförfrĂ„gan')).toBeVisible() + }) + + test('selecting template fills textarea and closes picker', async ({ + page, + }) => { + await page.goto('/logga-in') + await page.getByLabel('E-postadress').fill('test@bilhalsning.se') + await page.getByLabel('Lösenord').fill('test1234') + await page.getByRole('button', { name: 'Logga in' }).click() + await page.waitForURL('/') + + await page.goto('/compose?plate=ABC123') + + await page.getByRole('button', { name: 'Visa mallar' }).click() + await page.getByText('Komplimang').click() + + const textarea = page.getByLabel('Ditt meddelande') + await expect(textarea).toHaveValue(/jĂ€ttefin/) + await expect(page.getByRole('heading', { name: 'VĂ€lj en mall' })).not.toBeVisible() + }) }) diff --git a/frontend/src/__tests__/ComposePage.spec.ts b/frontend/src/__tests__/ComposePage.spec.ts index bdd50bd..3c1fc76 100644 --- a/frontend/src/__tests__/ComposePage.spec.ts +++ b/frontend/src/__tests__/ComposePage.spec.ts @@ -168,4 +168,43 @@ describe('ComposePage', () => { const { wrapper } = await mountPage() expect(wrapper.text()).toContain('Detta brev skickades via BilHej.se') }) + + it('shows Visa mallar button', async () => { + const { wrapper } = await mountPage() + const btn = wrapper.find('.compose__templates-btn') + expect(btn.exists()).toBe(true) + expect(btn.text()).toContain('Visa mallar') + }) + + it('opens template picker when Visa mallar is clicked', async () => { + const { wrapper } = await mountPage() + const btn = wrapper.find('.compose__templates-btn') + await btn.trigger('click') + + expect(wrapper.text()).toContain('VĂ€lj en mall') + expect(wrapper.text()).toContain('Komplimang') + }) + + it('fills textarea when template is selected', async () => { + const { wrapper } = await mountPage() + const btn = wrapper.find('.compose__templates-btn') + await btn.trigger('click') + + const cards = wrapper.findAll('.modal__card') + await cards[0].trigger('click') + + const textarea = wrapper.find('textarea') + expect(textarea.element.value).toContain('jĂ€ttefin') + }) + + it('closes picker after template is selected', async () => { + const { wrapper } = await mountPage() + const btn = wrapper.find('.compose__templates-btn') + await btn.trigger('click') + + const cards = wrapper.findAll('.modal__card') + await cards[0].trigger('click') + + expect(wrapper.find('.modal-overlay').exists()).toBe(false) + }) }) diff --git a/frontend/src/__tests__/TemplatePicker.spec.ts b/frontend/src/__tests__/TemplatePicker.spec.ts new file mode 100644 index 0000000..d0a66b6 --- /dev/null +++ b/frontend/src/__tests__/TemplatePicker.spec.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import TemplatePicker from '@/components/TemplatePicker.vue' + +describe('TemplatePicker', () => { + it('renders all template cards', () => { + const wrapper = mount(TemplatePicker) + const cards = wrapper.findAll('.modal__card') + expect(cards).toHaveLength(7) + }) + + it('shows template names', () => { + const wrapper = mount(TemplatePicker) + expect(wrapper.text()).toContain('Komplimang') + expect(wrapper.text()).toContain('KöpförfrĂ„gan') + expect(wrapper.text()).toContain('Fritt meddelande') + }) + + it('emits select event with template data when card is clicked', async () => { + const wrapper = mount(TemplatePicker) + const cards = wrapper.findAll('.modal__card') + await cards[0].trigger('click') + + expect(wrapper.emitted('select')).toHaveLength(1) + expect(wrapper.emitted('select')![0][0]).toMatchObject({ + name: 'Komplimang', + icon: '🌟', + }) + }) + + it('emits close event when card is clicked', async () => { + const wrapper = mount(TemplatePicker) + const cards = wrapper.findAll('.modal__card') + await cards[0].trigger('click') + + expect(wrapper.emitted('close')).toHaveLength(1) + }) + + it('emits close event when close button is clicked', async () => { + const wrapper = mount(TemplatePicker) + const closeBtn = wrapper.find('.modal__close') + await closeBtn.trigger('click') + + expect(wrapper.emitted('close')).toHaveLength(1) + }) + + it('emits close event when overlay is clicked', async () => { + const wrapper = mount(TemplatePicker) + const overlay = wrapper.find('.modal-overlay') + await overlay.trigger('click') + + expect(wrapper.emitted('close')).toHaveLength(1) + }) + + it('includes new parking damage template', () => { + const wrapper = mount(TemplatePicker) + expect(wrapper.text()).toContain('Mindre parkeringsskada') + }) +}) diff --git a/frontend/src/components/TemplatePicker.vue b/frontend/src/components/TemplatePicker.vue new file mode 100644 index 0000000..5dabaf8 --- /dev/null +++ b/frontend/src/components/TemplatePicker.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/frontend/src/data/templates.ts b/frontend/src/data/templates.ts index 7c4771d..725b330 100644 --- a/frontend/src/data/templates.ts +++ b/frontend/src/data/templates.ts @@ -1,11 +1,13 @@ export interface LetterTemplate { name: string + icon: string body: string } export const templates: LetterTemplate[] = [ { name: 'Komplimang', + icon: '🌟', body: `Hej! Jag ville bara sĂ€ga att din bil Ă€r jĂ€ttefin! Det syns att den Ă€r vĂ€l omhĂ€ndertagen och jag uppskattar verkligen att du tar hand om den sĂ„ bra. @@ -13,7 +15,8 @@ Jag ville bara sĂ€ga att din bil Ă€r jĂ€ttefin! Det syns att den Ă€r vĂ€l omhĂ€n Ha en trevlig dag!`, }, { - name: 'Jag vill köpa din bil', + name: 'KöpförfrĂ„gan', + icon: '🚗', body: `Hej! Jag Ă€r intresserad av att köpa din bil. Om du nĂ„gon gĂ„ng funderar pĂ„ att sĂ€lja den, sĂ„ fĂ„r du gĂ€rna höra av dig. @@ -25,6 +28,7 @@ VĂ€nliga hĂ€lsningar, }, { name: 'Tips / servicebehov', + icon: '🔧', body: `Hej! Jag ville tipsa dig om att jag mĂ€rkte att din bil behöver lite uppmĂ€rksamhet. Det kan vara bra att kolla upp det sĂ„ snart som möjligt. @@ -32,7 +36,8 @@ Jag ville tipsa dig om att jag mĂ€rkte att din bil behöver lite uppmĂ€rksamhet. Hoppas detta var till hjĂ€lp!`, }, { - name: 'Synpunkter pĂ„ körbeteende', + name: 'Körbeteende', + icon: 'đŸ›Łïž', body: `Hej! Jag ville uppmĂ€rksamma dig pĂ„ en situation i trafiken dĂ€r jag reagerade pĂ„ ditt körbeteende. Jag menar inget illa utan vill bara ge en vĂ€nlig pĂ„minnelse om att vara extra uppmĂ€rksam. @@ -41,14 +46,28 @@ Tack för att du lyssnar!`, }, { name: 'Tuta / frustration', + icon: '📱', body: `Hej! Jag ville nĂ€mna en situation i trafiken dĂ€r vi bĂ„da kanske blev lite frustrerade. Det Ă€r lĂ€tt att det blir sĂ„ ibland, men jag ville nĂ„ ut för att lösa det pĂ„ ett trevligt sĂ€tt. Ha det bra!`, + }, + { + name: 'Mindre parkeringsskada', + icon: 'đŸ…żïž', + body: `Hej! + +Jag rĂ„kade skada din bil lite nĂ€r jag parkerade. Det var inte meningen och jag ber om ursĂ€kt. Jag vill gĂ€rna att vi löser det hĂ€r tillsammans. + +Du kan nĂ„ mig pĂ„: [din e-postadress eller telefonnummer] + +VĂ€nliga hĂ€lsningar, +[Ditt namn]`, }, { name: 'Fritt meddelande', + icon: '✏', body: '', }, ] diff --git a/frontend/src/pages/ComposePage.vue b/frontend/src/pages/ComposePage.vue index 44e7b24..a133c27 100644 --- a/frontend/src/pages/ComposePage.vue +++ b/frontend/src/pages/ComposePage.vue @@ -2,6 +2,8 @@ import { ref, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { createOrder } from '@/api/orders' +import { type LetterTemplate } from '@/data/templates' +import TemplatePicker from '@/components/TemplatePicker.vue' const router = useRouter() const route = useRoute() @@ -10,6 +12,7 @@ const plate = computed(() => (route.query.plate as string) || '') const letterText = ref('') const submitting = ref(false) const errorMessage = ref('') +const showPicker = ref(false) const charCount = computed(() => letterText.value.length) const maxChars = 1000 @@ -20,6 +23,10 @@ const canSubmit = computed( const GDPR_FOOTER = 'Detta brev skickades via BilHej.se. Din adress hĂ€mtades frĂ„n Transportstyrelsens fordonsregister och har raderats efter utskick. För frĂ„gor: hej@bilhalsning.se' +function handleTemplateSelect(template: LetterTemplate) { + letterText.value = template.body +} + async function handleSubmit() { if (!canSubmit.value) return @@ -50,7 +57,16 @@ async function handleSubmit() {
- +
+ + +