Skip to content

Commit e8a7c1c

Browse files
committed
refactor: extract notification component from login page to reuse in other forms
1 parent 1fe68e3 commit e8a7c1c

File tree

5 files changed

+381
-168
lines changed

5 files changed

+381
-168
lines changed
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
<template>
2+
<transition name="slide-fade">
3+
<div v-if="show" class="notification-toast" :class="[`is-${type}`, { 'is-mobile': isMobile }]">
4+
<div class="notification-content">
5+
<span class="notification-icon">
6+
<i :class="iconClass"></i>
7+
</span>
8+
<span class="notification-text">{{ message }}</span>
9+
<button class="delete" @click="close" aria-label="Fechar notificação"></button>
10+
</div>
11+
</div>
12+
</transition>
13+
</template>
14+
15+
<script lang="ts">
16+
import { defineComponent } from 'vue'
17+
18+
export default defineComponent({
19+
name: 'Notification',
20+
props: {
21+
message: {
22+
type: String,
23+
required: true,
24+
},
25+
type: {
26+
type: String,
27+
default: 'info',
28+
validator: (value: string) => ['success', 'danger', 'warning', 'info'].includes(value),
29+
},
30+
duration: {
31+
type: Number,
32+
default: 3000,
33+
},
34+
show: {
35+
type: Boolean,
36+
default: false,
37+
},
38+
},
39+
emits: ['close'],
40+
data() {
41+
return {
42+
timeoutId: null as number | null,
43+
isMobile: false,
44+
}
45+
},
46+
computed: {
47+
iconClass() {
48+
const icons = {
49+
success: 'fas fa-check-circle',
50+
danger: 'fas fa-exclamation-circle',
51+
warning: 'fas fa-exclamation-triangle',
52+
info: 'fas fa-info-circle',
53+
}
54+
return icons[this.type as keyof typeof icons] || icons.info
55+
},
56+
},
57+
watch: {
58+
show(newValue) {
59+
if (newValue && this.duration > 0) {
60+
this.startTimer()
61+
}
62+
},
63+
},
64+
mounted() {
65+
this.checkMobile()
66+
window.addEventListener('resize', this.checkMobile)
67+
if (this.show && this.duration > 0) {
68+
this.startTimer()
69+
}
70+
},
71+
beforeUnmount() {
72+
this.clearTimer()
73+
window.removeEventListener('resize', this.checkMobile)
74+
},
75+
methods: {
76+
close() {
77+
this.clearTimer()
78+
this.$emit('close')
79+
},
80+
startTimer() {
81+
this.clearTimer()
82+
this.timeoutId = window.setTimeout(() => {
83+
this.close()
84+
}, this.duration)
85+
},
86+
clearTimer() {
87+
if (this.timeoutId) {
88+
clearTimeout(this.timeoutId)
89+
this.timeoutId = null
90+
}
91+
},
92+
checkMobile() {
93+
this.isMobile = window.innerWidth <= 768
94+
},
95+
},
96+
})
97+
</script>
98+
99+
<style scoped>
100+
* {
101+
color-scheme: light !important;
102+
}
103+
104+
.notification-toast {
105+
position: fixed;
106+
top: 20px;
107+
right: 20px;
108+
z-index: 99999;
109+
max-width: 400px;
110+
border-radius: 8px;
111+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
112+
backdrop-filter: blur(10px);
113+
}
114+
115+
.notification-content {
116+
display: flex;
117+
align-items: center;
118+
gap: 0.75rem;
119+
padding: 1.25rem;
120+
}
121+
122+
.notification-icon {
123+
color: #ffffff !important;
124+
font-size: 1.5rem;
125+
flex-shrink: 0;
126+
display: flex;
127+
align-items: center;
128+
justify-content: center;
129+
}
130+
131+
.notification-icon i {
132+
color: #ffffff !important;
133+
}
134+
135+
.notification-text {
136+
color: #ffffff !important;
137+
font-weight: 600;
138+
flex: 1;
139+
word-break: break-word;
140+
}
141+
142+
.notification-toast .delete {
143+
background-color: rgba(255, 255, 255, 0.3) !important;
144+
flex-shrink: 0;
145+
margin-left: auto;
146+
}
147+
148+
.notification-toast .delete:hover {
149+
background-color: rgba(255, 255, 255, 0.5) !important;
150+
}
151+
152+
.notification-toast .delete::before,
153+
.notification-toast .delete::after {
154+
background-color: #ffffff !important;
155+
}
156+
157+
.notification-toast.is-success {
158+
background-color: #48c78e !important;
159+
border-left: 4px solid #3ab57a;
160+
animation: successPulse 0.5s ease-out;
161+
}
162+
163+
.notification-toast.is-danger {
164+
background-color: #f14668 !important;
165+
border-left: 4px solid #d93654;
166+
animation: errorShake 0.5s ease-out;
167+
}
168+
169+
.notification-toast.is-warning {
170+
background-color: #ffe08a !important;
171+
border-left: 4px solid #ffd83d;
172+
}
173+
174+
.notification-toast.is-warning .notification-icon,
175+
.notification-toast.is-warning .notification-text {
176+
color: #947600 !important;
177+
}
178+
179+
.notification-toast.is-warning .notification-icon i {
180+
color: #947600 !important;
181+
}
182+
183+
.notification-toast.is-warning .delete::before,
184+
.notification-toast.is-warning .delete::after {
185+
background-color: #947600 !important;
186+
}
187+
188+
.notification-toast.is-info {
189+
background-color: #3e8ed0 !important;
190+
border-left: 4px solid #296fa8;
191+
}
192+
193+
.slide-fade-enter-active {
194+
animation: slideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
195+
}
196+
197+
.slide-fade-leave-active {
198+
animation: slideOut 0.3s ease-in;
199+
}
200+
201+
@keyframes slideIn {
202+
from {
203+
transform: translateX(120%);
204+
opacity: 0;
205+
}
206+
to {
207+
transform: translateX(0);
208+
opacity: 1;
209+
}
210+
}
211+
212+
@keyframes slideOut {
213+
from {
214+
transform: translateX(0) scale(1);
215+
opacity: 1;
216+
}
217+
to {
218+
transform: translateX(120%) scale(0.9);
219+
opacity: 0;
220+
}
221+
}
222+
223+
@keyframes successPulse {
224+
0% {
225+
transform: scale(0.95);
226+
}
227+
50% {
228+
transform: scale(1.05);
229+
}
230+
100% {
231+
transform: scale(1);
232+
}
233+
}
234+
235+
@keyframes errorShake {
236+
0%,
237+
100% {
238+
transform: translateX(0);
239+
}
240+
25% {
241+
transform: translateX(-10px);
242+
}
243+
75% {
244+
transform: translateX(10px);
245+
}
246+
}
247+
248+
@media (max-width: 768px) {
249+
.notification-toast {
250+
top: 10px;
251+
right: 10px;
252+
left: 10px;
253+
max-width: calc(100% - 20px);
254+
}
255+
256+
.notification-content {
257+
font-size: 0.9rem;
258+
padding: 1rem;
259+
}
260+
261+
.notification-icon {
262+
font-size: 1.25rem;
263+
}
264+
}
265+
266+
@media (max-width: 480px) {
267+
.notification-toast {
268+
top: 5px;
269+
right: 5px;
270+
left: 5px;
271+
max-width: calc(100% - 10px);
272+
}
273+
274+
.notification-content {
275+
font-size: 0.85rem;
276+
padding: 0.875rem;
277+
gap: 0.5rem;
278+
}
279+
280+
.notification-icon {
281+
font-size: 1.1rem;
282+
}
283+
}
284+
</style>

frontend/src/components/TimeTrackerForm.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default defineComponent({
6262
components: {
6363
Timer,
6464
},
65+
emits: ['taskCompleted'],
6566
data() {
6667
return {
6768
selectTask: '',
@@ -88,6 +89,7 @@ export default defineComponent({
8889
createTaskPayload.collaborator_id = this.selectCollaborator
8990
}
9091
await postGenericEndPoint('time-trackers', createTaskPayload)
92+
this.$emit('taskCompleted')
9193
},
9294
},
9395
})

0 commit comments

Comments
 (0)