Skip to content

Commit 4778b51

Browse files
author
Rajat
committed
Untested: product manage screen broken in multiple components; demo cert generation logic on backend
1 parent 9edbd47 commit 4778b51

9 files changed

Lines changed: 1589 additions & 1350 deletions

File tree

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/components/certificates.tsx

Lines changed: 703 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Label } from "@/components/ui/label";
5+
import { Switch } from "@/components/ui/switch";
6+
import { Separator } from "@/components/ui/separator";
7+
import { useToast } from "@courselit/components-library";
8+
import { Constants } from "@courselit/common-models";
9+
import { COURSE_TYPE_DOWNLOAD } from "@ui-config/constants";
10+
import {
11+
APP_MESSAGE_COURSE_SAVED,
12+
TOAST_TITLE_ERROR,
13+
TOAST_TITLE_SUCCESS,
14+
} from "@ui-config/strings";
15+
import { useGraphQLFetch } from "@/hooks/use-graphql-fetch";
16+
import { AlertCircle } from "lucide-react";
17+
18+
const { PaymentPlanType: paymentPlanType } = Constants;
19+
20+
const MUTATION_UPDATE_LEAD_MAGNET = `
21+
mutation UpdateLeadMagnet($courseId: String!, $leadMagnet: Boolean!) {
22+
updateCourse(courseData: { id: $courseId, leadMagnet: $leadMagnet }) {
23+
courseId
24+
}
25+
}
26+
`;
27+
28+
interface DownloadOptionsProps {
29+
product: any;
30+
paymentPlans: any[];
31+
}
32+
33+
export default function DownloadOptions({
34+
product,
35+
paymentPlans,
36+
}: DownloadOptionsProps) {
37+
const { toast } = useToast();
38+
const fetch = useGraphQLFetch();
39+
const [loading, setLoading] = useState(false);
40+
const [leadMagnet, setLeadMagnet] = useState(product?.leadMagnet || false);
41+
42+
const handleSwitchChange = async () => {
43+
const newValue = !leadMagnet;
44+
const previousValue = leadMagnet;
45+
setLeadMagnet(newValue);
46+
47+
if (!product?.courseId) return;
48+
49+
try {
50+
setLoading(true);
51+
const response = await fetch
52+
.setPayload({
53+
query: MUTATION_UPDATE_LEAD_MAGNET,
54+
variables: {
55+
courseId: product.courseId,
56+
leadMagnet: newValue,
57+
},
58+
})
59+
.build()
60+
.exec();
61+
62+
if (response?.updateCourse) {
63+
toast({
64+
title: TOAST_TITLE_SUCCESS,
65+
description: APP_MESSAGE_COURSE_SAVED,
66+
});
67+
}
68+
} catch (err: any) {
69+
// Revert to previous state on error
70+
setLeadMagnet(previousValue);
71+
toast({
72+
title: TOAST_TITLE_ERROR,
73+
description: err.message,
74+
variant: "destructive",
75+
});
76+
} finally {
77+
setLoading(false);
78+
}
79+
};
80+
81+
// Only show for download type products
82+
if (product?.type?.toLowerCase() !== COURSE_TYPE_DOWNLOAD) {
83+
return null;
84+
}
85+
86+
const hasExactlyOneFreePlan =
87+
paymentPlans.length === 1 &&
88+
paymentPlans.some((plan) => plan.type === paymentPlanType.FREE);
89+
const isSwitchDisabled = loading || !hasExactlyOneFreePlan;
90+
91+
return (
92+
<div className="space-y-8">
93+
<div className="flex items-center justify-between">
94+
<div className="space-y-0.5">
95+
<Label
96+
className={`${!hasExactlyOneFreePlan ? "text-muted-foreground" : ""} text-base font-semibold`}
97+
>
98+
Lead Magnet
99+
</Label>
100+
<p className="text-sm text-muted-foreground">
101+
Send the product to user for free in exchange of their
102+
email address
103+
</p>
104+
{!hasExactlyOneFreePlan && (
105+
<div className="flex items-start gap-2 text-red-600">
106+
<AlertCircle className="h-4 w-4 mt-0.5 text-red-600" />
107+
<p className="text-xs leading-5">
108+
Product must have exactly one free payment plan
109+
to enable lead magnet
110+
</p>
111+
</div>
112+
)}
113+
</div>
114+
<div>
115+
<Switch
116+
checked={leadMagnet}
117+
disabled={isSwitchDisabled}
118+
onCheckedChange={handleSwitchChange}
119+
/>
120+
</div>
121+
</div>
122+
<Separator />
123+
</div>
124+
);
125+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use client";
2+
3+
import { Label } from "@/components/ui/label";
4+
import { Separator } from "@/components/ui/separator";
5+
import { useToast } from "@courselit/components-library";
6+
import { PaymentPlanType, Constants } from "@courselit/common-models";
7+
import PaymentPlanList from "@components/admin/payments/payment-plan-list";
8+
import { TOAST_TITLE_ERROR } from "@ui-config/strings";
9+
10+
const { MembershipEntityType } = Constants;
11+
12+
interface PaymentPlansProps {
13+
productId: string;
14+
paymentPlans: any[];
15+
setPaymentPlans: (plans: any[]) => void;
16+
defaultPaymentPlan: string;
17+
setDefaultPaymentPlan: (planId: string) => void;
18+
onPlanArchived: (planId: string) => Promise<any>;
19+
onDefaultPlanChanged: (planId: string) => Promise<any>;
20+
loading: boolean;
21+
}
22+
23+
export default function PaymentPlans({
24+
productId,
25+
paymentPlans,
26+
setPaymentPlans,
27+
defaultPaymentPlan,
28+
setDefaultPaymentPlan,
29+
onPlanArchived,
30+
onDefaultPlanChanged,
31+
loading,
32+
}: PaymentPlansProps) {
33+
const { toast } = useToast();
34+
35+
return (
36+
<div className="space-y-8">
37+
<div className="space-y-4 flex flex-col md:flex-row md:items-start md:justify-between w-full">
38+
<div className="space-y-2">
39+
<Label className="text-base font-semibold">Pricing</Label>
40+
<p className="text-sm text-muted-foreground">
41+
Manage your product&apos;s pricing plans
42+
</p>
43+
</div>
44+
<PaymentPlanList
45+
paymentPlans={paymentPlans.map((plan) => ({
46+
...plan,
47+
type: plan.type.toLowerCase() as PaymentPlanType,
48+
}))}
49+
onPlanArchived={async (id) => {
50+
try {
51+
await onPlanArchived(id);
52+
} catch (err: any) {
53+
toast({
54+
title: TOAST_TITLE_ERROR,
55+
description: err.message,
56+
variant: "destructive",
57+
});
58+
}
59+
}}
60+
onDefaultPlanChanged={async (id) => {
61+
try {
62+
await onDefaultPlanChanged(id);
63+
} catch (err: any) {
64+
toast({
65+
title: TOAST_TITLE_ERROR,
66+
description: err.message,
67+
});
68+
}
69+
}}
70+
defaultPaymentPlanId={defaultPaymentPlan}
71+
entityId={productId}
72+
entityType={"product"}
73+
disabled={loading}
74+
/>
75+
</div>
76+
<Separator />
77+
</div>
78+
);
79+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"use client";
2+
3+
import { useContext, useState } from "react";
4+
import { Button } from "@/components/ui/button";
5+
import { Input } from "@/components/ui/input";
6+
import { Label } from "@/components/ui/label";
7+
import { Trash2, Loader2 } from "lucide-react";
8+
import {
9+
AlertDialog,
10+
AlertDialogAction,
11+
AlertDialogCancel,
12+
AlertDialogContent,
13+
AlertDialogDescription,
14+
AlertDialogFooter,
15+
AlertDialogHeader,
16+
AlertDialogTitle,
17+
AlertDialogTrigger,
18+
} from "@/components/ui/alert-dialog";
19+
import { useToast } from "@courselit/components-library";
20+
import { FetchBuilder } from "@courselit/utils";
21+
import { AddressContext } from "@components/contexts";
22+
import {
23+
APP_MESSAGE_COURSE_DELETED,
24+
BTN_DELETE_COURSE,
25+
DANGER_ZONE_HEADER,
26+
TOAST_TITLE_ERROR,
27+
TOAST_TITLE_SUCCESS,
28+
} from "@ui-config/strings";
29+
import { useRouter } from "next/navigation";
30+
31+
interface ProductDeletionProps {
32+
product: any;
33+
}
34+
35+
export default function ProductDeletion({ product }: ProductDeletionProps) {
36+
const { toast } = useToast();
37+
const router = useRouter();
38+
const address = useContext(AddressContext);
39+
const [deleteConfirmation, setDeleteConfirmation] = useState("");
40+
const [isDeleting, setIsDeleting] = useState(false);
41+
42+
const deleteProduct = async () => {
43+
if (!product) return;
44+
45+
const query = `
46+
mutation {
47+
result: deleteCourse(id: "${product?.courseId}")
48+
}
49+
`;
50+
51+
const fetch = new FetchBuilder()
52+
.setUrl(`${address.backend}/api/graph`)
53+
.setPayload(query)
54+
.setIsGraphQLEndpoint(true)
55+
.build();
56+
57+
try {
58+
setIsDeleting(true);
59+
const response = await fetch.exec();
60+
61+
if (response.result) {
62+
toast({
63+
title: TOAST_TITLE_SUCCESS,
64+
description: APP_MESSAGE_COURSE_DELETED,
65+
});
66+
router.push("/dashboard/products");
67+
}
68+
} catch (err: any) {
69+
toast({
70+
title: TOAST_TITLE_ERROR,
71+
description: err.message,
72+
variant: "destructive",
73+
});
74+
} finally {
75+
setIsDeleting(false);
76+
}
77+
};
78+
79+
return (
80+
<div className="space-y-8">
81+
<div className="space-y-4">
82+
<h2 className="text-destructive font-semibold">
83+
{DANGER_ZONE_HEADER}
84+
</h2>
85+
<AlertDialog
86+
onOpenChange={(open) =>
87+
!open &&
88+
(setDeleteConfirmation(""), setIsDeleting(false))
89+
}
90+
>
91+
<AlertDialogTrigger asChild>
92+
<Button
93+
type="button"
94+
variant="destructive"
95+
disabled={isDeleting}
96+
>
97+
<Trash2 className="mr-2 h-4 w-4" />
98+
{BTN_DELETE_COURSE}
99+
</Button>
100+
</AlertDialogTrigger>
101+
<AlertDialogContent>
102+
<AlertDialogHeader>
103+
<AlertDialogTitle>
104+
Are you absolutely sure?
105+
</AlertDialogTitle>
106+
<AlertDialogDescription>
107+
This action is irreversible. All product data
108+
will be permanently deleted.
109+
</AlertDialogDescription>
110+
</AlertDialogHeader>
111+
<div className="py-4">
112+
<Label
113+
htmlFor="delete-confirmation"
114+
className="text-sm font-medium"
115+
>
116+
Type &quot;delete&quot; to confirm
117+
</Label>
118+
<Input
119+
id="delete-confirmation"
120+
type="text"
121+
placeholder="Type 'delete' to confirm"
122+
value={deleteConfirmation}
123+
onChange={(e) =>
124+
setDeleteConfirmation(e.target.value)
125+
}
126+
className="mt-2"
127+
/>
128+
</div>
129+
<AlertDialogFooter>
130+
<AlertDialogCancel>Cancel</AlertDialogCancel>
131+
<AlertDialogAction
132+
onClick={deleteProduct}
133+
disabled={
134+
deleteConfirmation !== "delete" ||
135+
isDeleting
136+
}
137+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 disabled:opacity-50 disabled:cursor-not-allowed"
138+
>
139+
{isDeleting ? (
140+
<>
141+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
142+
Deleting...
143+
</>
144+
) : (
145+
"Delete"
146+
)}
147+
</AlertDialogAction>
148+
</AlertDialogFooter>
149+
</AlertDialogContent>
150+
</AlertDialog>
151+
</div>
152+
</div>
153+
);
154+
}

0 commit comments

Comments
 (0)