Skip to content

Commit 4b7d7bd

Browse files
committed
Contact option added
1 parent 0b1b1da commit 4b7d7bd

File tree

9 files changed

+357
-10
lines changed

9 files changed

+357
-10
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { X } from "lucide-react";
5+
import { apiClient } from "../lib/api";
6+
import toast from "react-hot-toast";
7+
8+
interface ContactModalProps {
9+
isOpen: boolean;
10+
onClose: () => void;
11+
}
12+
13+
export default function ContactModal({ isOpen, onClose }: ContactModalProps) {
14+
const [formData, setFormData] = useState({
15+
name: "",
16+
message: "",
17+
});
18+
const [isLoading, setIsLoading] = useState(false);
19+
20+
if (!isOpen) return null;
21+
22+
const handleChange = (
23+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
24+
) => {
25+
const { name, value } = e.target;
26+
setFormData((prev) => ({
27+
...prev,
28+
[name]: value,
29+
}));
30+
};
31+
32+
const handleSubmit = async (e: React.FormEvent) => {
33+
e.preventDefault();
34+
35+
if (!formData.name.trim() || !formData.message.trim()) {
36+
toast.error("Please fill in all fields");
37+
return;
38+
}
39+
40+
setIsLoading(true);
41+
try {
42+
await apiClient.sendContact(formData.name.trim(), formData.message.trim());
43+
toast.success("Message sent successfully!");
44+
setFormData({ name: "", message: "" });
45+
onClose();
46+
} catch (error) {
47+
const errorMessage =
48+
error instanceof Error
49+
? error.message
50+
: "Failed to send message. Please try again.";
51+
toast.error(errorMessage);
52+
} finally {
53+
setIsLoading(false);
54+
}
55+
};
56+
57+
return (
58+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
59+
<div className="bg-slate-800/95 backdrop-blur-md rounded-2xl border border-slate-700/50 shadow-2xl w-full max-w-md">
60+
{/* Header */}
61+
<div className="flex items-center justify-between p-6 border-b border-slate-700/50">
62+
<h2 className="text-xl font-bold text-slate-100">Contact Us</h2>
63+
<button
64+
onClick={onClose}
65+
className="p-2 text-slate-400 hover:text-slate-200 transition-colors cursor-pointer rounded-lg hover:bg-slate-700/50"
66+
aria-label="Close"
67+
>
68+
<X className="w-5 h-5" />
69+
</button>
70+
</div>
71+
72+
{/* Form */}
73+
<form onSubmit={handleSubmit} className="p-6 space-y-5">
74+
<div>
75+
<label
76+
htmlFor="name"
77+
className="block text-sm font-semibold text-slate-300 mb-2"
78+
>
79+
Name
80+
</label>
81+
<input
82+
type="text"
83+
id="name"
84+
name="name"
85+
value={formData.name}
86+
onChange={handleChange}
87+
required
88+
maxLength={200}
89+
autoComplete="name"
90+
className="w-full px-4 py-3 bg-slate-900/80 backdrop-blur-sm border border-slate-600 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all text-slate-100 font-medium placeholder:text-slate-500"
91+
placeholder="Your name"
92+
/>
93+
</div>
94+
95+
<div>
96+
<label
97+
htmlFor="message"
98+
className="block text-sm font-semibold text-slate-300 mb-2"
99+
>
100+
Message
101+
</label>
102+
<textarea
103+
id="message"
104+
name="message"
105+
value={formData.message}
106+
onChange={handleChange}
107+
required
108+
maxLength={5000}
109+
rows={6}
110+
className="w-full px-4 py-3 bg-slate-900/80 backdrop-blur-sm border border-slate-600 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all text-slate-100 font-medium placeholder:text-slate-500 resize-none"
111+
placeholder="Your message..."
112+
/>
113+
<p className="mt-1 text-xs text-slate-500">
114+
{formData.message.length}/5000 characters
115+
</p>
116+
</div>
117+
118+
{/* Actions */}
119+
<div className="flex gap-3 pt-2">
120+
<button
121+
type="button"
122+
onClick={onClose}
123+
disabled={isLoading}
124+
className="flex-1 px-4 py-3 bg-slate-700/50 hover:bg-slate-700 text-slate-200 rounded-xl font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
125+
>
126+
Cancel
127+
</button>
128+
<button
129+
type="submit"
130+
disabled={isLoading}
131+
className="flex-1 px-4 py-3 bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 text-white rounded-xl font-semibold hover:shadow-xl transition-all shadow-lg hover:shadow-indigo-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
132+
>
133+
{isLoading ? (
134+
<span className="flex items-center justify-center">
135+
<svg
136+
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
137+
fill="none"
138+
viewBox="0 0 24 24"
139+
>
140+
<circle
141+
className="opacity-25"
142+
cx="12"
143+
cy="12"
144+
r="10"
145+
stroke="currentColor"
146+
strokeWidth="4"
147+
></circle>
148+
<path
149+
className="opacity-75"
150+
fill="currentColor"
151+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
152+
></path>
153+
</svg>
154+
Sending...
155+
</span>
156+
) : (
157+
"Send Message"
158+
)}
159+
</button>
160+
</div>
161+
</form>
162+
</div>
163+
</div>
164+
);
165+
}
166+

client/app/dashboard/page.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "../lib/api";
1313
import toast from "react-hot-toast";
1414
import { Plus, Menu, X, Lock, Crown, ShieldCheck, User } from "lucide-react";
15+
import ContactModal from "../components/ContactModal";
1516

1617
export default function DashboardPage() {
1718
const { isLoading, isAuthenticated, user, logout } = useAuth();
@@ -24,6 +25,7 @@ export default function DashboardPage() {
2425
null
2526
);
2627
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
28+
const [showContactModal, setShowContactModal] = useState(false);
2729
const [createForm, setCreateForm] = useState<CreateVaultRequest>({
2830
name: "",
2931
description: "",
@@ -242,16 +244,22 @@ export default function DashboardPage() {
242244
</Link>
243245

244246
{/* Desktop Navigation */}
245-
<div className="hidden md:flex items-center space-x-4">
247+
<div className="hidden md:flex items-center gap-1">
246248
<Link
247249
href="/account"
248-
className="px-5 py-2.5 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-800/50 backdrop-blur-sm cursor-pointer"
250+
className="px-4 py-2 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-800/50 backdrop-blur-sm cursor-pointer"
249251
>
250252
Account
251253
</Link>
254+
<button
255+
onClick={() => setShowContactModal(true)}
256+
className="px-4 py-2 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-800/50 backdrop-blur-sm cursor-pointer"
257+
>
258+
Contact
259+
</button>
252260
<button
253261
onClick={handleLogout}
254-
className="px-5 py-2.5 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-800/50 backdrop-blur-sm cursor-pointer"
262+
className="px-4 py-2 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-800/50 backdrop-blur-sm cursor-pointer"
255263
>
256264
Logout
257265
</button>
@@ -273,20 +281,29 @@ export default function DashboardPage() {
273281
{/* Mobile Menu */}
274282
{isMobileMenuOpen && (
275283
<div className="absolute top-full left-0 right-0 mt-2 mx-6 bg-slate-800/95 backdrop-blur-md rounded-xl border border-slate-700/50 shadow-2xl md:hidden z-50">
276-
<div className="flex flex-col p-4 space-y-2">
284+
<div className="flex flex-col p-2">
277285
<Link
278286
href="/account"
279287
onClick={() => setIsMobileMenuOpen(false)}
280-
className="px-5 py-3 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-700/50 cursor-pointer text-left block"
288+
className="px-4 py-2.5 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-700/50 cursor-pointer text-left"
281289
>
282290
Account
283291
</Link>
292+
<button
293+
onClick={() => {
294+
setShowContactModal(true);
295+
setIsMobileMenuOpen(false);
296+
}}
297+
className="px-4 py-2.5 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-700/50 cursor-pointer text-left"
298+
>
299+
Contact
300+
</button>
284301
<button
285302
onClick={() => {
286303
handleLogout();
287304
setIsMobileMenuOpen(false);
288305
}}
289-
className="px-5 py-3 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-700/50 cursor-pointer text-left"
306+
className="px-4 py-2.5 text-slate-300 hover:text-indigo-400 font-medium transition-colors rounded-lg hover:bg-slate-700/50 cursor-pointer text-left"
290307
>
291308
Logout
292309
</button>
@@ -321,7 +338,6 @@ export default function DashboardPage() {
321338
<p className="text-slate-400 text-lg">
322339
You don&apos;t have any vaults yet.
323340
</p>
324-
325341
</div>
326342
) : (
327343
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -646,6 +662,12 @@ export default function DashboardPage() {
646662
</div>
647663
</div>
648664
)}
665+
666+
{/* Contact Modal */}
667+
<ContactModal
668+
isOpen={showContactModal}
669+
onClose={() => setShowContactModal(false)}
670+
/>
649671
</div>
650672
);
651673
}

client/app/lib/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,16 @@ class ApiClient {
488488
});
489489
}
490490

491+
async sendContact(name: string, message: string): Promise<{ message: string }> {
492+
return this.request<{ message: string }>("/contact", {
493+
method: "POST",
494+
body: JSON.stringify({
495+
Name: name,
496+
Message: message,
497+
}),
498+
});
499+
}
500+
491501
isAuthenticated(): boolean {
492502
return !!this.getAccessToken();
493503
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Identity;
3+
using Microsoft.AspNetCore.Mvc;
4+
using server.Dtos.Contact;
5+
using server.Interfaces;
6+
using server.Models;
7+
8+
namespace server.Controllers;
9+
10+
[ApiController]
11+
[Route("api/[controller]")]
12+
[Authorize]
13+
public class ContactController : ControllerBase
14+
{
15+
private readonly UserManager<User> _userManager;
16+
private readonly IEmailService _emailService;
17+
18+
public ContactController(UserManager<User> userManager, IEmailService emailService)
19+
{
20+
_userManager = userManager;
21+
_emailService = emailService;
22+
}
23+
24+
[HttpPost]
25+
public async Task<ActionResult> SendContact([FromBody] ContactRequestDTO dto)
26+
{
27+
if (!ModelState.IsValid)
28+
{
29+
return BadRequest(ModelState);
30+
}
31+
32+
try
33+
{
34+
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
35+
if (string.IsNullOrEmpty(userId))
36+
{
37+
return Unauthorized();
38+
}
39+
40+
var user = await _userManager.FindByIdAsync(userId);
41+
if (user == null)
42+
{
43+
return Unauthorized();
44+
}
45+
46+
await _emailService.SendContactEmailAsync(
47+
user.UserName ?? "Unknown",
48+
user.Email ?? "Unknown",
49+
user.Id,
50+
dto.Name,
51+
dto.Message
52+
);
53+
54+
return Ok(new { message = "Contact message sent successfully" });
55+
}
56+
catch (Exception ex)
57+
{
58+
return StatusCode(500, new { message = "Failed to send contact message. Please try again later." });
59+
}
60+
}
61+
}
62+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace server.Dtos.Contact;
4+
5+
public class ContactRequestDTO
6+
{
7+
[Required]
8+
[StringLength(200, ErrorMessage = "Name cannot exceed 200 characters.")]
9+
public string Name { get; set; } = string.Empty;
10+
11+
[Required]
12+
[StringLength(5000, ErrorMessage = "Message cannot exceed 5000 characters.")]
13+
public string Message { get; set; } = string.Empty;
14+
}
15+

server/Interfaces/IEmailService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public interface IEmailService
66
Task SendDeviceVerificationAsync(string email, string username, string deviceCode, string verificationUrl);
77
Task SendPasswordResetAsync(string email, string username, string resetCode, string resetUrl);
88
Task SendVaultInviteAsync(string email, string inviterName, string vaultName, string inviteUrl, string privilege, string? note = null);
9+
Task SendContactEmailAsync(string userName, string userEmail, string userId, string contactName, string message);
910
}
1011

server/Services/CloudflareR2Service.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ public CloudflareR2Service(IConfiguration configuration, ILogger<CloudflareR2Ser
3434
secretAccessKey = secretAccessKey.Trim();
3535

3636
_logger.LogInformation(
37-
"Initializing Cloudflare R2 service with MinIO | Endpoint: {Endpoint} | Bucket: {Bucket}",
38-
endpoint, _bucketName
37+
"Initializing Cloudflare R2 service with MinIO"
3938
);
4039

4140
// Don't log access key details in production

0 commit comments

Comments
 (0)