mirror of
https://github.com/FranP-code/inbox-negotiator.git
synced 2025-10-13 00:42:26 +00:00
feat: enhance Navbar with user profile and configuration links
- Updated Navbar component to include a link to the configuration page. - Added a new Settings icon and link for user configuration. - Improved user session handling and UI updates based on authentication state. feat: implement OnboardingDialog for user setup - Created OnboardingDialog component to guide users through initial setup. - Added functionality to collect additional email addresses during onboarding. - Integrated toast notifications for error handling during email addition. feat: extend Supabase admin functions for user management - Added functions to retrieve user IDs and full user information by email. - Implemented error handling and logging for database operations. feat: update Supabase schema with new user features - Created new tables: user_profiles, additional_emails, and email_processing_usage. - Enabled Row Level Security (RLS) on new tables with appropriate policies. - Added triggers and functions for automatic user profile creation and email usage tracking. feat: create public users table for simplified access - Established a public.users table to mirror relevant auth.users data. - Implemented triggers to automatically populate public.users upon user creation. - Set up RLS policies to restrict access to user data. chore: add configuration files for Supabase local development - Included .gitignore and config.toml for local Supabase setup. - Configured email testing server and other development settings. feat: add configuration page for user settings - Created configuration.astro page to manage user settings. - Integrated AuthGuard to protect the configuration route.
This commit is contained in:
400
src/components/Configuration.tsx
Normal file
400
src/components/Configuration.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
supabase,
|
||||
type AdditionalEmail,
|
||||
type UserProfile,
|
||||
type EmailProcessingUsage,
|
||||
} from "../lib/supabase";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Settings,
|
||||
Mail,
|
||||
Plus,
|
||||
Trash2,
|
||||
Check,
|
||||
X,
|
||||
TrendingUp,
|
||||
Infinity,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { toast } from "../hooks/use-toast";
|
||||
|
||||
export function Configuration() {
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [additionalEmails, setAdditionalEmails] = useState<AdditionalEmail[]>(
|
||||
[]
|
||||
);
|
||||
const [usage, setUsage] = useState<EmailProcessingUsage | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [addingEmail, setAddingEmail] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
const fetchUserData = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
// Fetch user profile
|
||||
const { data: profileData } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
// Fetch additional emails
|
||||
const { data: emailsData } = await supabase
|
||||
.from("additional_emails")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
// Fetch current month usage
|
||||
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
||||
const { data: usageData } = await supabase
|
||||
.from("email_processing_usage")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.eq("month_year", currentMonth)
|
||||
.single();
|
||||
|
||||
setProfile(profileData);
|
||||
setAdditionalEmails(emailsData || []);
|
||||
setUsage(usageData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addAdditionalEmail = async () => {
|
||||
if (!newEmail || !profile) return;
|
||||
|
||||
setAddingEmail(true);
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("additional_emails")
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
email_address: newEmail.trim().toLowerCase(),
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setAdditionalEmails([data, ...additionalEmails]);
|
||||
setNewEmail("");
|
||||
toast({
|
||||
title: "Email added successfully",
|
||||
description: "Additional email has been added to your account.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error adding email",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setAddingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeAdditionalEmail = async (emailId: string) => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("additional_emails")
|
||||
.delete()
|
||||
.eq("id", emailId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setAdditionalEmails(
|
||||
additionalEmails.filter((email) => email.id !== emailId)
|
||||
);
|
||||
toast({
|
||||
title: "Email removed",
|
||||
description: "Additional email has been removed from your account.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error removing email",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getUsagePercentage = () => {
|
||||
if (!profile || !usage) return 0;
|
||||
return Math.min(
|
||||
(usage.emails_processed / profile.email_processing_limit) * 100,
|
||||
100
|
||||
);
|
||||
};
|
||||
|
||||
const getRemainingEmails = () => {
|
||||
if (!profile || !usage) return profile?.email_processing_limit || 1000;
|
||||
return Math.max(profile.email_processing_limit - usage.emails_processed, 0);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-background">
|
||||
<div className="flex items-center gap-2 text-lg text-gray-900 dark:text-foreground">
|
||||
<Settings className="h-5 w-5 animate-spin" />
|
||||
Loading configuration...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-background">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-foreground flex items-center gap-3">
|
||||
<Settings className="h-8 w-8 text-primary" />
|
||||
Configuration
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300 mt-2">
|
||||
Manage your account settings and email processing options
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Email Processing Usage */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5" />
|
||||
Email Processing Usage
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Track your monthly email processing usage and limits
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
Emails Processed This Month
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{usage?.emails_processed || 0} /{" "}
|
||||
{profile?.email_processing_limit || 1000}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">Remaining</p>
|
||||
<p className="text-lg font-semibold text-green-600 dark:text-green-400">
|
||||
{getRemainingEmails() ===
|
||||
(profile?.email_processing_limit || 1000) ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Infinity className="h-4 w-4" />
|
||||
Unlimited
|
||||
</span>
|
||||
) : (
|
||||
getRemainingEmails()
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Progress</span>
|
||||
<span>{getUsagePercentage().toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress value={getUsagePercentage()} className="h-2" />
|
||||
</div>
|
||||
|
||||
{getUsagePercentage() > 80 && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You're approaching your monthly email processing limit.
|
||||
Consider upgrading your plan if you need to process more
|
||||
emails.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Additional Emails */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5" />
|
||||
Additional Email Addresses
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Add additional email addresses to process debt emails from
|
||||
multiple accounts
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Add new email */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="newEmail" className="sr-only">
|
||||
Additional Email
|
||||
</Label>
|
||||
<Input
|
||||
id="newEmail"
|
||||
type="email"
|
||||
placeholder="additional@example.com"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
addAdditionalEmail();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={addAdditionalEmail}
|
||||
disabled={!newEmail || addingEmail}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Email
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* List of additional emails */}
|
||||
<div className="space-y-3">
|
||||
{additionalEmails.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Mail className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium">No additional emails</p>
|
||||
<p className="text-sm">
|
||||
Add additional email addresses to expand your debt
|
||||
processing capabilities.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
additionalEmails.map((email) => (
|
||||
<div
|
||||
key={email.id}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">{email.email_address}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Added{" "}
|
||||
{new Date(email.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={email.verified ? "default" : "secondary"}
|
||||
>
|
||||
{email.verified ? (
|
||||
<>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Verified
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="h-3 w-3 mr-1" />
|
||||
Unverified
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeAdditionalEmail(email.id)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
<CardDescription>
|
||||
Your account details and settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Email Processing Limit</Label>
|
||||
<p className="text-lg font-semibold">
|
||||
{profile?.email_processing_limit || 1000} emails/month
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Account Created</Label>
|
||||
<p className="text-lg font-semibold">
|
||||
{profile?.created_at
|
||||
? new Date(profile.created_at).toLocaleDateString()
|
||||
: "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Onboarding Status</Label>
|
||||
<Badge
|
||||
variant={
|
||||
profile?.onboarding_completed ? "default" : "secondary"
|
||||
}
|
||||
>
|
||||
{profile?.onboarding_completed ? "Completed" : "Pending"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Additional Emails</Label>
|
||||
<p className="text-lg font-semibold">
|
||||
{additionalEmails.length} configured
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { supabase, type Debt } from "../lib/supabase";
|
||||
import { supabase, type Debt, type UserProfile } from "../lib/supabase";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DebtCard } from "./DebtCard";
|
||||
import { DebtTimeline } from "./DebtTimeline";
|
||||
import { OnboardingDialog } from "./OnboardingDialog";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -22,11 +23,14 @@ import {
|
||||
RefreshCw,
|
||||
BarChart3,
|
||||
LogOut,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
|
||||
export function Dashboard() {
|
||||
const [debts, setDebts] = useState<Debt[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const [stats, setStats] = useState({
|
||||
totalDebts: 0,
|
||||
totalAmount: 0,
|
||||
@@ -35,6 +39,7 @@ export function Dashboard() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserProfile();
|
||||
fetchDebts();
|
||||
setupRealtimeSubscription();
|
||||
}, []);
|
||||
@@ -43,11 +48,41 @@ export function Dashboard() {
|
||||
calculateStats();
|
||||
}, [debts]);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
setUserProfile(profile);
|
||||
|
||||
// Show onboarding if user hasn't completed it
|
||||
if (profile && !profile.onboarding_completed) {
|
||||
setShowOnboarding(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user profile:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDebts = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("debts")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
@@ -111,6 +146,12 @@ export function Dashboard() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnboardingComplete = () => {
|
||||
setShowOnboarding(false);
|
||||
// Refresh user profile to reflect onboarding completion
|
||||
fetchUserProfile();
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
window.location.href = "/";
|
||||
@@ -152,13 +193,18 @@ export function Dashboard() {
|
||||
<div className="mb-8 flex justify-between items-start">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-foreground flex items-center gap-3">
|
||||
<BarChart3 className="h-8 w-8 text-primary" />
|
||||
InboxNegotiator Dashboard
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-300 mt-2">
|
||||
AI-powered debt resolution platform with real-time updates
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<a href="/configuration" className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Configuration
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
@@ -290,6 +336,12 @@ export function Dashboard() {
|
||||
<p className="mt-1">Real-time updates powered by Supabase</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Onboarding Dialog */}
|
||||
<OnboardingDialog
|
||||
open={showOnboarding}
|
||||
onComplete={handleOnboardingComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,104 +1,118 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { BarChart3, LogOut, User as UserIcon } from 'lucide-react';
|
||||
import { ModeToggle } from './ModeToggle';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import type { User } from "@supabase/supabase-js";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { BarChart3, LogOut, User as UserIcon, Settings } from "lucide-react";
|
||||
import { ModeToggle } from "./ModeToggle";
|
||||
|
||||
export function Navbar() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
window.location.href = '/';
|
||||
};
|
||||
const handleSignOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
const getInitials = (email: string) => {
|
||||
return email.substring(0, 2).toUpperCase();
|
||||
};
|
||||
const getInitials = (email: string) => {
|
||||
return email.substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="border-b bg-white dark:bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-3">
|
||||
<BarChart3 className="h-8 w-8 text-primary" />
|
||||
<a href="/" className="text-xl font-bold text-gray-900 dark:text-foreground">
|
||||
InboxNegotiator
|
||||
</a>
|
||||
</div>
|
||||
return (
|
||||
<nav className="border-b bg-white dark:bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-3">
|
||||
<BarChart3 className="h-8 w-8 text-primary" />
|
||||
<a
|
||||
href={user ? "/dashboard" : "/"}
|
||||
className="text-xl font-bold text-gray-900 dark:text-foreground"
|
||||
>
|
||||
InboxNegotiator
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<ModeToggle />
|
||||
{user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback className="text-xs">
|
||||
{getInitials(user.email || '')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end">
|
||||
<div className="flex items-center justify-start gap-2 p-2">
|
||||
<div className="flex flex-col space-y-1 leading-none">
|
||||
<p className="font-medium text-sm">{user.user_metadata?.full_name || 'User'}</p>
|
||||
<p className="w-[200px] truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/dashboard" className="flex items-center">
|
||||
<UserIcon className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleSignOut}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" asChild>
|
||||
<a href="/login">Sign In</a>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a href="/signup">Get Started</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
<div className="flex items-center gap-4">
|
||||
<ModeToggle />
|
||||
{user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative h-8 w-8 rounded-full"
|
||||
>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback className="text-xs">
|
||||
{getInitials(user.email || "")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end">
|
||||
<div className="flex items-center justify-start gap-2 p-2">
|
||||
<div className="flex flex-col space-y-1 leading-none">
|
||||
<p className="font-medium text-sm">
|
||||
{user.user_metadata?.full_name || "User"}
|
||||
</p>
|
||||
<p className="w-[200px] truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/dashboard" className="flex items-center">
|
||||
<UserIcon className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a href="/configuration" className="flex items-center">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Configuration
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleSignOut}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" asChild>
|
||||
<a href="/login">Sign In</a>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a href="/signup">Get Started</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
235
src/components/OnboardingDialog.tsx
Normal file
235
src/components/OnboardingDialog.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useState } from "react";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Mail, Plus, CheckCircle, ArrowRight, UserCheck } from "lucide-react";
|
||||
import { toast } from "../hooks/use-toast";
|
||||
|
||||
interface OnboardingDialogProps {
|
||||
open: boolean;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
const [step, setStep] = useState<"welcome" | "email" | "complete">("welcome");
|
||||
const [email, setEmail] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [skipEmail, setSkipEmail] = useState(false);
|
||||
|
||||
const handleAddEmail = async () => {
|
||||
if (!email && !skipEmail) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
if (email && !skipEmail) {
|
||||
const { error } = await supabase.from("additional_emails").insert({
|
||||
user_id: user.id,
|
||||
email_address: email.trim().toLowerCase(),
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
// Mark onboarding as completed
|
||||
const { error: profileError } = await supabase
|
||||
.from("user_profiles")
|
||||
.update({ onboarding_completed: true })
|
||||
.eq("user_id", user.id);
|
||||
|
||||
if (profileError) throw profileError;
|
||||
|
||||
setStep("complete");
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
onComplete();
|
||||
};
|
||||
|
||||
const handleSkipEmail = () => {
|
||||
setSkipEmail(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={() => {}}>
|
||||
<DialogContent className="sm:max-w-md [&>button]:hidden">
|
||||
{/* Hide close button */}
|
||||
{step === "welcome" && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<UserCheck className="h-6 w-6 text-green-500" />
|
||||
Welcome to InboxNegotiator!
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
Your account has been created successfully. Let's get you set up
|
||||
to start processing debt emails with AI assistance.
|
||||
</DialogDescription>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<Mail className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
InboxNegotiator helps you automatically negotiate debt
|
||||
settlements by processing emails sent to your configured email
|
||||
addresses.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<h4 className="font-medium">What you can do:</h4>
|
||||
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||
<li>• Process up to 1,000 debt emails per month</li>
|
||||
<li>• AI-powered debt amount and vendor extraction</li>
|
||||
<li>• Automated negotiation responses</li>
|
||||
<li>• Real-time tracking and analytics</li>
|
||||
</ul>
|
||||
</div> */}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => setStep("email")}>
|
||||
Get Started
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === "email" && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Plus className="h-6 w-6 text-primary" />
|
||||
Add Additional Email (Optional)
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
Would you like to add an additional email address to process debt
|
||||
emails from multiple accounts?
|
||||
</DialogDescription>
|
||||
<DialogDescription>
|
||||
You can attach here the Postmark inbound email address for the
|
||||
additional email.{" "}
|
||||
<a
|
||||
className="text-blue-600 dark:text-blue-500 hover:underline"
|
||||
href="https://postmarkapp.com/blog/an-introduction-to-inbound-email-parsing-what-it-is-and-how-you-can-do-it#how-to-use-postmark-for-email-parsing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Postmark Inbound Email Parsing Guide
|
||||
</a>
|
||||
</DialogDescription>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="additional-email">
|
||||
Additional Email Address
|
||||
</Label>
|
||||
<Input
|
||||
id="additional-email"
|
||||
type="email"
|
||||
placeholder="additional@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter" && email) {
|
||||
handleAddEmail();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
You can always add more email addresses later in the
|
||||
configuration page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSkipEmail}
|
||||
disabled={loading}
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddEmail}
|
||||
disabled={!email || loading}
|
||||
className="min-w-[100px]"
|
||||
>
|
||||
{loading ? "Adding..." : "Add Email"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === "complete" && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||
Setup Complete!
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Your account is now ready to process debt emails with AI
|
||||
assistance.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<Mail className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Start by forwarding debt collection emails to your configured
|
||||
addresses. Our AI will automatically extract debt information
|
||||
and begin the negotiation process.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Next steps:</h4>
|
||||
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||
<li>• Forward debt emails to your monitored addresses</li>
|
||||
<li>• Monitor negotiations in your dashboard</li>
|
||||
<li>• Review AI-generated settlement offers</li>
|
||||
<li>• Track your savings and progress</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleComplete}>
|
||||
Go to Dashboard
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user