| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- /**
- * Accept Invitation Page — Public page where invited users accept their invitation
- */
- import { useState, useEffect } from "react";
- import { useAuth } from "@/_core/hooks/useAuth";
- import { trpc } from "@/lib/trpc";
- import { Button } from "@/components/ui/button";
- import { getLoginUrl } from "@/const";
- import { toast } from "sonner";
- import {
- CheckCircle2, XCircle, Clock, Mail, Shield,
- Crown, Headphones, User, LogIn, Loader2,
- } from "lucide-react";
- import { useRoute, useLocation } from "wouter";
- const ROLE_LABELS: Record<string, { icon: React.ReactNode; label: string; description: string; color: string }> = {
- admin: {
- icon: <Crown className="w-5 h-5" />,
- label: "Admin",
- description: "Full access to conversations, workflow designer, and user management",
- color: "#14532D",
- },
- agent: {
- icon: <Headphones className="w-5 h-5" />,
- label: "Agent",
- description: "Access to the agent dashboard to monitor and reply to conversations",
- color: "#0369a1",
- },
- user: {
- icon: <User className="w-5 h-5" />,
- label: "User",
- description: "Basic access to the platform",
- color: "#78716C",
- },
- };
- export default function AcceptInvite() {
- const [, params] = useRoute("/invite/:token");
- const [, navigate] = useLocation();
- const token = params?.token || "";
- const { user, isAuthenticated, loading: authLoading } = useAuth();
- const [accepted, setAccepted] = useState(false);
- const { data: validation, isLoading: validating, error: validationError } = trpc.invitations.validate.useQuery(
- { token },
- { enabled: !!token }
- );
- const acceptMutation = trpc.invitations.accept.useMutation({
- onSuccess: (data) => {
- setAccepted(true);
- toast.success(`Welcome! You've been assigned the ${data.role} role.`);
- },
- onError: (error) => {
- toast.error("Failed to accept invitation", { description: error.message });
- },
- });
- const handleAccept = () => {
- if (token) {
- acceptMutation.mutate({ token });
- }
- };
- const handleGoToDashboard = () => {
- navigate("/dashboard");
- };
- // Loading state
- if (authLoading || validating) {
- return (
- <div className="min-h-screen flex items-center justify-center" style={{ background: "#FFFBEB" }}>
- <div className="text-center">
- <Loader2 className="w-8 h-8 animate-spin mx-auto" style={{ color: "#14532D" }} />
- <p className="text-sm mt-3" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
- Validating invitation...
- </p>
- </div>
- </div>
- );
- }
- // Invalid or expired invitation
- if (validation && !validation.valid) {
- return (
- <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
- <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)" }}>
- <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#dc262614" }}>
- <XCircle className="w-8 h-8" style={{ color: "#dc2626" }} />
- </div>
- <h1 className="text-2xl font-bold mb-2" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
- Invalid Invitation
- </h1>
- <p className="text-sm mb-6" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
- {validation.reason}
- </p>
- <Button onClick={() => navigate("/")} variant="outline" style={{ borderColor: "#e7e0d5" }}>
- Go to Home
- </Button>
- </div>
- </div>
- );
- }
- // Successfully accepted
- if (accepted) {
- return (
- <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
- <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)" }}>
- <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#16a34a14" }}>
- <CheckCircle2 className="w-8 h-8" style={{ color: "#16a34a" }} />
- </div>
- <h1 className="text-2xl font-bold mb-2" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
- Welcome Aboard!
- </h1>
- <p className="text-sm mb-6" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
- Your invitation has been accepted. You now have access to the Homelegance chatbot system.
- </p>
- <Button onClick={handleGoToDashboard} className="text-white" style={{ background: "#14532D" }}>
- Go to Dashboard
- </Button>
- </div>
- </div>
- );
- }
- // Valid invitation — show details
- if (validation && validation.valid) {
- const roleConfig = ROLE_LABELS[validation.role] || ROLE_LABELS.user;
- return (
- <div className="min-h-screen flex items-center justify-center p-4" style={{ background: "#FFFBEB" }}>
- <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)" }}>
- <div className="text-center mb-6">
- <div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4" style={{ background: "#14532D14" }}>
- <Mail className="w-8 h-8" style={{ color: "#14532D" }} />
- </div>
- <h1 className="text-2xl font-bold mb-1" style={{ fontFamily: "'Playfair Display', serif", color: "#292524" }}>
- You're Invited!
- </h1>
- <p className="text-sm" style={{ color: "#78716C", fontFamily: "'Source Sans 3', sans-serif" }}>
- <strong>{validation.invitedBy}</strong> has invited you to join the Homelegance chatbot team.
- </p>
- </div>
- {/* Invitation Details */}
- <div className="space-y-3 mb-6">
- <div className="p-4 rounded-xl" style={{ background: roleConfig.color + "08", border: `1px solid ${roleConfig.color}20` }}>
- <div className="flex items-center gap-3">
- <div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ background: roleConfig.color + "18", color: roleConfig.color }}>
- {roleConfig.icon}
- </div>
- <div>
- <div className="text-sm font-semibold" style={{ color: roleConfig.color }}>{roleConfig.label} Role</div>
- <div className="text-xs" style={{ color: "#78716C" }}>{roleConfig.description}</div>
- </div>
- </div>
- </div>
- <div className="flex items-center gap-2 text-xs" style={{ color: "#78716C" }}>
- <Mail className="w-3.5 h-3.5" />
- <span>Invited: <strong>{validation.email}</strong></span>
- </div>
- <div className="flex items-center gap-2 text-xs" style={{ color: "#78716C" }}>
- <Clock className="w-3.5 h-3.5" />
- <span>Expires: {validation.expiresAt ? new Date(validation.expiresAt).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" }) : "—"}</span>
- </div>
- {validation.message && (
- <div className="p-3 rounded-lg" style={{ background: "#f5f0e8" }}>
- <p className="text-sm italic" style={{ color: "#57534e", fontFamily: "'Source Sans 3', sans-serif" }}>
- "{validation.message}"
- </p>
- </div>
- )}
- </div>
- {/* Action */}
- {isAuthenticated ? (
- <Button
- onClick={handleAccept}
- disabled={acceptMutation.isPending}
- className="w-full text-white"
- style={{ background: "#14532D" }}
- >
- {acceptMutation.isPending ? (
- <><Loader2 className="w-4 h-4 animate-spin mr-2" /> Accepting...</>
- ) : (
- <><CheckCircle2 className="w-4 h-4 mr-2" /> Accept Invitation</>
- )}
- </Button>
- ) : (
- <div className="space-y-3">
- <p className="text-xs text-center" style={{ color: "#a8a29e" }}>
- You need to sign in first to accept this invitation.
- </p>
- <a
- href={`${import.meta.env.BASE_URL ?? "/"}login?returnTo=${encodeURIComponent(`/invite/${token}`)}`}
- 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"
- style={{ background: "#14532D" }}
- >
- <LogIn className="w-4 h-4" /> Sign In to Accept
- </a>
- </div>
- )}
- </div>
- </div>
- );
- }
- return null;
- }
|