AcceptInvite.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. * Accept Invitation Page — Public page where invited users accept their invitation
  3. */
  4. import { useState, useEffect } from "react";
  5. import { useAuth } from "@/_core/hooks/useAuth";
  6. import { trpc } from "@/lib/trpc";
  7. import { Button } from "@/components/ui/button";
  8. import { getLoginUrl } from "@/const";
  9. import { toast } from "sonner";
  10. import {
  11. CheckCircle2, XCircle, Clock, Mail, Shield,
  12. Crown, Headphones, User, LogIn, Loader2,
  13. } from "lucide-react";
  14. import { useRoute, useLocation } from "wouter";
  15. const ROLE_LABELS: Record<string, { icon: React.ReactNode; label: string; description: string; color: string }> = {
  16. admin: {
  17. icon: <Crown className="w-5 h-5" />,
  18. label: "Admin",
  19. description: "Full access to conversations, workflow designer, and user management",
  20. color: "#14532D",
  21. },
  22. agent: {
  23. icon: <Headphones className="w-5 h-5" />,
  24. label: "Agent",
  25. description: "Access to the agent dashboard to monitor and reply to conversations",
  26. color: "#0369a1",
  27. },
  28. user: {
  29. icon: <User className="w-5 h-5" />,
  30. label: "User",
  31. description: "Basic access to the platform",
  32. color: "#78716C",
  33. },
  34. };
  35. export default function AcceptInvite() {
  36. const [, params] = useRoute("/invite/:token");
  37. const [, navigate] = useLocation();
  38. const token = params?.token || "";
  39. const { user, isAuthenticated, loading: authLoading } = useAuth();
  40. const [accepted, setAccepted] = useState(false);
  41. const { data: validation, isLoading: validating, error: validationError } = trpc.invitations.validate.useQuery(
  42. { token },
  43. { enabled: !!token }
  44. );
  45. const acceptMutation = trpc.invitations.accept.useMutation({
  46. onSuccess: (data) => {
  47. setAccepted(true);
  48. toast.success(`Welcome! You've been assigned the ${data.role} role.`);
  49. },
  50. onError: (error) => {
  51. toast.error("Failed to accept invitation", { description: error.message });
  52. },
  53. });
  54. const handleAccept = () => {
  55. if (token) {
  56. acceptMutation.mutate({ token });
  57. }
  58. };
  59. const handleGoToDashboard = () => {
  60. navigate("/dashboard");
  61. };
  62. // Loading state
  63. if (authLoading || validating) {
  64. return (
  65. <div className="min-h-screen flex items-center justify-center" style={{ background: "#FFFBEB" }}>
  66. <div className="text-center">
  67. <Loader2 className="w-8 h-8 animate-spin mx-auto" style={{ color: "#14532D" }} />
  68. <p className="text-sm mt-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
  69. Validating invitation...
  70. </p>
  71. </div>
  72. </div>
  73. );
  74. }
  75. // Invalid or expired invitation
  76. if (validation && !validation.valid) {
  77. return (
  78. <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
  79. <div className="max-w-md w-full p-8 rounded-2xl text-center" style={{ background: "#fff", border: "1px solid #e7e0d5", boxShadow: "0 4px 24px rgba(120, 113, 108, 0.08)" }}>
  80. <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#dc262614" }}>
  81. <XCircle className="w-8 h-8" style={{ color: "#dc2626" }} />
  82. </div>
  83. <h1 className="text-2xl font-bold mb-2" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
  84. Invalid Invitation
  85. </h1>
  86. <p className="text-sm mb-6" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
  87. {validation.reason}
  88. </p>
  89. <Button onClick={() => navigate("/")} variant="outline" style={{ borderColor: "#e7e0d5" }}>
  90. Go to Home
  91. </Button>
  92. </div>
  93. </div>
  94. );
  95. }
  96. // Successfully accepted
  97. if (accepted) {
  98. return (
  99. <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
  100. <div className="max-w-md w-full p-8 rounded-2xl text-center" style={{ background: "#fff", border: "1px solid #e7e0d5", boxShadow: "0 4px 24px rgba(120, 113, 108, 0.08)" }}>
  101. <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#16a34a14" }}>
  102. <CheckCircle2 className="w-8 h-8" style={{ color: "#16a34a" }} />
  103. </div>
  104. <h1 className="text-2xl font-bold mb-2" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
  105. Welcome Aboard!
  106. </h1>
  107. <p className="text-sm mb-6" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
  108. Your invitation has been accepted. You now have access to the Homelegance chatbot system.
  109. </p>
  110. <Button onClick={handleGoToDashboard} className="text-white" style={{ background: "#14532D" }}>
  111. Go to Dashboard
  112. </Button>
  113. </div>
  114. </div>
  115. );
  116. }
  117. // Valid invitation — show details
  118. if (validation && validation.valid) {
  119. const roleConfig = ROLE_LABELS[validation.role] || ROLE_LABELS.user;
  120. return (
  121. <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
  122. <div className="max-w-md w-full p-8 rounded-2xl" style={{ background: "#fff", border: "1px solid #e7e0d5", boxShadow: "0 4px 24px rgba(120, 113, 108, 0.08)" }}>
  123. <div className="text-center mb-6">
  124. <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#14532D14" }}>
  125. <Mail className="w-8 h-8" style={{ color: "#14532D" }} />
  126. </div>
  127. <h1 className="text-2xl font-bold mb-1" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
  128. You're Invited!
  129. </h1>
  130. <p className="text-sm" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
  131. <strong>{validation.invitedBy}</strong> has invited you to join the Homelegance chatbot team.
  132. </p>
  133. </div>
  134. {/* Invitation Details */}
  135. <div className="space-y-3 mb-6">
  136. <div className="p-4 rounded-xl" style={{ background: roleConfig.color + "08", border: `1px solid ${roleConfig.color}20` }}>
  137. <div className="flex items-center gap-3">
  138. <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: roleConfig.color + "18", color: roleConfig.color }}>
  139. {roleConfig.icon}
  140. </div>
  141. <div>
  142. <div className="text-sm font-semibold" style={{ color: roleConfig.color }}>{roleConfig.label} Role</div>
  143. <div className="text-xs" style={{ color: "#78716C" }}>{roleConfig.description}</div>
  144. </div>
  145. </div>
  146. </div>
  147. <div className="flex items-center gap-2 text-xs" style={{ color: "#78716C" }}>
  148. <Mail className="w-3.5 h-3.5" />
  149. <span>Invited: <strong>{validation.email}</strong></span>
  150. </div>
  151. <div className="flex items-center gap-2 text-xs" style={{ color: "#78716C" }}>
  152. <Clock className="w-3.5 h-3.5" />
  153. <span>Expires: {validation.expiresAt ? new Date(validation.expiresAt).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" }) : "—"}</span>
  154. </div>
  155. {validation.message && (
  156. <div className="p-3 rounded-lg" style={{ background: "#f5f0e8" }}>
  157. <p className="text-sm italic" style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}>
  158. "{validation.message}"
  159. </p>
  160. </div>
  161. )}
  162. </div>
  163. {/* Action */}
  164. {isAuthenticated ? (
  165. <Button
  166. onClick={handleAccept}
  167. disabled={acceptMutation.isPending}
  168. className="w-full text-white"
  169. style={{ background: "#14532D" }}
  170. >
  171. {acceptMutation.isPending ? (
  172. <><Loader2 className="w-4 h-4 animate-spin mr-2" /> Accepting...</>
  173. ) : (
  174. <><CheckCircle2 className="w-4 h-4 mr-2" /> Accept Invitation</>
  175. )}
  176. </Button>
  177. ) : (
  178. <div className="space-y-3">
  179. <p className="text-xs text-center" style={{ color: "#a8a29e" }}>
  180. You need to sign in first to accept this invitation.
  181. </p>
  182. <a
  183. href={`${import.meta.env.BASE_URL ?? "/"}login?returnTo=${encodeURIComponent(`/invite/${token}`)}`}
  184. className="flex items-center justify-center gap-2 w-full px-4 py-2.5 rounded-xl text-sm font-semibold text-white transition-colors"
  185. style={{ background: "#14532D" }}
  186. >
  187. <LogIn className="w-4 h-4" /> Sign In to Accept
  188. </a>
  189. </div>
  190. )}
  191. </div>
  192. </div>
  193. );
  194. }
  195. return null;
  196. }