mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
feat: move More Pages to dialog
- Introduced `MorePagesDialog` and `MorePagesContent` components to manage and display tasks for earning additional pages. - Integrated dialog state management using `morePagesDialogAtom`. - Updated `LayoutDataProvider` to include the new dialog and handle task completion logic. - Enhanced `PageUsageDisplay` to trigger the More Pages dialog, improving user interaction for page management.
This commit is contained in:
parent
993c8539e8
commit
2ab00c1746
8 changed files with 259 additions and 222 deletions
|
|
@ -1,80 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { IconCalendar, IconMailFilled } from "@tabler/icons-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, ExternalLink, Gift, Mail, Star, Zap } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import type { IncentiveTaskInfo } from "@/contracts/types/incentive-tasks.types";
|
||||
import { incentiveTasksApiService } from "@/lib/apis/incentive-tasks-api.service";
|
||||
import {
|
||||
trackIncentiveContactOpened,
|
||||
trackIncentivePageViewed,
|
||||
trackIncentiveTaskClicked,
|
||||
trackIncentiveTaskCompleted,
|
||||
} from "@/lib/posthog/events";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MorePagesContent } from "@/components/settings/more-pages-content";
|
||||
|
||||
export default function MorePagesPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
trackIncentivePageViewed();
|
||||
}, []);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["incentive-tasks"],
|
||||
queryFn: () => incentiveTasksApiService.getTasks(),
|
||||
});
|
||||
|
||||
const completeMutation = useMutation({
|
||||
mutationFn: incentiveTasksApiService.completeTask,
|
||||
onSuccess: (response, taskType) => {
|
||||
if (response.success) {
|
||||
toast.success(response.message);
|
||||
const task = data?.tasks.find((t) => t.task_type === taskType);
|
||||
if (task) {
|
||||
trackIncentiveTaskCompleted(taskType, task.pages_reward);
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["incentive-tasks"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to complete task. Please try again.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleTaskClick = (task: IncentiveTaskInfo) => {
|
||||
if (!task.completed) {
|
||||
trackIncentiveTaskClicked(task.task_type);
|
||||
completeMutation.mutate(task.task_type);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-64px)] select-none items-center justify-center px-4 py-8">
|
||||
<motion.div
|
||||
|
|
@ -83,140 +12,7 @@ export default function MorePagesPage() {
|
|||
transition={{ duration: 0.3 }}
|
||||
className="w-full max-w-md space-y-6"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<Gift className="mx-auto mb-3 h-8 w-8 text-primary" />
|
||||
<h2 className="text-xl font-bold tracking-tight">Get More Pages</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Complete tasks to earn additional pages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tasks */}
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-3">
|
||||
<Skeleton className="h-9 w-9 rounded-full" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/4" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{data?.tasks.map((task) => (
|
||||
<Card
|
||||
key={task.task_type}
|
||||
className={cn("transition-colors", task.completed && "bg-muted/50")}
|
||||
>
|
||||
<CardContent className="flex items-center gap-3 p-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
|
||||
task.completed ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||
)}
|
||||
>
|
||||
{task.completed ? <Check className="h-4 w-4" /> : <Star className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
task.completed && "text-muted-foreground line-through"
|
||||
)}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={task.completed ? "ghost" : "outline"}
|
||||
size="sm"
|
||||
disabled={task.completed || completeMutation.isPending}
|
||||
onClick={() => handleTaskClick(task)}
|
||||
asChild={!task.completed}
|
||||
>
|
||||
{task.completed ? (
|
||||
<span>Done</span>
|
||||
) : (
|
||||
<a
|
||||
href={task.action_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="gap-1"
|
||||
>
|
||||
{completeMutation.isPending ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<>
|
||||
Go
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PRO Upgrade */}
|
||||
<Separator />
|
||||
|
||||
<Card className="overflow-hidden border-emerald-500/20">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-emerald-500" />
|
||||
<CardTitle className="text-base">Upgrade to PRO</CardTitle>
|
||||
<Badge className="bg-emerald-600 text-white border-transparent hover:bg-emerald-600">
|
||||
FREE
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
For a limited time, get{" "}
|
||||
<span className="font-semibold text-foreground">6,000 additional pages</span> at no
|
||||
cost. Contact us and we'll upgrade your account instantly.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-2">
|
||||
<Dialog onOpenChange={(open) => open && trackIncentiveContactOpened()}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full bg-emerald-600 text-white hover:bg-emerald-700">
|
||||
<Mail className="h-4 w-4" />
|
||||
Contact Us to Upgrade
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="select-none sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Get in Touch</DialogTitle>
|
||||
<DialogDescription>Pick the option that works best for you.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button asChild>
|
||||
<Link
|
||||
href="https://cal.com/mod-rohan"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconCalendar className="h-4 w-4" />
|
||||
Schedule a Meeting
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="mailto:rohan@surfsense.com">
|
||||
<IconMailFilled className="h-4 w-4" />
|
||||
rohan@surfsense.com
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<MorePagesContent />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ export const userSettingsDialogAtom = atom<UserSettingsDialogState>({
|
|||
});
|
||||
|
||||
export const teamDialogAtom = atom<boolean>(false);
|
||||
|
||||
export const morePagesDialogAtom = atom<boolean>(false);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
|
|||
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import {
|
||||
morePagesDialogAtom,
|
||||
searchSpaceSettingsDialogAtom,
|
||||
teamDialogAtom,
|
||||
userSettingsDialogAtom,
|
||||
|
|
@ -40,7 +41,7 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { isPageLimitExceededMetadata } from "@/contracts/types/inbox.types";
|
||||
|
||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
||||
import { useInbox } from "@/hooks/use-inbox";
|
||||
|
|
@ -52,6 +53,7 @@ import { deleteThread, fetchThreads, updateThread } from "@/lib/chat/thread-pers
|
|||
import { cleanupElectric } from "@/lib/electric/client";
|
||||
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
|
||||
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
||||
import { TeamDialog } from "@/components/settings/team-dialog";
|
||||
import { UserSettingsDialog } from "@/components/settings/user-settings-dialog";
|
||||
|
|
@ -201,6 +203,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
const seenPageLimitNotifications = useRef<Set<number>>(new Set());
|
||||
const isInitialLoad = useRef(true);
|
||||
|
||||
const setMorePagesOpen = useSetAtom(morePagesDialogAtom);
|
||||
|
||||
// Effect to show toast for new page_limit_exceeded notifications
|
||||
useEffect(() => {
|
||||
if (statusInbox.loading) return;
|
||||
|
|
@ -224,21 +228,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
for (const notification of newNotifications) {
|
||||
seenPageLimitNotifications.current.add(notification.id);
|
||||
|
||||
const actionUrl = isPageLimitExceededMetadata(notification.metadata)
|
||||
? notification.metadata.action_url
|
||||
: `/dashboard/${searchSpaceId}/more-pages`;
|
||||
|
||||
toast.error(notification.title, {
|
||||
description: notification.message,
|
||||
duration: 8000,
|
||||
icon: <AlertTriangle className="h-5 w-5 text-amber-500" />,
|
||||
action: {
|
||||
label: "View Plans",
|
||||
onClick: () => router.push(actionUrl),
|
||||
onClick: () => setMorePagesOpen(true),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [statusInbox.inboxItems, statusInbox.loading, searchSpaceId, router]);
|
||||
}, [statusInbox.inboxItems, statusInbox.loading, searchSpaceId, setMorePagesOpen]);
|
||||
|
||||
// Delete dialogs state
|
||||
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
|
||||
|
|
@ -951,6 +951,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
|||
<SearchSpaceSettingsDialog searchSpaceId={Number(searchSpaceId)} />
|
||||
<UserSettingsDialog />
|
||||
<TeamDialog searchSpaceId={Number(searchSpaceId)} />
|
||||
<MorePagesDialog />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -362,6 +362,9 @@ export function LayoutShell({
|
|||
<div
|
||||
role="slider"
|
||||
aria-label="Resize sidebar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={50}
|
||||
tabIndex={0}
|
||||
onMouseDown={onResizeMouseDown}
|
||||
className="hidden md:block h-full cursor-col-resize z-30"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { useSetAtom } from "jotai";
|
||||
import { Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { morePagesDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
|
|
@ -12,8 +12,7 @@ interface PageUsageDisplayProps {
|
|||
}
|
||||
|
||||
export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProps) {
|
||||
const params = useParams();
|
||||
const searchSpaceId = params.search_space_id;
|
||||
const setMorePagesOpen = useSetAtom(morePagesDialogAtom);
|
||||
const usagePercentage = (pagesUsed / pagesLimit) * 100;
|
||||
|
||||
return (
|
||||
|
|
@ -26,9 +25,10 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
<span className="font-medium">{usagePercentage.toFixed(0)}%</span>
|
||||
</div>
|
||||
<Progress value={usagePercentage} className="h-1.5" />
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/more-pages`}
|
||||
className="group flex items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMorePagesOpen(true)}
|
||||
className="group flex w-full items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground group-hover:text-accent-foreground">
|
||||
<Zap className="h-3 w-3 shrink-0" />
|
||||
|
|
@ -37,7 +37,7 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
|||
<Badge className="h-4 rounded px-1 text-[10px] font-semibold leading-none bg-emerald-600 text-white border-transparent hover:bg-emerald-600">
|
||||
FREE
|
||||
</Badge>
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ interface SidebarProps {
|
|||
isLoadingChats?: boolean;
|
||||
disableTooltips?: boolean;
|
||||
sidebarWidth?: number;
|
||||
onResizeMouseDown?: (e: React.MouseEvent) => void;
|
||||
isResizing?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
212
surfsense_web/components/settings/more-pages-content.tsx
Normal file
212
surfsense_web/components/settings/more-pages-content.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"use client";
|
||||
|
||||
import { IconCalendar, IconMailFilled } from "@tabler/icons-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, ExternalLink, Gift, Mail, Star, Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import type { IncentiveTaskInfo } from "@/contracts/types/incentive-tasks.types";
|
||||
import { incentiveTasksApiService } from "@/lib/apis/incentive-tasks-api.service";
|
||||
import {
|
||||
trackIncentiveContactOpened,
|
||||
trackIncentivePageViewed,
|
||||
trackIncentiveTaskClicked,
|
||||
trackIncentiveTaskCompleted,
|
||||
} from "@/lib/posthog/events";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function MorePagesContent() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
trackIncentivePageViewed();
|
||||
}, []);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["incentive-tasks"],
|
||||
queryFn: () => incentiveTasksApiService.getTasks(),
|
||||
});
|
||||
|
||||
const completeMutation = useMutation({
|
||||
mutationFn: incentiveTasksApiService.completeTask,
|
||||
onSuccess: (response, taskType) => {
|
||||
if (response.success) {
|
||||
toast.success(response.message);
|
||||
const task = data?.tasks.find((t) => t.task_type === taskType);
|
||||
if (task) {
|
||||
trackIncentiveTaskCompleted(taskType, task.pages_reward);
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["incentive-tasks"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to complete task. Please try again.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleTaskClick = (task: IncentiveTaskInfo) => {
|
||||
if (!task.completed) {
|
||||
trackIncentiveTaskClicked(task.task_type);
|
||||
completeMutation.mutate(task.task_type);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-6">
|
||||
<div className="text-center">
|
||||
<Gift className="mx-auto mb-3 h-8 w-8 text-primary" />
|
||||
<h2 className="text-xl font-bold tracking-tight">Get More Pages</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Complete tasks to earn additional pages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-3 p-3">
|
||||
<Skeleton className="h-9 w-9 rounded-full" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/4" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{data?.tasks.map((task) => (
|
||||
<Card
|
||||
key={task.task_type}
|
||||
className={cn("transition-colors", task.completed && "bg-muted/50")}
|
||||
>
|
||||
<CardContent className="flex items-center gap-3 p-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
|
||||
task.completed ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||
)}
|
||||
>
|
||||
{task.completed ? <Check className="h-4 w-4" /> : <Star className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium",
|
||||
task.completed && "text-muted-foreground line-through"
|
||||
)}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={task.completed ? "ghost" : "outline"}
|
||||
size="sm"
|
||||
disabled={task.completed || completeMutation.isPending}
|
||||
onClick={() => handleTaskClick(task)}
|
||||
asChild={!task.completed}
|
||||
>
|
||||
{task.completed ? (
|
||||
<span>Done</span>
|
||||
) : (
|
||||
<a
|
||||
href={task.action_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="gap-1"
|
||||
>
|
||||
{completeMutation.isPending ? (
|
||||
<Spinner size="xs" />
|
||||
) : (
|
||||
<>
|
||||
Go
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<Card className="overflow-hidden border-emerald-500/20 bg-transparent">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-emerald-500" />
|
||||
<CardTitle className="text-base">Upgrade to PRO</CardTitle>
|
||||
<Badge className="bg-emerald-600 text-white border-transparent hover:bg-emerald-600">
|
||||
FREE
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>
|
||||
For a limited time, get{" "}
|
||||
<span className="font-semibold text-foreground">6,000 additional pages</span> at no
|
||||
cost. Contact us and we'll upgrade your account instantly.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-2">
|
||||
<Dialog onOpenChange={(open) => open && trackIncentiveContactOpened()}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full bg-emerald-600 text-white hover:bg-emerald-700">
|
||||
<Mail className="h-4 w-4" />
|
||||
Contact Us to Upgrade
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="select-none sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Get in Touch</DialogTitle>
|
||||
<DialogDescription>Pick the option that works best for you.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button asChild>
|
||||
<Link
|
||||
href="https://cal.com/mod-rohan"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconCalendar className="h-4 w-4" />
|
||||
Schedule a Meeting
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="mailto:rohan@surfsense.com">
|
||||
<IconMailFilled className="h-4 w-4" />
|
||||
rohan@surfsense.com
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
surfsense_web/components/settings/more-pages-dialog.tsx
Normal file
24
surfsense_web/components/settings/more-pages-dialog.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import { useAtom } from "jotai";
|
||||
import { morePagesDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { MorePagesContent } from "./more-pages-content";
|
||||
|
||||
export function MorePagesDialog() {
|
||||
const [open, setOpen] = useAtom(morePagesDialogAtom);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
className="select-none max-w-md w-[95vw] max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogTitle className="sr-only">Get More Pages</DialogTitle>
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<MorePagesContent />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue