diff --git a/apps/api/prisma/migrations/20251114063002_add_completed_steps/migration.sql b/apps/api/prisma/migrations/20251114063002_add_completed_steps/migration.sql
new file mode 100644
index 0000000..965fcd6
--- /dev/null
+++ b/apps/api/prisma/migrations/20251114063002_add_completed_steps/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "completedSteps" JSONB;
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 229f624..2bf9695 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -32,15 +32,16 @@ enum SubscriptionStatus {
}
model User {
- id String @id @default(cuid())
- email String @unique
- firstName String
- authMethod String
- createdAt DateTime @default(now())
- lastLogin DateTime @updatedAt
- accounts Account[]
- payments Payment[]
- subscriptions Subscription[]
+ id String @id @default(cuid())
+ email String @unique
+ firstName String
+ authMethod String
+ createdAt DateTime @default(now())
+ lastLogin DateTime @updatedAt
+ completedSteps Json?
+ accounts Account[]
+ payments Payment[]
+ subscriptions Subscription[]
}
model Account {
diff --git a/apps/api/src/routers/user.ts b/apps/api/src/routers/user.ts
index 2618fa3..94205d0 100644
--- a/apps/api/src/routers/user.ts
+++ b/apps/api/src/routers/user.ts
@@ -1,5 +1,6 @@
import { router, publicProcedure, protectedProcedure } from "../trpc.js";
import { userService } from "../services/user.service.js";
+import { z } from "zod";
export const userRouter = router({
// get the total count of users
@@ -12,4 +13,26 @@ export const userRouter = router({
const userId = ctx.user.id;
return await userService.checkSubscriptionStatus(ctx.db.prisma, userId);
}),
+
+ // get user's completed steps
+ getCompletedSteps: protectedProcedure.query(async ({ ctx }: any) => {
+ const userId = ctx.user.id;
+ return await userService.getCompletedSteps(ctx.db.prisma, userId);
+ }),
+
+ // update user's completed steps
+ updateCompletedSteps: protectedProcedure
+ .input(
+ z.object({
+ completedSteps: z.array(z.string()),
+ })
+ )
+ .mutation(async ({ ctx, input }: any) => {
+ const userId = ctx.user.id;
+ return await userService.updateCompletedSteps(
+ ctx.db.prisma,
+ userId,
+ input.completedSteps
+ );
+ }),
});
diff --git a/apps/api/src/services/user.service.ts b/apps/api/src/services/user.service.ts
index 3bd794f..26ac225 100644
--- a/apps/api/src/services/user.service.ts
+++ b/apps/api/src/services/user.service.ts
@@ -47,4 +47,43 @@ export const userService = {
: null,
};
},
+
+ /**
+ * Get user's completed steps
+ */
+ async getCompletedSteps(
+ prisma: ExtendedPrismaClient | PrismaClient,
+ userId: string
+ ) {
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: { completedSteps: true },
+ });
+
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ const completedSteps = user.completedSteps as string[] | null;
+ return completedSteps || [];
+ },
+
+ /**
+ * Update user's completed steps
+ */
+ async updateCompletedSteps(
+ prisma: ExtendedPrismaClient | PrismaClient,
+ userId: string,
+ completedSteps: string[]
+ ) {
+ const user = await prisma.user.update({
+ where: { id: userId },
+ data: {
+ completedSteps: completedSteps,
+ },
+ select: { completedSteps: true },
+ });
+
+ return (user.completedSteps as string[]) || [];
+ },
};
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index fdebc20..3819a18 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -6,6 +6,10 @@ const nextConfig = {
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
+ {
+ protocol: "https",
+ hostname: "lh3.googleusercontent.com",
+ },
],
},
};
diff --git a/apps/web/package.json b/apps/web/package.json
index 7848f7c..77494fc 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -24,6 +24,7 @@
"@vercel/speed-insights": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "dompurify": "^3.3.0",
"framer-motion": "^11.15.0",
"geist": "^1.5.1",
"lucide-react": "^0.456.0",
@@ -41,6 +42,7 @@
"zustand": "^5.0.1"
},
"devDependencies": {
+ "@types/dompurify": "^3.2.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
diff --git a/apps/web/public/images/dm.webp b/apps/web/public/images/dm.webp
new file mode 100644
index 0000000..1469589
Binary files /dev/null and b/apps/web/public/images/dm.webp differ
diff --git a/apps/web/public/images/doc.webp b/apps/web/public/images/doc.webp
new file mode 100644
index 0000000..3959926
Binary files /dev/null and b/apps/web/public/images/doc.webp differ
diff --git a/apps/web/public/images/lv-1.webp b/apps/web/public/images/lv-1.webp
new file mode 100644
index 0000000..951f3a2
Binary files /dev/null and b/apps/web/public/images/lv-1.webp differ
diff --git a/apps/web/public/images/module-1.webp b/apps/web/public/images/module-1.webp
new file mode 100644
index 0000000..60e7cd6
Binary files /dev/null and b/apps/web/public/images/module-1.webp differ
diff --git a/apps/web/public/images/ptm.webp b/apps/web/public/images/ptm.webp
new file mode 100644
index 0000000..f5eeb86
Binary files /dev/null and b/apps/web/public/images/ptm.webp differ
diff --git a/apps/web/public/images/sheet-1.webp b/apps/web/public/images/sheet-1.webp
new file mode 100644
index 0000000..0cff34b
Binary files /dev/null and b/apps/web/public/images/sheet-1.webp differ
diff --git a/apps/web/public/images/sheet-2.webp b/apps/web/public/images/sheet-2.webp
new file mode 100644
index 0000000..fa18498
Binary files /dev/null and b/apps/web/public/images/sheet-2.webp differ
diff --git a/apps/web/src/app/(main)/(landing)/pricing/page.tsx b/apps/web/src/app/(main)/(landing)/pricing/page.tsx
index c5505be..2823888 100644
--- a/apps/web/src/app/(main)/(landing)/pricing/page.tsx
+++ b/apps/web/src/app/(main)/(landing)/pricing/page.tsx
@@ -61,7 +61,7 @@ const opensoxFeatures = [
const whySub = [
{
content:
- "Currently, Opensox 2.0 is in progress (70% done) so till the launch, we are offering premium plan at a discounted price - $49 for the whole year",
+ "Currently, Opensox 2.0 is in progress (70% done) so till the launch, we are offering Pro plan at a discounted price - $49 for the whole year",
},
{
content:
@@ -69,7 +69,7 @@ const whySub = [
},
{
content:
- "After the launch, this $49 offer be removed and Opensox premium will be around ~ $120 for whole year ($10/mo.)",
+ "After the launch, this $49 offer be removed and Opensox Pro will be around ~ $120 for whole year ($10/mo.)",
},
{
content: "The price of the dollar is constantly increasing.",
@@ -93,17 +93,17 @@ const premiumPlanCard = {
"1:1 session on finding remote jobs and internships in open-source companies.",
"Quick doubts resolution.",
"Personalized guidance for GSoC, LFX, Outreachy, etc",
- "Access to premium Slack where you can ask anything anytime.",
+ "Access to Pro Slack where you can ask anything anytime.",
"Support to enhance skills for open source",
"GSOC proposal, resume reviews, etc.",
- "Upcoming premium features",
+ "Upcoming Pro features",
],
whatYouGetAfterLaunch: [
"Everything mentioned above",
- "Advanced tool with premium filters to find open source projects",
- "Premium newsletter",
+ "Advanced tool with Pro filters to find open source projects",
+ "Pro newsletter",
"30 days opensox challenge sheet",
- "Upcoming premium features.",
+ "Upcoming Pro features.",
],
};
@@ -202,7 +202,7 @@ const Pricing = () => {
}}
className="text-center text-3xl tracking-tight font-medium"
>
- Why should you subscribe to Opensox premium now?
+ Why should you subscribe to Opensox Pro now?
@@ -377,7 +377,7 @@ const SecondaryPricingCard = () => {
{username}
{showPremium && (
-
Opensox Premium
+
Opensox Pro
)}
@@ -445,14 +445,14 @@ const TestimonialsSection = () => {
id: 1,
username: "Tarun Parmar",
content:
- "Getting the Opensox Premium Subscription has been such a game-changer for me. I really like the personal touch in the way the team guides you-it feels like someone is genuinely there to help you navigate. It gave me the initial push I needed and made it so much easier to cut through all the chaos and focus on the right and simple steps. The best part is, it helps you start your open source journey quickly and I know I can reach out to the team anytime. Honestly, it's been an awesome experience so far!",
+ "Getting the Opensox Pro Subscription has been such a game-changer for me. I really like the personal touch in the way the team guides you-it feels like someone is genuinely there to help you navigate. It gave me the initial push I needed and made it so much easier to cut through all the chaos and focus on the right and simple steps. The best part is, it helps you start your open source journey quickly and I know I can reach out to the team anytime. Honestly, it's been an awesome experience so far!",
column: 1,
},
{
id: 2,
username: "Daksh Yadav",
content:
- "My experience with your guidance and opensox has been great. Your tips have really helped in doing my tasks quicker and better. And I would definitely recommend others to opt for opensox premium.",
+ "My experience with your guidance and opensox has been great. Your tips have really helped in doing my tasks quicker and better. And I would definitely recommend others to opt for opensox Pro.",
column: 1,
},
{
@@ -462,7 +462,7 @@ const TestimonialsSection = () => {
Okay so there are a few things I genuinely value about OpenSox
- Premium, and I'll focus on the core points because everything
+ Pro, and I'll focus on the core points because everything
else is just a natural extension of these.
@@ -496,7 +496,7 @@ const TestimonialsSection = () => {
right direction.
- Overall, I'd absolutely recommend OpenSox Premium to anyone
+ Overall, I'd absolutely recommend OpenSox Pro to anyone
serious about open source. The personalized guidance is exactly
what most of us hope for, since everyone is at a different stage
of their journey.
@@ -534,7 +534,7 @@ const TestimonialsSection = () => {
return (
-
+
{groupedTestimonials[1].map((testimonial) => (
diff --git a/apps/web/src/app/(main)/dashboard/account/page.tsx b/apps/web/src/app/(main)/dashboard/account/page.tsx
new file mode 100644
index 0000000..5a9fec8
--- /dev/null
+++ b/apps/web/src/app/(main)/dashboard/account/page.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { useSubscription } from "@/hooks/useSubscription";
+import Link from "next/link";
+import { ArrowLeft } from "lucide-react";
+
+export default function AccountPage() {
+ const { isPaidUser, isLoading } = useSubscription();
+
+ const plan = isPaidUser ? "Pro" : "Free";
+
+ return (
+
+ {isLoading ? (
+
+ Loading...
+
+ ) : (
+ <>
+
+
+
+
Back to Dashboard
+
+
+ Account Settings
+
+
+
+
+
+
+
Plan
+
+
+ {plan}
+
+ {isPaidUser && (
+
+ Active
+
+ )}
+
+
+ {!isPaidUser && (
+
+
+ be a pro
+
+
+ )}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/src/app/(main)/dashboard/layout.tsx b/apps/web/src/app/(main)/dashboard/layout.tsx
index 893a416..8eb8a05 100644
--- a/apps/web/src/app/(main)/dashboard/layout.tsx
+++ b/apps/web/src/app/(main)/dashboard/layout.tsx
@@ -5,6 +5,7 @@ import { useFilterStore } from "@/store/useFilterStore";
import { useShowSidebar } from "@/store/useShowSidebar";
import { IconWrapper } from "@/components/ui/IconWrapper";
import { Bars3Icon } from "@heroicons/react/24/outline";
+import Link from "next/link";
export default function DashboardLayout({
children,
@@ -14,19 +15,21 @@ export default function DashboardLayout({
const { showFilters } = useFilterStore();
const { showSidebar, setShowSidebar } = useShowSidebar();
return (
-
+
{showFilters &&
}
-
-
+
+
setShowSidebar(true)}>
-
Opensox
+
+ Opensox
+
-
+
{children}
diff --git a/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx b/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx
new file mode 100644
index 0000000..f29086a
--- /dev/null
+++ b/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { useSubscription } from "@/hooks/useSubscription";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+export default function ProDashboardPage() {
+ const { isPaidUser, isLoading } = useSubscription();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!isLoading && !isPaidUser) {
+ router.push("/pricing");
+ }
+ }, [isPaidUser, isLoading, router]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!isPaidUser) {
+ return null;
+ }
+
+ return (
+
+
+
+ hi investors, ajeetunc is on the way to deliver the shareholder value.
+ soon you'll see all the pro perks here. thanks for investing
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(main)/dashboard/sheet/page.tsx b/apps/web/src/app/(main)/dashboard/sheet/page.tsx
new file mode 100644
index 0000000..bb32eca
--- /dev/null
+++ b/apps/web/src/app/(main)/dashboard/sheet/page.tsx
@@ -0,0 +1,393 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useSession } from "next-auth/react";
+import { trpc } from "@/lib/trpc";
+import { sheetModules } from "@/data/sheet";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Checkbox } from "@/components/ui/checkbox";
+import Link from "next/link";
+import { FileText, Download, Share2, Check } from "lucide-react";
+import { Youtube } from "@/components/icons/icons";
+import { OpensoxProBadge } from "@/components/sheet/OpensoxProBadge";
+import { ProgressBar } from "@/components/sheet/ProgressBar";
+import { Badge } from "@/components/ui/badge";
+
+const tableColumns = [
+ "S.No",
+ "Module Name",
+ "Doc",
+ "Watch",
+ "Live Sessions / Doubts",
+ "Done?",
+];
+
+export default function SheetPage() {
+ const { data: session, status } = useSession();
+ const [completedSteps, setCompletedSteps] = useState
([]);
+ const [copied, setCopied] = useState(false);
+ const utils = trpc.useUtils();
+
+ // TypeScript has difficulty narrowing TRPC procedure union types.
+ // These procedures are correctly typed at runtime (query vs mutation).
+ const getCompletedStepsProcedure = trpc.user
+ .getCompletedSteps as typeof trpc.user.getCompletedSteps & {
+ useQuery: (input: undefined, opts?: any) => any;
+ };
+ const updateCompletedStepsProcedure = trpc.user
+ .updateCompletedSteps as typeof trpc.user.updateCompletedSteps & {
+ useMutation: (opts?: any) => any;
+ };
+ const getCompletedStepsUtilsProcedure = utils.user
+ .getCompletedSteps as typeof utils.user.getCompletedSteps & {
+ cancel: () => Promise;
+ invalidate: () => Promise;
+ };
+
+ const { data: fetchedSteps, isLoading: isLoadingSteps } =
+ getCompletedStepsProcedure.useQuery(undefined, {
+ enabled: !!session?.user && status === "authenticated",
+ refetchOnWindowFocus: false,
+ });
+
+ const updateStepsMutation = updateCompletedStepsProcedure.useMutation({
+ onMutate: async (newData: { completedSteps: string[] }) => {
+ // Cancel any outgoing refetches to avoid overwriting optimistic update
+ await getCompletedStepsUtilsProcedure.cancel();
+
+ // Snapshot the previous value
+ const previousSteps = completedSteps;
+
+ // Optimistically update to the new value
+ setCompletedSteps(newData.completedSteps);
+
+ // Return context with the previous value
+ return { previousSteps };
+ },
+ onSuccess: (data: string[]) => {
+ setCompletedSteps(data);
+ },
+ onError: (
+ error: unknown,
+ _newData: { completedSteps: string[] },
+ context: { previousSteps: string[] } | undefined
+ ) => {
+ console.error("Failed to update completed steps:", error);
+ if (context?.previousSteps) {
+ setCompletedSteps(context.previousSteps);
+ } else if (fetchedSteps) {
+ setCompletedSteps(fetchedSteps);
+ }
+ },
+ onSettled: () => {
+ getCompletedStepsUtilsProcedure.invalidate();
+ },
+ });
+
+ useEffect(() => {
+ if (fetchedSteps) {
+ setCompletedSteps(fetchedSteps);
+ }
+ }, [fetchedSteps]);
+
+ const handleCheckboxChange = (moduleId: string, checked: boolean) => {
+ let newCompletedSteps: string[];
+ if (checked) {
+ newCompletedSteps = [...completedSteps, moduleId];
+ } else {
+ newCompletedSteps = completedSteps.filter((id) => id !== moduleId);
+ }
+ setCompletedSteps(newCompletedSteps);
+ updateStepsMutation.mutate({ completedSteps: newCompletedSteps });
+ };
+
+ const handleDownloadPDF = () => {
+ // Create a printable version of the sheet
+ const printWindow = window.open("", "_blank");
+ if (!printWindow) return;
+
+ const total = sheetModules.length;
+ const totalCompleted = completedSteps.length;
+ const percentage =
+ total > 0 ? Math.round((totalCompleted / total) * 100) : 0;
+
+ const htmlContent = `
+
+
+
+ 30 days of Open Source sheet
+
+
+
+ 30 days of Open Source sheet
+
+
Total Progress: ${totalCompleted} / ${total} (${percentage}%)
+
+
+
+
+ S.No
+ Module Name
+ Status
+
+
+
+ ${sheetModules
+ .map(
+ (module, index) => `
+
+ ${index + 1}
+ ${module.name}
+
+ ${completedSteps.includes(module.id) ? "β Completed" : "Pending"}
+
+
+ `
+ )
+ .join("")}
+
+
+
+
+ `;
+
+ printWindow.document.write(htmlContent);
+ printWindow.document.close();
+ printWindow.focus();
+ setTimeout(() => {
+ printWindow.print();
+ }, 250);
+ };
+
+ const handleShare = async () => {
+ const url = window.location.href;
+ try {
+ await navigator.clipboard.writeText(url);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (clipboardErr) {
+ console.error("Failed to copy:", clipboardErr);
+ }
+ };
+
+ if (status === "loading" || isLoadingSteps) {
+ return (
+
+ );
+ }
+
+ const totalModules = sheetModules.length;
+ const completedCount = completedSteps.length;
+
+ return (
+
+
+
+
+ 30 days of Open Source sheet
+
+
+ (i don't have a marketing budget, please share this sheet with
+ others π :)
+
+
+
+ {copied && (
+
+
+ Copied
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Progress Bar */}
+
+
+
+
+ "sometimes, these modules may feel boring and hard af but
+ that's the cost of learning something worthy. you go through it.
+ you win. simple." β ajeet
+
+
+
+
+
+
+
+ {tableColumns.map((name, i) => (
+
+ {name}
+
+ ))}
+
+
+
+
+ {sheetModules.map((module, index) => {
+ const isCompleted = completedSteps.includes(module.id);
+ const isComingSoon = module.comingSoon === true;
+ return (
+
+
+ {index}
+
+
+
+
+ {module.name}
+ {isComingSoon && (
+
+ Coming Soon
+
+ )}
+
+
+
+
+ {isComingSoon ? (
+
+
+
+ read
+
+
+ ) : (
+
+
+
+ read
+
+
+ )}
+
+
+
+ {isComingSoon ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ handleCheckboxChange(module.id, checked === true)
+ }
+ disabled={isComingSoon}
+ className="border-ox-purple/50 data-[state=checked]:bg-ox-purple data-[state=checked]:border-ox-purple data-[state=checked]:text-white disabled:opacity-50 disabled:cursor-not-allowed"
+ />
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(main)/legal/privacy/page.tsx b/apps/web/src/app/(main)/legal/privacy/page.tsx
index a201597..b42e0df 100644
--- a/apps/web/src/app/(main)/legal/privacy/page.tsx
+++ b/apps/web/src/app/(main)/legal/privacy/page.tsx
@@ -351,7 +351,7 @@ export default function PrivacyPolicyPage() {
Sending emails and other communications according to your
preferences or that display content that we think will
interest you, including our newsletter about open-source
- trends, jobs, and internships (for premium subscribers).
+ trends, jobs, and internships (for Pro subscribers).
diff --git a/apps/web/src/app/(main)/legal/terms/page.tsx b/apps/web/src/app/(main)/legal/terms/page.tsx
index 1f52833..50f5fab 100644
--- a/apps/web/src/app/(main)/legal/terms/page.tsx
+++ b/apps/web/src/app/(main)/legal/terms/page.tsx
@@ -59,7 +59,7 @@ export default function TermsOfServicePage() {
opportunities
- Premium Features: Access to personalized
+ Pro Features: Access to personalized
mentoring, exclusive newsletter, open-source jobs and internship
opportunities, and our 30-day contribution challenge
@@ -136,10 +136,10 @@ export default function TermsOfServicePage() {
{/* Section 4 */}
- 4. Premium Subscription Services
+ 4. Pro Subscription Services
- Opensox.ai offers premium subscription plans with enhanced
+ Opensox.ai offers Pro subscription plans with enhanced
features including:
@@ -161,7 +161,7 @@ export default function TermsOfServicePage() {
- Payment Terms: Premium subscriptions are billed
+ Payment Terms: Pro subscriptions are billed
in advance on a monthly or annual basis. All fees are
non-refundable except as required by law. You may cancel your
subscription at any time, and cancellation will take effect at the
@@ -170,7 +170,7 @@ export default function TermsOfServicePage() {
Price Changes: We reserve the right to modify
subscription pricing with at least 30 days' notice. Continued use
- of premium services after a price change constitutes acceptance of
+ of Pro services after a price change constitutes acceptance of
the new pricing.
@@ -262,7 +262,7 @@ export default function TermsOfServicePage() {
8. Mentoring Services
- Premium subscribers may access personalized mentoring services.
+ Pro subscribers may access personalized mentoring services.
Please note: