profileShare

rasmusjy / splitapp-frontend-vue

Read-only snapshot

No repository description.

main default branch 96 files Expires Sep 13, 2026, 9:06 AM
AcceptView.vue 5,584 bytes
1 <script setup lang="ts">
2 import { ref, onMounted } from 'vue'
3 import { useRouter, useRoute } from 'vue-router'
4 import { useI18n } from 'vue-i18n'
5 import InvitationService from '@/services/InvitationService'
6 import type { IInvitation } from '@/types/IInvitation'
7
8 const router = useRouter()
9 const route = useRoute()
10 const { t, d } = useI18n()
11
12 const token = route.params.token as string
13
14 const invitation = ref<IInvitation | null>(null)
15 const isLoading = ref(true)
16 const error = ref<string | null>(null)
17 const actionDone = ref(false)
18 const actionMessage = ref('')
19 const isProcessing = ref(false)
20
21 onMounted(async () => {
22 const result = await InvitationService.getByToken(token)
23 if (result.data) {
24 invitation.value = result.data
25 } else if (result.errors) {
26 error.value = result.errors.join(', ')
27 }
28 isLoading.value = false
29 })
30
31 async function acceptInvitation() {
32 isProcessing.value = true
33 error.value = null
34
35 const result = await InvitationService.accept(token)
36 if (result.errors) {
37 error.value = result.errors.join(', ')
38 } else {
39 actionDone.value = true
40 actionMessage.value = t('invitations.accept.accepted')
41 }
42
43 isProcessing.value = false
44 }
45
46 async function declineInvitation() {
47 isProcessing.value = true
48 error.value = null
49
50 const result = await InvitationService.decline(token)
51 if (result.errors) {
52 error.value = result.errors.join(', ')
53 } else {
54 actionDone.value = true
55 actionMessage.value = t('invitations.accept.declined')
56 }
57
58 isProcessing.value = false
59 }
60
61 function formatDate(dateStr: string) {
62 return d(new Date(dateStr), 'long')
63 }
64 </script>
65
66 <template>
67 <div class="sa-invite-page">
68 <!-- Loading -->
69 <div v-if="isLoading" class="text-center py-5">
70 <div class="spinner-border text-secondary" role="status"></div>
71 </div>
72
73 <!-- Error / Expired (no invitation loaded) -->
74 <div v-else-if="error && !invitation" class="sa-card-static sa-invite-card">
75 <div class="sa-card-body" style="padding: var(--sa-space-10) var(--sa-space-8)">
76 <div class="sa-invite-icon" style="color: var(--sa-danger)">
77 <i class="bi bi-exclamation-triangle"></i>
78 </div>
79 <h3 class="mb-3">{{ t('invitations.accept.unavailable') }}</h3>
80 <p style="color: var(--sa-gray-500); font-size: 0.938rem">{{ error }}</p>
81 <button
82 class="sa-btn sa-btn-primary sa-btn-pill"
83 @click="router.push({ name: 'Home' })"
84 >
85 {{ t('common.goHome') }}
86 </button>
87 </div>
88 </div>
89
90 <!-- Action complete -->
91 <div v-else-if="actionDone" class="sa-card-static sa-invite-card">
92 <div class="sa-card-body" style="padding: var(--sa-space-10) var(--sa-space-8)">
93 <div class="sa-invite-icon" style="color: var(--sa-success)">
94 <i class="bi bi-check-circle"></i>
95 </div>
96 <h3 class="mb-3">{{ t('invitations.accept.allDone') }}</h3>
97 <p style="color: var(--sa-gray-500); font-size: 0.938rem; margin-bottom: var(--sa-space-6)">
98 {{ actionMessage }}
99 </p>
100 <button
101 class="sa-btn sa-btn-primary sa-btn-pill"
102 @click="router.push({ name: 'TripsIndex' })"
103 >
104 <i class="bi bi-airplane"></i> {{ t('common.goToTrips') }}
105 </button>
106 </div>
107 </div>
108
109 <!-- Invitation card -->
110 <div v-else-if="invitation" class="sa-card-static sa-invite-card">
111 <div class="sa-card-gradient-strip sa-card-gradient-strip-teal"></div>
112 <div class="sa-card-body" style="padding: var(--sa-space-10) var(--sa-space-8)">
113 <!-- Icon -->
114 <div class="sa-invite-icon" style="color: var(--sa-secondary)">
115 <i class="bi bi-envelope-heart"></i>
116 </div>
117
118 <h3 class="mb-2">{{ t('invitations.accept.title') }}</h3>
119
120 <!-- Trip name -->
121 <p class="fw-bold mb-2" style="font-size: 1.25rem; color: var(--sa-gray-900)">
122 {{ invitation.tripName || t('invitations.accept.aTrip') }}
123 </p>
124
125 <!-- Invited by -->
126 <p v-if="invitation.invitedByUserName" style="color: var(--sa-gray-500); font-size: 0.938rem; margin-bottom: var(--sa-space-2)">
127 <i class="bi bi-person me-1"></i> {{ t('invitations.accept.invitedBy', { name: invitation.invitedByUserName }) }}
128 </p>
129
130 <!-- Expiry -->
131 <p style="color: var(--sa-gray-400); font-size: 0.85rem; margin-bottom: var(--sa-space-6)">
132 <i class="bi bi-clock me-1"></i> {{ t('invitations.accept.expires', { date: formatDate(invitation.expiresAt) }) }}
133 </p>
134
135 <!-- Inline error -->
136 <div v-if="error" class="sa-card-static sa-card-accent sa-card-accent-danger mb-4">
137 <div class="sa-card-body" style="padding: var(--sa-space-3) var(--sa-space-4); font-size: 0.875rem">
138 <i class="bi bi-exclamation-circle me-1"></i>{{ error }}
139 </div>
140 </div>
141
142 <!-- Action buttons -->
143 <div class="d-flex gap-3 justify-content-center">
144 <button
145 class="sa-btn sa-btn-primary sa-btn-pill"
146 :class="{ 'sa-btn-loading': isProcessing }"
147 :disabled="isProcessing"
148 @click="acceptInvitation"
149 >
150 <i class="bi bi-check-lg"></i> {{ t('invitations.accept.accept') }}
151 </button>
152 <button
153 class="sa-btn sa-btn-ghost sa-btn-pill"
154 :disabled="isProcessing"
155 @click="declineInvitation"
156 >
157 {{ t('invitations.accept.decline') }}
158 </button>
159 </div>
160 </div>
161 </div>
162 </div>
163 </template>
164