Skip to content

Commit 41d94d5

Browse files
committed
feat: implemented gratitude modal and action mark
1 parent 9e82eca commit 41d94d5

File tree

10 files changed

+607
-92
lines changed

10 files changed

+607
-92
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import {
5+
SimpleDialog as Dialog,
6+
SimpleDialogContent as DialogContent,
7+
SimpleDialogDescription as DialogDescription,
8+
} from '../ui/simple-dialog';
9+
import { Button } from '../ui/button';
10+
import { Loader2, Heart } from 'lucide-react';
11+
import { sendGratitude } from '../../features/email/api/gratitudeApi';
12+
import { useEntries } from '../../features/statements/hooks/useEntries';
13+
import { Action } from '../../types/entries';
14+
15+
interface GratitudeModalProps {
16+
onClose: () => void;
17+
statementId: string;
18+
action: Action;
19+
onGratitudeSent: (
20+
statementId: string,
21+
actionId: string,
22+
message: string
23+
) => void;
24+
}
25+
26+
const GratitudeModal: React.FC<GratitudeModalProps> = ({
27+
onClose,
28+
statementId,
29+
action,
30+
onGratitudeSent,
31+
}) => {
32+
const { data } = useEntries();
33+
const { managerName, managerEmail } = data;
34+
35+
const [message, setMessage] = useState('');
36+
const [isSending, setIsSending] = useState(false);
37+
const [sendError, setSendError] = useState<string | null>(null);
38+
const [sendSuccess, setSendSuccess] = useState(false);
39+
40+
const handleSendGratitude = async () => {
41+
try {
42+
setIsSending(true);
43+
setSendError(null);
44+
45+
if (!managerEmail || managerEmail.trim() === '') {
46+
throw new Error(
47+
'Manager email is not set. Please set a manager email first.'
48+
);
49+
}
50+
51+
if (!message.trim()) {
52+
throw new Error('Please enter a gratitude message.');
53+
}
54+
55+
const gratitudeRequest = {
56+
statementId,
57+
actionId: action.id,
58+
message: message.trim(),
59+
recipientEmail: managerEmail,
60+
recipientName: managerName,
61+
};
62+
63+
// Check if we're in mock mode
64+
const isMockMode =
65+
typeof import.meta.env.VITE_MOCK_EMAIL_SENDING === 'undefined' ||
66+
import.meta.env.VITE_MOCK_EMAIL_SENDING === 'true';
67+
68+
try {
69+
// Try to send gratitude via API
70+
await sendGratitude(gratitudeRequest);
71+
} catch (apiError) {
72+
// If we're in mock mode, continue as if successful
73+
if (!isMockMode) {
74+
// Only throw the error if we're not in mock mode
75+
throw apiError;
76+
}
77+
console.warn('API error in mock mode (continuing anyway):', apiError);
78+
}
79+
80+
// Always mark the action as having gratitude sent
81+
// (This works regardless of API success when in mock mode)
82+
onGratitudeSent(statementId, action.id, message);
83+
84+
setSendSuccess(true);
85+
86+
// Automatically close after successful send
87+
setTimeout(() => onClose(), 2000);
88+
} catch (error) {
89+
console.error('Failed to send gratitude:', error);
90+
setSendError(
91+
typeof error === 'object' && error !== null && 'message' in error
92+
? (error as Error).message
93+
: 'Failed to send gratitude email. Please try again later.'
94+
);
95+
} finally {
96+
setIsSending(false);
97+
}
98+
};
99+
100+
return (
101+
<>
102+
<Dialog open onOpenChange={onClose}>
103+
<DialogContent headerTitle='Send Gratitude'>
104+
<div
105+
className='relative rounded-md overflow-hidden'
106+
style={{
107+
border: '8px solid transparent',
108+
borderImage:
109+
'repeating-linear-gradient(45deg, #ff69b4, #ff69b4 10px, #fb86b0 10px, #fb86b0 20px) 8',
110+
padding: '16px',
111+
margin: '-8px',
112+
background: '#f9f9f9',
113+
}}
114+
onClick={(e) => e.stopPropagation()}
115+
>
116+
{/* Heart decoration */}
117+
<div className='absolute top-4 right-4 text-pink-400 opacity-20 pointer-events-none'>
118+
<Heart size={40} />
119+
</div>
120+
121+
<DialogDescription className='mt-0 text-center'>
122+
Express gratitude for this action
123+
</DialogDescription>
124+
125+
<div className='text-center mb-4'>
126+
<p className='text-sm text-gray-500 mt-1'>
127+
To: {managerName || managerEmail || 'No recipient set'}
128+
</p>
129+
{sendSuccess && (
130+
<p className='text-green-600 text-sm font-medium mt-2 bg-green-50 py-1 px-2 rounded-full inline-block'>
131+
✓ Gratitude sent successfully!
132+
</p>
133+
)}
134+
</div>
135+
136+
{sendError && (
137+
<div className='bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4'>
138+
{sendError}
139+
</div>
140+
)}
141+
142+
<div className='mb-6 p-4 bg-white rounded-md shadow-sm border border-pink-100'>
143+
<h3 className='font-semibold text-gray-700 mb-2 flex items-center'>
144+
<span className='mr-2'></span>
145+
Action
146+
</h3>
147+
<p className='text-gray-600'>{action.action}</p>
148+
{action.byDate && (
149+
<p className='text-xs text-gray-500 mt-1'>
150+
Due by: {action.byDate}
151+
</p>
152+
)}
153+
</div>
154+
155+
<div className='mb-6'>
156+
<label
157+
htmlFor='gratitude-message'
158+
className='block text-sm font-medium text-gray-700 mb-2'
159+
>
160+
Your gratitude message:
161+
</label>
162+
<textarea
163+
id='gratitude-message'
164+
rows={4}
165+
className='w-full rounded-md border border-pink-200 focus:border-pink-500 focus:ring-pink-500 text-gray-900 p-3'
166+
placeholder='Express your thanks and appreciation here...'
167+
value={message}
168+
onChange={(e) => setMessage(e.target.value)}
169+
/>
170+
<p className='text-xs text-gray-500 mt-1'>
171+
This message will be sent with your gratitude email.
172+
</p>
173+
</div>
174+
175+
<div className='mt-6 flex justify-center gap-4'>
176+
<Button
177+
variant='default'
178+
onClick={handleSendGratitude}
179+
disabled={isSending || !message.trim() || !managerEmail}
180+
className='shadow-sm bg-pink-600 hover:bg-pink-700 text-white'
181+
>
182+
{isSending ? (
183+
<>
184+
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
185+
<span>Sending...</span>
186+
</>
187+
) : (
188+
<>
189+
<Heart className='mr-2 h-4 w-4' />
190+
<span>Send Gratitude</span>
191+
</>
192+
)}
193+
</Button>
194+
<Button variant='outline' onClick={onClose}>
195+
<span>Cancel</span>
196+
</Button>
197+
</div>
198+
</div>
199+
</DialogContent>
200+
</Dialog>
201+
</>
202+
);
203+
};
204+
205+
export default GratitudeModal;

src/components/modals/PrivacyModal.tsx

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import React from 'react';
4-
import { SimpleDialog as Dialog, SimpleDialogContent as DialogContent } from '../ui/simple-dialog';
4+
// import { SimpleDialog as Dialog, SimpleDialogContent as DialogContent } from '../ui/simple-dialog';
55
import { Button } from '../ui/button';
66
import { X, Shield, CheckCircle, Lock, Eye } from 'lucide-react';
77

@@ -19,17 +19,17 @@ const PrivacyModal: React.FC<PrivacyModalProps> = ({ onClose }) => {
1919
};
2020

2121
return (
22-
<div
22+
<div
2323
className='fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-black/50'
2424
onClick={handleOutsideClick}
2525
>
26-
<div
26+
<div
2727
className='bg-white m-2 sm:m-5 max-w-2xl w-full rounded-lg p-0 overflow-hidden shadow-xl'
2828
onClick={(e) => e.stopPropagation()} // Prevent clicks inside from closing
2929
>
3030
<div className='bg-brand-pink p-2 flex items-center justify-between sm:rounded-t-lg'>
3131
<h2 className='text-xl font-semibold text-white flex items-center'>
32-
<Shield className="mr-2" size={20} />
32+
<Shield className='mr-2' size={20} />
3333
Privacy & Data Protection
3434
</h2>
3535
<button
@@ -47,37 +47,46 @@ const PrivacyModal: React.FC<PrivacyModalProps> = ({ onClose }) => {
4747
}}
4848
>
4949
<div className='p-4 sm:p-6'>
50-
<div className="space-y-6">
50+
<div className='space-y-6'>
5151
{/* How We Protect Your Data */}
5252
<section>
53-
<h3 className="text-lg font-semibold flex items-center text-gray-800 mb-3">
54-
<Lock className="mr-2 text-brand-pink" size={20} />
53+
<h3 className='text-lg font-semibold flex items-center text-gray-800 mb-3'>
54+
<Lock className='mr-2 text-brand-pink' size={20} />
5555
How We Protect Your Data
5656
</h3>
57-
<div className="bg-white p-4 rounded-lg shadow-sm">
58-
<p className="mb-3 text-gray-700">
59-
At Beacons, we prioritize your privacy and ensure your personal information is handled securely.
57+
<div className='bg-white p-4 rounded-lg shadow-sm'>
58+
<p className='mb-3 text-gray-700'>
59+
At Beacons, we prioritize your privacy and ensure your
60+
personal information is handled securely.
6061
</p>
61-
<ul className="list-disc pl-5 space-y-2 text-gray-700">
62+
<ul className='list-disc pl-5 space-y-2 text-gray-700'>
6263
<li>Your data is encrypted both in transit and at rest</li>
63-
<li>We use secure passwordless authentication (Magic Links) to protect your account</li>
64-
<li>We only collect the minimum information needed to provide our service</li>
65-
<li>You retain complete control over your data at all times</li>
64+
<li>
65+
We use secure passwordless authentication (Magic Links) to
66+
protect your account
67+
</li>
68+
<li>
69+
We only collect the minimum information needed to provide
70+
our service
71+
</li>
72+
<li>
73+
You retain complete control over your data at all times
74+
</li>
6675
</ul>
6776
</div>
6877
</section>
6978

7079
{/* Your Rights */}
7180
<section>
72-
<h3 className="text-lg font-semibold flex items-center text-gray-800 mb-3">
73-
<CheckCircle className="mr-2 text-brand-pink" size={20} />
81+
<h3 className='text-lg font-semibold flex items-center text-gray-800 mb-3'>
82+
<CheckCircle className='mr-2 text-brand-pink' size={20} />
7483
Your Rights
7584
</h3>
76-
<div className="bg-white p-4 rounded-lg shadow-sm">
77-
<p className="mb-3 text-gray-700">
85+
<div className='bg-white p-4 rounded-lg shadow-sm'>
86+
<p className='mb-3 text-gray-700'>
7887
Under data protection law, you have the right to:
7988
</p>
80-
<ul className="list-disc pl-5 space-y-2 text-gray-700">
89+
<ul className='list-disc pl-5 space-y-2 text-gray-700'>
8190
<li>Access your personal data</li>
8291
<li>Correct inaccurate data</li>
8392
<li>Request deletion of your data</li>
@@ -90,25 +99,29 @@ const PrivacyModal: React.FC<PrivacyModalProps> = ({ onClose }) => {
9099

91100
{/* Data Sharing */}
92101
<section>
93-
<h3 className="text-lg font-semibold flex items-center text-gray-800 mb-3">
94-
<Eye className="mr-2 text-brand-pink" size={20} />
102+
<h3 className='text-lg font-semibold flex items-center text-gray-800 mb-3'>
103+
<Eye className='mr-2 text-brand-pink' size={20} />
95104
Data Sharing
96105
</h3>
97-
<div className="bg-white p-4 rounded-lg shadow-sm">
98-
<p className="mb-3 text-gray-700">
99-
We only share your data:
100-
</p>
101-
<ul className="list-disc pl-5 space-y-2 text-gray-700">
106+
<div className='bg-white p-4 rounded-lg shadow-sm'>
107+
<p className='mb-3 text-gray-700'>We only share your data:</p>
108+
<ul className='list-disc pl-5 space-y-2 text-gray-700'>
102109
<li>With your explicit consent</li>
103-
<li>Only statements marked as "public" can be shared with your line manager</li>
110+
<li>
111+
Only statements marked as "public" can be shared with your
112+
line manager
113+
</li>
104114
<li>You control when and how your data is shared</li>
105-
<li>Email sharing is initiated by you and only sent to the email address you provide</li>
115+
<li>
116+
Email sharing is initiated by you and only sent to the
117+
email address you provide
118+
</li>
106119
</ul>
107120
</div>
108121
</section>
109122
</div>
110123

111-
<div className="mt-6 flex justify-center">
124+
<div className='mt-6 flex justify-center'>
112125
<Button
113126
variant='outline'
114127
className='px-6'
@@ -126,4 +139,4 @@ const PrivacyModal: React.FC<PrivacyModalProps> = ({ onClose }) => {
126139
);
127140
};
128141

129-
export default PrivacyModal;
142+
export default PrivacyModal;

0 commit comments

Comments
 (0)