mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +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";
|
"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 { motion } from "motion/react";
|
||||||
import Link from "next/link";
|
import { MorePagesContent } from "@/components/settings/more-pages-content";
|
||||||
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 default function MorePagesPage() {
|
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 (
|
return (
|
||||||
<div className="flex min-h-[calc(100vh-64px)] select-none items-center justify-center px-4 py-8">
|
<div className="flex min-h-[calc(100vh-64px)] select-none items-center justify-center px-4 py-8">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -83,140 +12,7 @@ export default function MorePagesPage() {
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="w-full max-w-md space-y-6"
|
className="w-full max-w-md space-y-6"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<MorePagesContent />
|
||||||
<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>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,5 @@ export const userSettingsDialogAtom = atom<UserSettingsDialogState>({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const teamDialogAtom = atom<boolean>(false);
|
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 { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||||
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||||
import {
|
import {
|
||||||
|
morePagesDialogAtom,
|
||||||
searchSpaceSettingsDialogAtom,
|
searchSpaceSettingsDialogAtom,
|
||||||
teamDialogAtom,
|
teamDialogAtom,
|
||||||
userSettingsDialogAtom,
|
userSettingsDialogAtom,
|
||||||
|
|
@ -40,7 +41,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { isPageLimitExceededMetadata } from "@/contracts/types/inbox.types";
|
|
||||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||||
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
import { useDocumentsProcessing } from "@/hooks/use-documents-processing";
|
||||||
import { useInbox } from "@/hooks/use-inbox";
|
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 { cleanupElectric } from "@/lib/electric/client";
|
||||||
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
import { resetUser, trackLogout } from "@/lib/posthog/events";
|
||||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
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 { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
||||||
import { TeamDialog } from "@/components/settings/team-dialog";
|
import { TeamDialog } from "@/components/settings/team-dialog";
|
||||||
import { UserSettingsDialog } from "@/components/settings/user-settings-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 seenPageLimitNotifications = useRef<Set<number>>(new Set());
|
||||||
const isInitialLoad = useRef(true);
|
const isInitialLoad = useRef(true);
|
||||||
|
|
||||||
|
const setMorePagesOpen = useSetAtom(morePagesDialogAtom);
|
||||||
|
|
||||||
// Effect to show toast for new page_limit_exceeded notifications
|
// Effect to show toast for new page_limit_exceeded notifications
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusInbox.loading) return;
|
if (statusInbox.loading) return;
|
||||||
|
|
@ -224,21 +228,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
for (const notification of newNotifications) {
|
for (const notification of newNotifications) {
|
||||||
seenPageLimitNotifications.current.add(notification.id);
|
seenPageLimitNotifications.current.add(notification.id);
|
||||||
|
|
||||||
const actionUrl = isPageLimitExceededMetadata(notification.metadata)
|
|
||||||
? notification.metadata.action_url
|
|
||||||
: `/dashboard/${searchSpaceId}/more-pages`;
|
|
||||||
|
|
||||||
toast.error(notification.title, {
|
toast.error(notification.title, {
|
||||||
description: notification.message,
|
description: notification.message,
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
icon: <AlertTriangle className="h-5 w-5 text-amber-500" />,
|
icon: <AlertTriangle className="h-5 w-5 text-amber-500" />,
|
||||||
action: {
|
action: {
|
||||||
label: "View Plans",
|
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
|
// Delete dialogs state
|
||||||
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
|
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
|
||||||
|
|
@ -951,6 +951,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
<SearchSpaceSettingsDialog searchSpaceId={Number(searchSpaceId)} />
|
<SearchSpaceSettingsDialog searchSpaceId={Number(searchSpaceId)} />
|
||||||
<UserSettingsDialog />
|
<UserSettingsDialog />
|
||||||
<TeamDialog searchSpaceId={Number(searchSpaceId)} />
|
<TeamDialog searchSpaceId={Number(searchSpaceId)} />
|
||||||
|
<MorePagesDialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -362,6 +362,9 @@ export function LayoutShell({
|
||||||
<div
|
<div
|
||||||
role="slider"
|
role="slider"
|
||||||
aria-label="Resize sidebar"
|
aria-label="Resize sidebar"
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-valuenow={50}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseDown={onResizeMouseDown}
|
onMouseDown={onResizeMouseDown}
|
||||||
className="hidden md:block h-full cursor-col-resize z-30"
|
className="hidden md:block h-full cursor-col-resize z-30"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
import { Zap } from "lucide-react";
|
import { Zap } from "lucide-react";
|
||||||
import Link from "next/link";
|
import { morePagesDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
|
||||||
|
|
@ -12,8 +12,7 @@ interface PageUsageDisplayProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProps) {
|
export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProps) {
|
||||||
const params = useParams();
|
const setMorePagesOpen = useSetAtom(morePagesDialogAtom);
|
||||||
const searchSpaceId = params.search_space_id;
|
|
||||||
const usagePercentage = (pagesUsed / pagesLimit) * 100;
|
const usagePercentage = (pagesUsed / pagesLimit) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -26,9 +25,10 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
|
||||||
<span className="font-medium">{usagePercentage.toFixed(0)}%</span>
|
<span className="font-medium">{usagePercentage.toFixed(0)}%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={usagePercentage} className="h-1.5" />
|
<Progress value={usagePercentage} className="h-1.5" />
|
||||||
<Link
|
<button
|
||||||
href={`/dashboard/${searchSpaceId}/more-pages`}
|
type="button"
|
||||||
className="group flex items-center justify-between rounded-md px-1.5 py-1 -mx-1.5 transition-colors hover:bg-accent"
|
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">
|
<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" />
|
<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">
|
<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
|
FREE
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ interface SidebarProps {
|
||||||
isLoadingChats?: boolean;
|
isLoadingChats?: boolean;
|
||||||
disableTooltips?: boolean;
|
disableTooltips?: boolean;
|
||||||
sidebarWidth?: number;
|
sidebarWidth?: number;
|
||||||
onResizeMouseDown?: (e: React.MouseEvent) => void;
|
|
||||||
isResizing?: boolean;
|
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