refactor: redesign all pages and components with new design system
- Rewrite homepage: practical headline, use-case cards, calm trust note - Switch from purple to blue brand tokens across all pages - Replace all CTA buttons with brand-primary, reserve green for success - Remove emoji from template picker and compose page - Replace unicode chevrons with SVG expand buttons in admin - Redesign template picker modal with accessibility semantics - Add aria-invalid, aria-describedby to form validation - Add role=status/alert to loading, error, and result messages - Remove inline styles, replace with scoped utility classes - Update compose submit text, payment button, order empty state copy - Remove icon field from letter templates
This commit is contained in:
parent
00327674ed
commit
851cd8afa0
13 changed files with 1318 additions and 1046 deletions
|
|
@ -36,7 +36,7 @@ watch(isValid, (valid) => {
|
|||
|
||||
<template>
|
||||
<div class="plate-input">
|
||||
<label for="plate" class="plate-input__label">Registreringsnummer</label>
|
||||
<label for="plate" class="field__label">Registreringsnummer</label>
|
||||
<input
|
||||
id="plate"
|
||||
type="text"
|
||||
|
|
@ -46,11 +46,13 @@ watch(isValid, (valid) => {
|
|||
:value="plate"
|
||||
class="plate-input__field"
|
||||
:class="{ 'plate-input__field--error': showError }"
|
||||
:aria-invalid="showError"
|
||||
aria-describedby="plate-error"
|
||||
placeholder="ABC 123"
|
||||
maxlength="7"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<p v-if="showError" class="plate-input__error">
|
||||
<p v-if="showError" id="plate-error" class="field__error">
|
||||
Ange ett giltigt registreringsnummer
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -60,47 +62,36 @@ watch(isValid, (valid) => {
|
|||
.plate-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: var(--space-sm);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plate-input__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.plate-input__field {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 1.5rem;
|
||||
font-family: monospace;
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
border: 2px solid #cbd5e0;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-surface);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.plate-input__field:focus {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-ring);
|
||||
}
|
||||
|
||||
.plate-input__field--error {
|
||||
border-color: #e53e3e;
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.plate-input__field--error:focus {
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.25);
|
||||
}
|
||||
|
||||
.plate-input__error {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: #e53e3e;
|
||||
box-shadow: 0 0 0 3px rgba(185, 28, 28, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { templates, type LetterTemplate } from '@/data/templates'
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -6,21 +7,59 @@ const emit = defineEmits<{
|
|||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const dialogRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
function handleSelect(template: LetterTemplate) {
|
||||
emit('select', template)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
dialogRef.value?.focus()
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-overlay" @click.self="emit('close')">
|
||||
<div class="modal">
|
||||
<div
|
||||
ref="dialogRef"
|
||||
class="modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="modal__header">
|
||||
<h2 class="modal__title">Välj en mall</h2>
|
||||
<button class="modal__close" @click="emit('close')">×</button>
|
||||
<h2 id="modal-title" class="modal__title">Välj en mall</h2>
|
||||
<button class="modal__close" aria-label="Stäng" @click="emit('close')">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="modal__subtitle">
|
||||
Klicka på en mall för att fylla i meddelandetexten.
|
||||
Välj en starttext. Du kan ändra allt innan du skickar.
|
||||
</p>
|
||||
<div class="modal__grid">
|
||||
<button
|
||||
|
|
@ -29,7 +68,6 @@ function handleSelect(template: LetterTemplate) {
|
|||
class="modal__card"
|
||||
@click="handleSelect(t)"
|
||||
>
|
||||
<span class="modal__card-icon">{{ t.icon }}</span>
|
||||
<span class="modal__card-name">{{ t.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -46,84 +84,88 @@ function handleSelect(template: LetterTemplate) {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 1rem;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-xl);
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--shadow-xl);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
padding: var(--space-lg) var(--space-lg) 0;
|
||||
}
|
||||
|
||||
.modal__title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.modal__close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #a0aec0;
|
||||
color: var(--color-soft);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
color 0.15s,
|
||||
background 0.15s;
|
||||
color var(--transition-fast),
|
||||
background var(--transition-fast);
|
||||
}
|
||||
|
||||
.modal__close:hover {
|
||||
color: #4a5568;
|
||||
background: #f7fafc;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.modal__subtitle {
|
||||
margin: 0.5rem 0 0;
|
||||
padding: 0 1.5rem;
|
||||
margin: var(--space-sm) 0 0;
|
||||
padding: 0 var(--space-lg);
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.modal__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.modal__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.25rem 0.75rem;
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-lg) var(--space-sm);
|
||||
background: var(--color-primary-soft);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s,
|
||||
border-color var(--transition-fast),
|
||||
background var(--transition-fast),
|
||||
transform 0.1s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.modal__card:hover {
|
||||
border-color: #4299e1;
|
||||
background: #ebf8ff;
|
||||
border-color: var(--color-primary);
|
||||
background: #dbeafe;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
|
@ -131,14 +173,10 @@ function handleSelect(template: LetterTemplate) {
|
|||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal__card-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.modal__card-name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
color: var(--color-ink);
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,50 +16,62 @@ defineProps<{
|
|||
|
||||
<template>
|
||||
<div class="vehicle-info">
|
||||
<div v-if="vehicle" class="vehicle-info__card">
|
||||
<p class="vehicle-info__card-text">
|
||||
<div
|
||||
v-if="vehicle"
|
||||
class="vehicle-info__card vehicle-info__card--found"
|
||||
role="status"
|
||||
>
|
||||
<p class="vehicle-info__text">
|
||||
{{ vehicle.make }} {{ vehicle.model }} ({{ vehicle.year }}) —
|
||||
{{ vehicle.color }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="loading" class="vehicle-info__loading">Söker...</div>
|
||||
<div v-else-if="notFound" class="vehicle-info__not-found">
|
||||
<p>Inget fordon hittades</p>
|
||||
<div v-else-if="loading" class="vehicle-info__loading" role="status">
|
||||
Söker...
|
||||
</div>
|
||||
<div
|
||||
v-else-if="notFound"
|
||||
class="vehicle-info__card vehicle-info__card--missing"
|
||||
role="status"
|
||||
>
|
||||
<p class="vehicle-info__text">Inget fordon hittades</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vehicle-info {
|
||||
margin-top: 0.75rem;
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.vehicle-info__loading {
|
||||
color: #718096;
|
||||
color: var(--color-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.vehicle-info__card {
|
||||
padding: 1rem;
|
||||
background: #f0fff4;
|
||||
border: 1px solid #c6f6d5;
|
||||
border-radius: 0.5rem;
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.vehicle-info__card-text {
|
||||
.vehicle-info__card--found {
|
||||
background: var(--color-success-soft);
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.vehicle-info__card--missing {
|
||||
background: var(--color-warning-soft);
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.vehicle-info__text {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.vehicle-info__not-found {
|
||||
padding: 1rem;
|
||||
background: #fffaf0;
|
||||
border: 1px solid #feebc8;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.vehicle-info__not-found p {
|
||||
margin: 0;
|
||||
color: #c05621;
|
||||
.vehicle-info__card--missing .vehicle-info__text {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
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.
|
||||
|
|
@ -16,7 +14,6 @@ Ha en trevlig dag!`,
|
|||
},
|
||||
{
|
||||
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.
|
||||
|
|
@ -28,7 +25,6 @@ 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.
|
||||
|
|
@ -37,7 +33,6 @@ Hoppas detta var till hjälp!`,
|
|||
},
|
||||
{
|
||||
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.
|
||||
|
|
@ -46,7 +41,6 @@ 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.
|
||||
|
|
@ -55,7 +49,6 @@ 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.
|
||||
|
|
@ -67,7 +60,6 @@ Vänliga hälsningar,
|
|||
},
|
||||
{
|
||||
name: 'Fritt meddelande',
|
||||
icon: '✏️',
|
||||
body: '',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,19 +1,39 @@
|
|||
<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 class="page">
|
||||
<div class="page__card">
|
||||
<h1>Om Bilhej</h1>
|
||||
<p>
|
||||
Bilhej är en tjänst som låter dig skicka fysiska brev till fordonsägare
|
||||
via registreringsnummer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.about {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
.page {
|
||||
max-width: 36rem;
|
||||
margin: var(--space-3xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.page__card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import {
|
||||
fetchAllOrders,
|
||||
updateOrderStatus,
|
||||
|
|
@ -24,13 +24,13 @@ const statusLabels: Record<string, string> = {
|
|||
failed: 'Misslyckad',
|
||||
}
|
||||
|
||||
const statusClasses: Record<string, string> = {
|
||||
pending_payment: 'badge--gray',
|
||||
paid: 'badge--blue',
|
||||
lookup_started: 'badge--blue',
|
||||
sent: 'badge--green',
|
||||
delivered: 'badge--green',
|
||||
failed: 'badge--red',
|
||||
const statusBadge: Record<string, string> = {
|
||||
pending_payment: 'badge--muted',
|
||||
paid: 'badge--primary',
|
||||
lookup_started: 'badge--primary',
|
||||
sent: 'badge--success',
|
||||
delivered: 'badge--success',
|
||||
failed: 'badge--danger',
|
||||
}
|
||||
|
||||
const allStatuses = [
|
||||
|
|
@ -42,6 +42,20 @@ const allStatuses = [
|
|||
'failed',
|
||||
]
|
||||
|
||||
const stats = computed(() => {
|
||||
const total = orders.value.length
|
||||
const paid = orders.value.filter((o) =>
|
||||
['paid', 'lookup_started', 'sent', 'delivered'].includes(o.status),
|
||||
).length
|
||||
const pending = orders.value.filter(
|
||||
(o) => o.status === 'pending_payment',
|
||||
).length
|
||||
const sent = orders.value.filter(
|
||||
(o) => o.status === 'sent' || o.status === 'delivered',
|
||||
).length
|
||||
return { total, paid, pending, sent }
|
||||
})
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString('sv-SE', {
|
||||
year: 'numeric',
|
||||
|
|
@ -107,361 +121,439 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-dashboard">
|
||||
<h1 class="admin-dashboard__title">Administration</h1>
|
||||
<p class="admin-dashboard__subtitle">
|
||||
Hantera beställningar, mallar och användare.
|
||||
</p>
|
||||
<div class="admin">
|
||||
<h1 class="admin__title">Administration</h1>
|
||||
|
||||
<p v-if="loading" class="admin-dashboard__loading">
|
||||
<p
|
||||
v-if="loading"
|
||||
class="text-muted text-center admin__loading"
|
||||
role="status"
|
||||
>
|
||||
Laddar beställningar...
|
||||
</p>
|
||||
|
||||
<p v-else-if="error" class="admin-dashboard__error">{{ error }}</p>
|
||||
<div v-else-if="error" class="message message--error">{{ error }}</div>
|
||||
|
||||
<p v-else-if="orders.length === 0" class="admin-dashboard__empty">
|
||||
<div v-else-if="orders.length === 0" class="message message--info">
|
||||
Inga beställningar ännu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="admin-dashboard__table-wrapper">
|
||||
<p v-if="statusError" class="admin-dashboard__status-error">
|
||||
<template v-else>
|
||||
<div class="admin__stats">
|
||||
<div class="admin__stat">
|
||||
<span class="admin__stat-value">{{ stats.total }}</span>
|
||||
<span class="admin__stat-label">Totalt</span>
|
||||
</div>
|
||||
<div class="admin__stat">
|
||||
<span class="admin__stat-value">{{ stats.paid }}</span>
|
||||
<span class="admin__stat-label">Betalda</span>
|
||||
</div>
|
||||
<div class="admin__stat">
|
||||
<span class="admin__stat-value">{{ stats.pending }}</span>
|
||||
<span class="admin__stat-label">Väntar</span>
|
||||
</div>
|
||||
<div class="admin__stat">
|
||||
<span class="admin__stat-value">{{ stats.sent }}</span>
|
||||
<span class="admin__stat-label">Skickade</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="statusError"
|
||||
class="message message--error admin__status-error"
|
||||
role="alert"
|
||||
>
|
||||
{{ statusError }}
|
||||
</p>
|
||||
|
||||
<table class="admin-dashboard__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>E-post</th>
|
||||
<th>Regnr</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="order in orders" :key="order.id">
|
||||
<tr
|
||||
class="admin-dashboard__row"
|
||||
:class="{
|
||||
'admin-dashboard__row--expanded': expandedOrderId === order.id,
|
||||
}"
|
||||
@click="toggleExpand(order.id)"
|
||||
>
|
||||
<td>{{ formatDate(order.createdAt) }}</td>
|
||||
<td>{{ order.email }}</td>
|
||||
<td class="admin-dashboard__plate">{{ order.plate }}</td>
|
||||
<td>
|
||||
<select
|
||||
class="admin-dashboard__status-select"
|
||||
:class="statusClasses[order.status] || 'badge--gray'"
|
||||
:value="order.status"
|
||||
@change="
|
||||
handleStatusChange(
|
||||
order.id,
|
||||
($event.target as HTMLSelectElement).value,
|
||||
)
|
||||
"
|
||||
@click.stop
|
||||
>
|
||||
<option v-for="s in allStatuses" :key="s" :value="s">
|
||||
{{ statusLabels[s] }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="admin-dashboard__expand">
|
||||
<span class="admin-dashboard__chevron">
|
||||
{{ expandedOrderId === order.id ? '▼' : '▶' }}
|
||||
</span>
|
||||
</td>
|
||||
<div class="admin__table-wrap">
|
||||
<table class="admin__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>E-post</th>
|
||||
<th>Regnr</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="expandedOrderId === order.id"
|
||||
class="admin-dashboard__expanded-row"
|
||||
>
|
||||
<td :colspan="5">
|
||||
<div class="admin-dashboard__letter">
|
||||
<div class="admin-dashboard__letter-label">Brevtext</div>
|
||||
<div class="admin-dashboard__letter-text">
|
||||
{{ order.letterText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-dashboard__tracking">
|
||||
<div class="admin-dashboard__tracking-header">
|
||||
<span class="admin-dashboard__tracking-label"
|
||||
>Spårnings-ID</span
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="order in orders" :key="order.id">
|
||||
<tr
|
||||
class="admin__row"
|
||||
:class="{
|
||||
'admin__row--expanded': expandedOrderId === order.id,
|
||||
}"
|
||||
>
|
||||
<td>{{ formatDate(order.createdAt) }}</td>
|
||||
<td>{{ order.email }}</td>
|
||||
<td class="admin__plate">{{ order.plate }}</td>
|
||||
<td>
|
||||
<select
|
||||
class="admin__status-select"
|
||||
:class="statusBadge[order.status] || 'badge--muted'"
|
||||
:value="order.status"
|
||||
@change="
|
||||
handleStatusChange(
|
||||
order.id,
|
||||
($event.target as HTMLSelectElement).value,
|
||||
)
|
||||
"
|
||||
@click.stop
|
||||
>
|
||||
<option v-for="s in allStatuses" :key="s" :value="s">
|
||||
{{ statusLabels[s] }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="admin__chevron-cell">
|
||||
<button
|
||||
class="admin__expand-btn"
|
||||
:aria-expanded="expandedOrderId === order.id"
|
||||
:aria-label="
|
||||
expandedOrderId === order.id
|
||||
? 'Dölj detaljer'
|
||||
: 'Visa detaljer'
|
||||
"
|
||||
@click.stop="toggleExpand(order.id)"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<a
|
||||
v-if="order.trackingId"
|
||||
class="admin-dashboard__tracking-link"
|
||||
:href="`https://www.postnord.se/verktyg/spara/?id=${order.trackingId}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.stop
|
||||
>
|
||||
Spåra hos PostNord ↗
|
||||
</a>
|
||||
</div>
|
||||
<polyline
|
||||
:points="
|
||||
expandedOrderId === order.id
|
||||
? '6 9 12 15 18 9'
|
||||
: '9 6 15 12 9 18'
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="expandedOrderId === order.id"
|
||||
class="admin__expanded-row"
|
||||
>
|
||||
<td :colspan="5">
|
||||
<div class="admin__expanded-inner">
|
||||
<div class="admin__section">
|
||||
<div class="admin__section-label">Brevtext</div>
|
||||
<div class="admin__section-body">
|
||||
{{ order.letterText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="trackingError" class="admin-dashboard__status-error">
|
||||
{{ trackingError }}
|
||||
</p>
|
||||
<div class="admin__section">
|
||||
<div class="admin__section-header">
|
||||
<span class="admin__section-label">Spårnings-ID</span>
|
||||
<a
|
||||
v-if="order.trackingId"
|
||||
class="admin__tracking-link"
|
||||
:href="`https://www.postnord.se/verktyg/spara/?id=${order.trackingId}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.stop
|
||||
>
|
||||
Spåra hos PostNord
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-dashboard__tracking-input-row">
|
||||
<input
|
||||
class="admin-dashboard__tracking-input"
|
||||
type="text"
|
||||
:value="
|
||||
trackingInputValues[order.id] ?? order.trackingId ?? ''
|
||||
"
|
||||
placeholder="PN..."
|
||||
@input="
|
||||
trackingInputValues[order.id] = (
|
||||
$event.target as HTMLInputElement
|
||||
).value
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<button
|
||||
class="admin-dashboard__tracking-save"
|
||||
@click.stop="handleTrackingSave(order.id)"
|
||||
>
|
||||
Spara spårning
|
||||
</button>
|
||||
<p
|
||||
v-if="trackingError"
|
||||
class="message message--error admin__tracking-error"
|
||||
role="alert"
|
||||
>
|
||||
{{ trackingError }}
|
||||
</p>
|
||||
|
||||
<div class="admin__tracking-row">
|
||||
<label
|
||||
:for="`tracking-${order.id}`"
|
||||
class="visually-hidden"
|
||||
>Spårnings-ID</label
|
||||
>
|
||||
<input
|
||||
:id="`tracking-${order.id}`"
|
||||
class="admin__tracking-input"
|
||||
type="text"
|
||||
:value="
|
||||
trackingInputValues[order.id] ??
|
||||
order.trackingId ??
|
||||
''
|
||||
"
|
||||
placeholder="PN..."
|
||||
@input="
|
||||
trackingInputValues[order.id] = (
|
||||
$event.target as HTMLInputElement
|
||||
).value
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<button
|
||||
class="btn btn--primary btn--sm"
|
||||
@click.stop="handleTrackingSave(order.id)"
|
||||
>
|
||||
Spara
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-dashboard {
|
||||
max-width: 64rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
.admin {
|
||||
max-width: 72rem;
|
||||
margin: var(--space-2xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.admin-dashboard__title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
.admin__title {
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.admin-dashboard__subtitle {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #718096;
|
||||
font-size: 0.875rem;
|
||||
.admin__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.admin-dashboard__loading,
|
||||
.admin-dashboard__error,
|
||||
.admin-dashboard__empty {
|
||||
margin: 2rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
.admin__stat {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.admin-dashboard__loading {
|
||||
color: #718096;
|
||||
.admin__stat-value {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.admin-dashboard__error {
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
color: #c53030;
|
||||
.admin__stat-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-muted);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.admin-dashboard__empty {
|
||||
background: #f7fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.admin-dashboard__status-error {
|
||||
margin: 0 0 0.75rem 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 0.375rem;
|
||||
color: #c53030;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.admin-dashboard__table-wrapper {
|
||||
.admin__table-wrap {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-dashboard__table {
|
||||
.admin__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.admin-dashboard__table thead {
|
||||
background: #f7fafc;
|
||||
.admin__table thead {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-dashboard__table th {
|
||||
padding: 0.75rem 1rem;
|
||||
.admin__table th {
|
||||
padding: 0.75rem var(--space-md);
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #718096;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.admin-dashboard__row {
|
||||
.admin__row {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
transition: background 0.1s;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.admin-dashboard__row:hover {
|
||||
background: #f7fafc;
|
||||
.admin__row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-dashboard__row--expanded {
|
||||
background: #ebf8ff;
|
||||
.admin__row:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-dashboard__row td {
|
||||
padding: 0.75rem 1rem;
|
||||
color: #4a5568;
|
||||
.admin__row--expanded {
|
||||
background: var(--color-primary-soft) !important;
|
||||
}
|
||||
|
||||
.admin__row td {
|
||||
padding: 0.75rem var(--space-md);
|
||||
color: var(--color-ink);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-dashboard__plate {
|
||||
.admin__plate {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
color: #1a202c !important;
|
||||
}
|
||||
|
||||
.admin-dashboard__status-select {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
.admin__status-select {
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
color: var(--color-ink);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.admin-dashboard__status-select:focus {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
|
||||
.admin__status-select:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-ring);
|
||||
}
|
||||
|
||||
.admin-dashboard__expand {
|
||||
.admin__chevron-cell {
|
||||
text-align: center;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.admin-dashboard__chevron {
|
||||
font-size: 0.625rem;
|
||||
color: #a0aec0;
|
||||
.admin__expand-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-soft);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
background var(--transition-fast);
|
||||
}
|
||||
|
||||
.admin-dashboard__expanded-row td {
|
||||
.admin__expand-btn:hover {
|
||||
color: var(--color-ink);
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin__expanded-row td {
|
||||
padding: 0;
|
||||
background: #f7fafc;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.admin-dashboard__letter {
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
.admin__expanded-inner {
|
||||
padding: var(--space-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-dashboard__letter-label {
|
||||
.admin__section {
|
||||
padding: var(--space-md);
|
||||
background: var(--color-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.admin__section-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #a0aec0;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.admin-dashboard__letter-text {
|
||||
.admin__section-body {
|
||||
font-size: 0.875rem;
|
||||
color: #4a5568;
|
||||
color: var(--color-ink);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking {
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-header {
|
||||
.admin__section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #a0aec0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-link {
|
||||
.admin__tracking-link {
|
||||
font-size: 0.8125rem;
|
||||
color: #4299e1;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-link:hover {
|
||||
.admin__tracking-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-input-row {
|
||||
.admin__tracking-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: var(--space-sm);
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-input {
|
||||
.admin__tracking-input {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8125rem;
|
||||
color: #4a5568;
|
||||
color: var(--color-ink);
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-input:focus {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.2);
|
||||
.admin__tracking-input:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-ring);
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-save {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background: #4299e1;
|
||||
color: #fff;
|
||||
.admin__loading {
|
||||
padding: var(--space-2xl) 0;
|
||||
}
|
||||
|
||||
.admin__status-error {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.admin__tracking-error {
|
||||
margin-bottom: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-dashboard__tracking-save:hover {
|
||||
background: #3182ce;
|
||||
@media (max-width: 768px) {
|
||||
.admin__stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||
import { createOrder } from '@/api/orders'
|
||||
import { type LetterTemplate } from '@/data/templates'
|
||||
import TemplatePicker from '@/components/TemplatePicker.vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
|
@ -21,7 +22,7 @@ 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'
|
||||
'Detta brev skickades via Bilhej. 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
|
||||
|
|
@ -50,61 +51,75 @@ async function handleSubmit() {
|
|||
|
||||
<template>
|
||||
<div class="compose">
|
||||
<h1 class="compose__title">Skriv ditt brev</h1>
|
||||
<p v-if="plate" class="compose__plate">
|
||||
Registreringsnummer: <strong>{{ plate }}</strong>
|
||||
</p>
|
||||
<p v-if="!plate" class="compose__error">
|
||||
Inget registreringsnummer valt.
|
||||
<RouterLink to="/">Gå tillbaka</RouterLink>
|
||||
</p>
|
||||
|
||||
<form v-if="plate" class="compose__form" @submit.prevent="handleSubmit">
|
||||
<div class="compose__field">
|
||||
<div class="compose__label-row">
|
||||
<label for="letter" class="compose__label">Ditt meddelande</label>
|
||||
<button
|
||||
type="button"
|
||||
class="compose__templates-btn"
|
||||
@click="showPicker = true"
|
||||
>
|
||||
✨ Visa mallar
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="letter"
|
||||
v-model="letterText"
|
||||
class="compose__textarea"
|
||||
:maxlength="maxChars"
|
||||
rows="10"
|
||||
placeholder="Skriv ditt meddelande här..."
|
||||
></textarea>
|
||||
<p
|
||||
class="compose__counter"
|
||||
:class="{ 'compose__counter--warn': charCount > 900 }"
|
||||
>
|
||||
{{ charCount }} / {{ maxChars }} tecken
|
||||
<div v-if="plate" class="compose__layout">
|
||||
<div class="compose__editor">
|
||||
<h1 class="compose__title">Skriv ditt brev</h1>
|
||||
<p class="compose__plate-badge">
|
||||
<span class="compose__plate-label">Regnr</span>
|
||||
<span class="compose__plate-value">{{ plate }}</span>
|
||||
</p>
|
||||
|
||||
<form class="compose__form" @submit.prevent="handleSubmit">
|
||||
<div class="field">
|
||||
<div class="compose__label-row">
|
||||
<label for="letter" class="field__label">Ditt meddelande</label>
|
||||
<button
|
||||
type="button"
|
||||
class="compose__templates-btn"
|
||||
@click="showPicker = true"
|
||||
>
|
||||
Visa mallar
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="letter"
|
||||
v-model="letterText"
|
||||
class="field__input compose__textarea"
|
||||
:maxlength="maxChars"
|
||||
rows="12"
|
||||
placeholder="Skriv ditt meddelande här..."
|
||||
></textarea>
|
||||
<p
|
||||
class="field__hint compose__counter"
|
||||
:class="{ 'compose__counter--warn': charCount > 900 }"
|
||||
>
|
||||
{{ charCount }} / {{ maxChars }} tecken
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="message message--error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn--primary btn--lg compose__submit"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
{{ submitting ? 'Skickar...' : 'Fortsätt till betalning' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="compose__preview">
|
||||
<h2 class="compose__preview-title">Förhandsvisning</h2>
|
||||
<div class="compose__preview-page">
|
||||
<p class="compose__preview-plate">Registreringsnummer: {{ plate }}</p>
|
||||
<p class="compose__preview-body" style="white-space: pre-wrap">
|
||||
<p class="compose__preview-plate-label">
|
||||
Registreringsnummer: {{ plate }}
|
||||
</p>
|
||||
<p class="compose__preview-body">
|
||||
{{ letterText }}
|
||||
</p>
|
||||
<hr class="compose__preview-divider" />
|
||||
<p class="compose__preview-footer">{{ GDPR_FOOTER }}</p>
|
||||
<p class="compose__preview-footer-text">{{ GDPR_FOOTER }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="compose__api-error">{{ errorMessage }}</p>
|
||||
|
||||
<button type="submit" class="compose__submit" :disabled="!canSubmit">
|
||||
{{ submitting ? 'Skickar...' : 'Skicka brev (49 kr)' }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-else class="message message--error compose__error">
|
||||
Inget registreringsnummer valt.
|
||||
<RouterLink to="/">Gå tillbaka</RouterLink>
|
||||
</div>
|
||||
|
||||
<TemplatePicker
|
||||
v-if="showPicker"
|
||||
|
|
@ -115,59 +130,50 @@ async function handleSubmit() {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.compose {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
.compose__layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-xl);
|
||||
align-items: start;
|
||||
max-width: 56rem;
|
||||
margin: var(--space-2xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.compose__title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.compose__plate {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #4a5568;
|
||||
font-size: 0.875rem;
|
||||
.compose__plate-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
background: var(--color-primary-soft);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-full);
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
}
|
||||
|
||||
.compose__error {
|
||||
margin: 2rem 0;
|
||||
padding: 1rem;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 0.5rem;
|
||||
color: #c53030;
|
||||
font-size: 0.875rem;
|
||||
.compose__plate-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.compose__error a {
|
||||
color: #4299e1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.compose__error a:hover {
|
||||
text-decoration: underline;
|
||||
.compose__plate-value {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.compose__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.compose__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.compose__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.compose__label-row {
|
||||
|
|
@ -177,129 +183,112 @@ async function handleSubmit() {
|
|||
}
|
||||
|
||||
.compose__templates-btn {
|
||||
background: #ebf8ff;
|
||||
border: 1px solid #bee3f8;
|
||||
color: #2b6cb0;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--color-primary-soft);
|
||||
border: 1px solid #ddd6fe;
|
||||
color: var(--color-primary-dark);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0.875rem;
|
||||
border-radius: 9999px;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: var(--radius-full);
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
background var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.compose__templates-btn:hover {
|
||||
background: #bee3f8;
|
||||
border-color: #90cdf4;
|
||||
background: #e9d5ff;
|
||||
border-color: #c4b5fd;
|
||||
}
|
||||
|
||||
.compose__textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
border: 2px solid #cbd5e0;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.compose__textarea:focus {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25);
|
||||
min-height: 10rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.compose__counter {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: #a0aec0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.compose__counter--warn {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.compose__preview {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.compose__preview-title {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.compose__preview-page {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.compose__preview-plate {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.compose__preview-body {
|
||||
margin: 0 0 1.5rem 0;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
.compose__preview-divider {
|
||||
margin: 1.5rem 0;
|
||||
border: none;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.compose__preview-footer {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: #a0aec0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.compose__api-error {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 0.5rem;
|
||||
color: #c53030;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-danger) !important;
|
||||
}
|
||||
|
||||
.compose__submit {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: #38a169;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.compose__error {
|
||||
max-width: 28rem;
|
||||
margin: var(--space-2xl) auto;
|
||||
}
|
||||
|
||||
.compose__preview-body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.compose__preview {
|
||||
position: sticky;
|
||||
top: 5rem;
|
||||
}
|
||||
|
||||
.compose__preview-title {
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.compose__submit:hover:not(:disabled) {
|
||||
background: #2f855a;
|
||||
.compose__preview-page {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
font-family: var(--font-serif);
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-ink);
|
||||
min-height: 20rem;
|
||||
}
|
||||
|
||||
.compose__submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
.compose__preview-plate-label {
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.compose__preview-body {
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
}
|
||||
|
||||
.compose__preview-divider {
|
||||
margin: var(--space-lg) 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.compose__preview-footer-text {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-soft);
|
||||
font-family: var(--font-sans);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.compose__layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.compose__preview {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,39 @@
|
|||
<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 på
|
||||
<a href="mailto:hej@bilhalsning.se">hej@bilhalsning.se</a>.
|
||||
</p>
|
||||
<div class="page">
|
||||
<div class="page__card">
|
||||
<h1>Kontakta oss</h1>
|
||||
<p>
|
||||
Har du frågor eller feedback? Hör av dig till oss på
|
||||
<a href="mailto:hej@bilhalsning.se">hej@bilhalsning.se</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contact {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
.page {
|
||||
max-width: 36rem;
|
||||
margin: var(--space-3xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.page__card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -37,53 +37,289 @@ function handleLookup(lookedUpPlate: string) {
|
|||
|
||||
<template>
|
||||
<div class="home">
|
||||
<p class="home__subtitle">Skicka ett brev till en fordonsägare</p>
|
||||
<section class="home__hero">
|
||||
<div class="home__hero-content">
|
||||
<p class="home__eyebrow">Brev via registreringsnummer</p>
|
||||
<h1 class="home__headline">Skicka ett brev<br />till en bilägare</h1>
|
||||
<p class="home__lead">
|
||||
Skriv ett respektfullt meddelande, ange registreringsnumret och låt
|
||||
Bilhej posta brevet åt dig.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PlateInput v-model="plate" @lookup="handleLookup" />
|
||||
<div class="home__card">
|
||||
<PlateInput v-model="plate" @lookup="handleLookup" />
|
||||
|
||||
<VehicleInfo
|
||||
:vehicle="vehicle"
|
||||
:loading="lookingUp"
|
||||
:not-found="notFound"
|
||||
:plate="plate"
|
||||
/>
|
||||
<VehicleInfo
|
||||
:vehicle="vehicle"
|
||||
:loading="lookingUp"
|
||||
:not-found="notFound"
|
||||
:plate="plate"
|
||||
/>
|
||||
|
||||
<RouterLink
|
||||
v-if="vehicle"
|
||||
:to="{ name: 'compose', query: { plate } }"
|
||||
class="home__cta"
|
||||
>
|
||||
Skicka ett brev till ägaren
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="vehicle"
|
||||
:to="{ name: 'compose', query: { plate } }"
|
||||
class="btn btn--primary btn--lg home__cta"
|
||||
>
|
||||
Fortsätt till brevet
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home__uses">
|
||||
<div class="home__uses-inner">
|
||||
<h2 class="home__uses-title">Vanliga anledningar</h2>
|
||||
<div class="home__uses-grid">
|
||||
<div class="home__use">
|
||||
<h3>Vill köpa bilen</h3>
|
||||
<p>Skicka en förfrågan utan att leta efter ägaren själv.</p>
|
||||
</div>
|
||||
<div class="home__use">
|
||||
<h3>Tipsa om något</h3>
|
||||
<p>
|
||||
Berätta om lampor, däck eller annat som ägaren kan vilja veta.
|
||||
</p>
|
||||
</div>
|
||||
<div class="home__use">
|
||||
<h3>Skicka en komplimang</h3>
|
||||
<p>En enkel hälsning till någon som tar hand om sin bil.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home__steps">
|
||||
<div class="home__steps-inner">
|
||||
<h2 class="home__steps-title">Så fungerar det</h2>
|
||||
<div class="home__steps-grid">
|
||||
<div class="home__step">
|
||||
<span class="home__step-number">1</span>
|
||||
<h3>Ange registreringsnummer</h3>
|
||||
<p>
|
||||
Vi visar grundläggande fordonsinformation så att du vet att det är
|
||||
rätt bil.
|
||||
</p>
|
||||
</div>
|
||||
<div class="home__step">
|
||||
<span class="home__step-number">2</span>
|
||||
<h3>Skriv meddelandet</h3>
|
||||
<p>
|
||||
Utgå från en mall eller skriv själv. Du ser brevet innan du
|
||||
skickar.
|
||||
</p>
|
||||
</div>
|
||||
<div class="home__step">
|
||||
<span class="home__step-number">3</span>
|
||||
<h3>Vi postar brevet</h3>
|
||||
<p>
|
||||
Efter betalning hanteras brevet manuellt och skickas med post.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home__trust">
|
||||
<div class="home__trust-inner">
|
||||
<p class="home__trust-text">
|
||||
<strong>Trygg hantering.</strong> Bilhej hanterar adressuppgifter
|
||||
endast för att kunna posta brevet. Mottagarens adress visas inte i
|
||||
tjänsten.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
.home__hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-3xl);
|
||||
align-items: center;
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-3xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.home__subtitle {
|
||||
color: #718096;
|
||||
margin: 0 0 1.5rem 0;
|
||||
.home__eyebrow {
|
||||
display: inline-block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary);
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
}
|
||||
|
||||
.home__headline {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
}
|
||||
|
||||
.home__lead {
|
||||
font-size: 1.0625rem;
|
||||
color: var(--color-muted);
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
.home__card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
margin-top: var(--space-lg);
|
||||
}
|
||||
|
||||
.home__cta:hover {
|
||||
background: #2f855a;
|
||||
.home__uses {
|
||||
padding: var(--space-3xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.home__uses-inner {
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.home__uses-title {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.home__uses-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.home__use {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.home__use h3 {
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
font-size: 1rem;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.home__use p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.home__steps {
|
||||
padding: var(--space-3xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.home__steps-inner {
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.home__steps-title {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.home__steps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.home__step {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-xl);
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.home__step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--radius-full);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.home__step h3 {
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
font-size: 1rem;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.home__step p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.home__trust {
|
||||
padding: 0 var(--space-lg) var(--space-3xl);
|
||||
}
|
||||
|
||||
.home__trust-inner {
|
||||
max-width: 48rem;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-lg) var(--space-xl);
|
||||
background: var(--color-primary-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home__trust-text {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-primary-dark);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home__hero {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-xl);
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.home__headline {
|
||||
font-size: clamp(1.75rem, 6vw, 2.5rem);
|
||||
}
|
||||
|
||||
.home__uses-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.home__steps-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -35,153 +35,107 @@ async function handleSubmit() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login">
|
||||
<h1 class="login__title">Logga in</h1>
|
||||
<p class="login__subtitle">
|
||||
Ange din e-postadress och ditt lösenord för att logga in.
|
||||
</p>
|
||||
<div class="page">
|
||||
<div class="page__card">
|
||||
<h1 class="page__title">Logga in</h1>
|
||||
<p class="page__subtitle">
|
||||
Ange din e-postadress och ditt lösenord för att logga in.
|
||||
</p>
|
||||
|
||||
<form class="login__form" @submit.prevent="handleSubmit">
|
||||
<div class="login__field">
|
||||
<label for="email" class="login__label">E-postadress</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="login__input"
|
||||
placeholder="namn@exempel.se"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="login__field">
|
||||
<label for="password" class="login__label">Lösenord</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
class="login__input"
|
||||
placeholder="Ditt lösenord"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="login__api-error">{{ errorMessage }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="login__submit"
|
||||
:disabled="!isValid || submitting"
|
||||
<form
|
||||
class="page__form"
|
||||
method="post"
|
||||
action="/api/auth/login"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
{{ submitting ? 'Loggar in...' : 'Logga in' }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="field">
|
||||
<label for="email" class="field__label">E-postadress</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
class="field__input"
|
||||
placeholder="namn@exempel.se"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="login__register-link">
|
||||
Har du inget konto?
|
||||
<RouterLink to="/registrera">Skapa konto</RouterLink>
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="password" class="field__label">Lösenord</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
class="field__input"
|
||||
placeholder="Ditt lösenord"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="message message--error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn--primary btn--lg login__submit"
|
||||
:disabled="!isValid || submitting"
|
||||
>
|
||||
{{ submitting ? 'Loggar in...' : 'Logga in' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="page__footer-link">
|
||||
Har du inget konto?
|
||||
<RouterLink to="/registrera">Skapa konto</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
.page {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
margin: var(--space-3xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.login__title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
.page__card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.page__title {
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.login__subtitle {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #718096;
|
||||
.page__subtitle {
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.login__form {
|
||||
.page__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.login__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.login__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.login__input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: 2px solid #cbd5e0;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login__input:focus {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25);
|
||||
}
|
||||
|
||||
.login__api-error {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 0.5rem;
|
||||
color: #c53030;
|
||||
.page__footer-link {
|
||||
margin-top: var(--space-lg);
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.login__submit {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: #4299e1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.login__submit:hover:not(:disabled) {
|
||||
background: #3182ce;
|
||||
}
|
||||
|
||||
.login__submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login__register-link {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.login__register-link a {
|
||||
color: #4299e1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login__register-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchOrders, type Order } from '@/api/orders'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const orders = ref<Order[]>([])
|
||||
const loading = ref(true)
|
||||
|
|
@ -15,13 +16,13 @@ const statusLabels: Record<string, string> = {
|
|||
failed: 'Misslyckad',
|
||||
}
|
||||
|
||||
const statusClasses: Record<string, string> = {
|
||||
pending_payment: 'badge--gray',
|
||||
paid: 'badge--blue',
|
||||
lookup_started: 'badge--blue',
|
||||
sent: 'badge--green',
|
||||
delivered: 'badge--green',
|
||||
failed: 'badge--red',
|
||||
const statusBadge: Record<string, string> = {
|
||||
pending_payment: 'badge--muted',
|
||||
paid: 'badge--primary',
|
||||
lookup_started: 'badge--primary',
|
||||
sent: 'badge--success',
|
||||
delivered: 'badge--success',
|
||||
failed: 'badge--danger',
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
|
|
@ -44,47 +45,63 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="orders">
|
||||
<h1 class="orders__title">Mina beställningar</h1>
|
||||
<p class="orders__subtitle">Här kan du se dina tidigare beställningar.</p>
|
||||
<div class="page">
|
||||
<h1 class="page__title">Mina beställningar</h1>
|
||||
<p class="page__subtitle">Här kan du se dina tidigare beställningar.</p>
|
||||
|
||||
<p v-if="loading" class="orders__loading">Laddar beställningar...</p>
|
||||
|
||||
<p v-else-if="error" class="orders__error">{{ error }}</p>
|
||||
|
||||
<p v-else-if="orders.length === 0" class="orders__empty">
|
||||
Du har inga beställningar ännu.
|
||||
<p
|
||||
v-if="loading"
|
||||
class="text-muted text-center orders__loading"
|
||||
role="status"
|
||||
>
|
||||
Laddar beställningar...
|
||||
</p>
|
||||
|
||||
<div v-else-if="error" class="message message--error" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="orders.length === 0" class="orders__empty">
|
||||
<div class="orders__empty-card">
|
||||
<p class="orders__empty-title">Inga beställningar ännu</p>
|
||||
<p class="orders__empty-text">
|
||||
Följ dina brev och se tidigare skickade hälsningar.
|
||||
</p>
|
||||
<RouterLink to="/" class="btn btn--primary orders__empty-cta">
|
||||
Skicka första brevet
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="orders__list">
|
||||
<div v-for="order in orders" :key="order.id" class="orders__card">
|
||||
<div class="orders__card-header">
|
||||
<div class="orders__card-top">
|
||||
<span class="orders__plate">{{ order.plate }}</span>
|
||||
<span
|
||||
class="orders__badge"
|
||||
:class="statusClasses[order.status] || 'badge--gray'"
|
||||
class="badge"
|
||||
:class="statusBadge[order.status] || 'badge--muted'"
|
||||
>
|
||||
{{ statusLabels[order.status] || order.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="orders__card-body">
|
||||
<div class="orders__detail">
|
||||
<span class="orders__label">Datum</span>
|
||||
<span class="orders__value">{{ formatDate(order.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="orders__card-meta">
|
||||
<span class="orders__meta-label">Datum</span>
|
||||
<span class="orders__meta-value">{{
|
||||
formatDate(order.createdAt)
|
||||
}}</span>
|
||||
|
||||
<div v-if="order.trackingId" class="orders__detail">
|
||||
<span class="orders__label">Spårning</span>
|
||||
<template v-if="order.trackingId">
|
||||
<span class="orders__meta-label">Spårning</span>
|
||||
<a
|
||||
class="orders__tracking-link"
|
||||
class="orders__tracking"
|
||||
:href="`https://www.postnord.se/verktyg/spara/?id=${order.trackingId}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ order.trackingId }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -92,140 +109,117 @@ onMounted(async () => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.orders {
|
||||
.page {
|
||||
max-width: 48rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
margin: var(--space-3xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.orders__title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
.page__title {
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.orders__subtitle {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #718096;
|
||||
.page__subtitle {
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.orders__loading,
|
||||
.orders__error,
|
||||
.orders__empty {
|
||||
margin: 2rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.orders__loading {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.orders__error {
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.orders__empty {
|
||||
background: #f7fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #718096;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.orders__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.orders__card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.orders__card-header {
|
||||
.orders__card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #f7fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
background: var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.orders__plate {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.orders__badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
.orders__card-meta {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-sm) var(--space-lg);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge--gray {
|
||||
background: #edf2f7;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.badge--blue {
|
||||
background: #ebf8ff;
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.badge--green {
|
||||
background: #f0fff4;
|
||||
color: #276749;
|
||||
}
|
||||
|
||||
.badge--red {
|
||||
background: #fff5f5;
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.orders__card-body {
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.orders__detail {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.orders__label {
|
||||
min-width: 5rem;
|
||||
.orders__meta-label {
|
||||
font-size: 0.8125rem;
|
||||
color: #a0aec0;
|
||||
color: var(--color-soft);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orders__meta-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.orders__tracking {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.orders__value {
|
||||
font-size: 0.875rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.orders__tracking-link {
|
||||
font-size: 0.875rem;
|
||||
color: #4299e1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.orders__tracking-link:hover {
|
||||
.orders__tracking:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.orders__empty {
|
||||
padding: var(--space-2xl) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.orders__empty-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-2xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.orders__empty-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
}
|
||||
|
||||
.orders__empty-text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.orders__empty-cta {
|
||||
margin-top: var(--space-md);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.orders__loading {
|
||||
padding: var(--space-2xl) 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -26,113 +26,101 @@ async function handlePay() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="payment">
|
||||
<h1 class="payment__title">Betalning</h1>
|
||||
<p class="payment__subtitle">
|
||||
Registreringsnummer: <strong>{{ route.query.plate || '—' }}</strong>
|
||||
</p>
|
||||
<div class="page">
|
||||
<div class="page__card">
|
||||
<h1 class="page__title">Betalning</h1>
|
||||
<p class="page__plate">
|
||||
Registreringsnummer: <strong>{{ route.query.plate || '—' }}</strong>
|
||||
</p>
|
||||
|
||||
<div class="payment__card">
|
||||
<div class="payment__amount-row">
|
||||
<span class="payment__label">Att betala</span>
|
||||
<span class="payment__amount">49 kr</span>
|
||||
<div class="payment__summary">
|
||||
<div class="payment__row">
|
||||
<span class="payment__label">Att betala</span>
|
||||
<span class="payment__amount">49 kr</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="payment__error">{{ error }}</p>
|
||||
<div
|
||||
v-if="error"
|
||||
class="message message--error"
|
||||
style="margin-bottom: var(--space-md)"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<button class="payment__button" :disabled="paying" @click="handlePay">
|
||||
{{ paying ? 'Bearbetar...' : 'Betalt' }}
|
||||
<button
|
||||
class="btn btn--primary btn--lg payment__submit"
|
||||
:disabled="paying"
|
||||
@click="handlePay"
|
||||
>
|
||||
{{ paying ? 'Bearbetar...' : 'Genomför testbetalning' }}
|
||||
</button>
|
||||
|
||||
<p class="payment__note">
|
||||
Detta är en mock-betalning. I framtiden skickas du till Stripe.
|
||||
Detta är en testbetalning i utvecklingsmiljön.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.payment {
|
||||
.page {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
margin: var(--space-3xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.payment__title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
.page__card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.page__title {
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.payment__subtitle {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #718096;
|
||||
.page__plate {
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.payment__card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
.payment__summary {
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-bottom: var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.payment__amount-row {
|
||||
.payment__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.payment__label {
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.payment__amount {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.payment__error {
|
||||
margin: 0 0 0.75rem 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 0.375rem;
|
||||
color: #c53030;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.payment__button {
|
||||
.payment__submit {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background: #48bb78;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.payment__button:hover:not(:disabled) {
|
||||
background: #38a169;
|
||||
}
|
||||
|
||||
.payment__button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.payment__note {
|
||||
margin: 0.75rem 0 0 0;
|
||||
color: #a0aec0;
|
||||
margin: var(--space-md) 0 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-soft);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -70,193 +70,139 @@ async function handleSubmit() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="register">
|
||||
<h1 class="register__title">Skapa konto</h1>
|
||||
<p class="register__subtitle">
|
||||
Ange din e-postadress och ett lösenord för att skapa ett konto.
|
||||
</p>
|
||||
<div class="page">
|
||||
<div class="page__card">
|
||||
<h1 class="page__title">Skapa konto</h1>
|
||||
<p class="page__subtitle">
|
||||
Ange din e-postadress och ett lösenord för att skapa ett konto.
|
||||
</p>
|
||||
|
||||
<form class="register__form" @submit.prevent="handleSubmit">
|
||||
<div class="register__field">
|
||||
<label for="email" class="register__label">E-postadress</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="register__input"
|
||||
:class="{ 'register__input--error': emailError }"
|
||||
placeholder="namn@exempel.se"
|
||||
@input="touched = true"
|
||||
/>
|
||||
<p v-if="emailError" class="register__error">{{ emailError }}</p>
|
||||
</div>
|
||||
<form class="page__form" @submit.prevent="handleSubmit">
|
||||
<div class="field">
|
||||
<label for="email" class="field__label">E-postadress</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
class="field__input"
|
||||
:class="{ 'field__input--error': emailError }"
|
||||
:aria-invalid="!!emailError"
|
||||
aria-describedby="email-error"
|
||||
placeholder="namn@exempel.se"
|
||||
@input="touched = true"
|
||||
/>
|
||||
<p v-if="emailError" id="email-error" class="field__error">
|
||||
{{ emailError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="register__field">
|
||||
<label for="password" class="register__label">Lösenord</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="register__input"
|
||||
:class="{ 'register__input--error': passwordError }"
|
||||
placeholder="Minst 8 tecken"
|
||||
@input="touched = true"
|
||||
/>
|
||||
<p v-if="passwordError" class="register__error">{{ passwordError }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password" class="field__label">Lösenord</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="field__input"
|
||||
:class="{ 'field__input--error': passwordError }"
|
||||
:aria-invalid="!!passwordError"
|
||||
aria-describedby="password-error"
|
||||
placeholder="Minst 8 tecken"
|
||||
@input="touched = true"
|
||||
/>
|
||||
<p v-if="passwordError" id="password-error" class="field__error">
|
||||
{{ passwordError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="register__field">
|
||||
<label for="confirm-password" class="register__label"
|
||||
>Bekräfta lösenord</label
|
||||
<div class="field">
|
||||
<label for="confirm-password" class="field__label"
|
||||
>Bekräfta lösenord</label
|
||||
>
|
||||
<input
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="field__input"
|
||||
:class="{ 'field__input--error': confirmPasswordError }"
|
||||
:aria-invalid="!!confirmPasswordError"
|
||||
aria-describedby="confirm-password-error"
|
||||
placeholder="Upprepa lösenord"
|
||||
@input="touched = true"
|
||||
/>
|
||||
<p
|
||||
v-if="confirmPasswordError"
|
||||
id="confirm-password-error"
|
||||
class="field__error"
|
||||
>
|
||||
{{ confirmPasswordError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="message message--error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn--primary btn--lg register__submit"
|
||||
:disabled="!isValid || submitting"
|
||||
>
|
||||
<input
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="register__input"
|
||||
:class="{ 'register__input--error': confirmPasswordError }"
|
||||
placeholder="Upprepa lösenord"
|
||||
@input="touched = true"
|
||||
/>
|
||||
<p v-if="confirmPasswordError" class="register__error">
|
||||
{{ confirmPasswordError }}
|
||||
</p>
|
||||
</div>
|
||||
{{ submitting ? 'Skapar konto...' : 'Skapa konto' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p v-if="errorMessage" class="register__api-error">{{ errorMessage }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="register__submit"
|
||||
:disabled="!isValid || submitting"
|
||||
>
|
||||
{{ submitting ? 'Skapar konto...' : 'Skapa konto' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="register__login-link">
|
||||
Har du redan ett konto?
|
||||
<RouterLink to="/logga-in">Logga in</RouterLink>
|
||||
</p>
|
||||
<p class="page__footer-link">
|
||||
Har du redan ett konto?
|
||||
<RouterLink to="/logga-in">Logga in</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.register {
|
||||
.page {
|
||||
max-width: 28rem;
|
||||
margin: 3rem auto 0;
|
||||
padding: 0 1rem;
|
||||
margin: var(--space-3xl) auto 0;
|
||||
padding: 0 var(--space-lg);
|
||||
}
|
||||
|
||||
.register__title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
.page__card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-xl);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.page__title {
|
||||
margin: 0 0 var(--space-sm) 0;
|
||||
font-size: 1.5rem;
|
||||
color: #1a202c;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.register__subtitle {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #718096;
|
||||
.page__subtitle {
|
||||
margin: 0 0 var(--space-xl) 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.register__form {
|
||||
.page__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.register__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.register__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.register__input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: 2px solid #cbd5e0;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.register__input:focus {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25);
|
||||
}
|
||||
|
||||
.register__input--error {
|
||||
border-color: #e53e3e;
|
||||
}
|
||||
|
||||
.register__input--error:focus {
|
||||
border-color: #e53e3e;
|
||||
box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.25);
|
||||
}
|
||||
|
||||
.register__error {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.register__api-error {
|
||||
margin: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #fff5f5;
|
||||
border: 1px solid #fed7d7;
|
||||
border-radius: 0.5rem;
|
||||
color: #c53030;
|
||||
.page__footer-link {
|
||||
margin-top: var(--space-lg);
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.register__submit {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: #38a169;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.register__submit:hover:not(:disabled) {
|
||||
background: #2f855a;
|
||||
}
|
||||
|
||||
.register__submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.register__login-link {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.register__login-link a {
|
||||
color: #4299e1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.register__login-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue