diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index 2bf9695f..0827dbae 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -102,3 +102,28 @@ model Plan {
updatedAt DateTime @updatedAt
subscriptions Subscription[]
}
+
+model WeeklySession {
+ id String @id @default(cuid())
+ title String
+ description String?
+ youtubeUrl String
+ sessionDate DateTime
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ topics SessionTopic[]
+
+ @@index([sessionDate])
+ @@index([createdAt])
+}
+
+model SessionTopic {
+ id String @id @default(cuid())
+ sessionId String
+ timestamp String
+ topic String
+ order Int
+ session WeeklySession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
+
+ @@index([sessionId, order])
+}
\ No newline at end of file
diff --git a/apps/api/src/routers/_app.ts b/apps/api/src/routers/_app.ts
index 782b4361..252e48b3 100644
--- a/apps/api/src/routers/_app.ts
+++ b/apps/api/src/routers/_app.ts
@@ -4,6 +4,7 @@ import { userRouter } from "./user.js";
import { projectRouter } from "./projects.js";
import { authRouter } from "./auth.js";
import { paymentRouter } from "./payment.js";
+import { sessionsRouter } from "./sessions.js";
import { z } from "zod";
const testRouter = router({
@@ -21,6 +22,7 @@ export const appRouter = router({
project: projectRouter,
auth: authRouter,
payment: paymentRouter,
+ sessions: sessionsRouter,
});
export type AppRouter = typeof appRouter;
diff --git a/apps/api/src/routers/sessions.ts b/apps/api/src/routers/sessions.ts
new file mode 100644
index 00000000..ab99d658
--- /dev/null
+++ b/apps/api/src/routers/sessions.ts
@@ -0,0 +1,11 @@
+import { router, protectedProcedure } from "../trpc.js";
+import { sessionService } from "../services/session.service.js";
+
+export const sessionsRouter = router({
+ // get all sessions for authenticated paid users
+ getAll: protectedProcedure.query(async ({ ctx }: any) => {
+ const userId = ctx.user.id;
+ return await sessionService.getSessions(ctx.db.prisma, userId);
+ }),
+});
+
diff --git a/apps/api/src/services/session.service.ts b/apps/api/src/services/session.service.ts
new file mode 100644
index 00000000..a2b2d5e9
--- /dev/null
+++ b/apps/api/src/services/session.service.ts
@@ -0,0 +1,46 @@
+import type { PrismaClient } from "@prisma/client";
+import type { ExtendedPrismaClient } from "../prisma.js";
+import { SUBSCRIPTION_STATUS } from "../constants/subscription.js";
+
+export const sessionService = {
+ /**
+ * Get all sessions for authenticated paid users
+ * Sessions are ordered by sessionDate descending (newest first)
+ */
+ async getSessions(
+ prisma: ExtendedPrismaClient | PrismaClient,
+ userId: string
+ ) {
+ // verify user has active subscription
+ const subscription = await prisma.subscription.findFirst({
+ where: {
+ userId,
+ status: SUBSCRIPTION_STATUS.ACTIVE,
+ endDate: {
+ gte: new Date(),
+ },
+ },
+ });
+
+ if (!subscription) {
+ throw new Error("Active subscription required to access sessions");
+ }
+
+ // fetch sessions with topics ordered by sessionDate descending
+ const sessions = await prisma.weeklySession.findMany({
+ include: {
+ topics: {
+ orderBy: {
+ order: "asc",
+ },
+ },
+ },
+ orderBy: {
+ sessionDate: "desc",
+ },
+ });
+
+ return sessions;
+ },
+};
+
diff --git a/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx b/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx
index fae7f9ba..13242854 100644
--- a/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx
+++ b/apps/web/src/app/(main)/dashboard/pro/dashboard/page.tsx
@@ -4,6 +4,8 @@ import { useSubscription } from "@/hooks/useSubscription";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
+import Link from "next/link";
+import { Play } from "lucide-react";
export default function ProDashboardPage() {
const { isPaidUser, isLoading } = useSubscription();
@@ -109,17 +111,26 @@ export default function ProDashboardPage() {
soon you'll see all the pro perks here. thanks for investin!
{isPaidUser && (
-
+
+
+
+ Pro Sessions
+
{isJoining ? "Joining..." : "Join Slack"}
- {error &&
{error}
}
)}
+ {error && (
+
{error}
+ )}
);
diff --git a/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx
new file mode 100644
index 00000000..c1abaf41
--- /dev/null
+++ b/apps/web/src/app/(main)/dashboard/pro/sessions/page.tsx
@@ -0,0 +1,274 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+import { ArrowLeft, CheckCircle2, ExternalLink, Play } from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useSession } from "next-auth/react";
+
+import { useSubscription } from "@/hooks/useSubscription";
+import { trpc } from "@/lib/trpc";
+
+interface SessionTopic {
+ id: string;
+ timestamp: string;
+ topic: string;
+ order: number;
+}
+
+interface WeeklySession {
+ id: string;
+ title: string;
+ description: string | null;
+ youtubeUrl: string;
+ sessionDate: Date;
+ createdAt: Date;
+ updatedAt: Date;
+ topics: SessionTopic[];
+}
+
+const SessionCard = ({
+ session,
+ index,
+}: {
+ session: WeeklySession;
+ index: number;
+}): JSX.Element | null => {
+ const [isHovered, setIsHovered] = useState(false);
+
+ const handleClick = () => {
+ window.open(session.youtubeUrl, "_blank", "noopener,noreferrer");
+ };
+
+ return (
+ {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ handleClick();
+ }
+ }}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ aria-label={`Watch session: ${session.title}`}
+ className="group relative bg-dash-surface border border-dash-border rounded-xl p-5 cursor-pointer
+ transition-all duration-300 ease-out
+ hover:border-brand-purple/50 hover:bg-dash-hover hover:shadow-lg hover:shadow-brand-purple/5
+ hover:-translate-y-1 active:scale-[0.98]
+ focus-visible:ring-2 focus-visible:ring-brand-purple/50 focus-visible:outline-none"
+ style={{
+ animationDelay: `${index * 50}ms`,
+ }}
+ >
+ {/* Session number badge */}
+
+
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
+ {session.title}
+
+
+
+
+
+ {/* Topics covered */}
+ {session.topics && session.topics.length > 0 && (
+
+
+ Topics Covered
+
+
+ {session.topics.map((topic) => (
+
+
+
+ {topic.timestamp && (
+ [{topic.timestamp}]
+ )}
+ {topic.topic}
+
+
+ ))}
+
+
+ )}
+
+ {/* Watch now indicator */}
+
+
+ Watch on YouTube
+
+
+ {/* Hover glow effect */}
+
+
+ );
+};
+
+const ProSessionsPage = (): JSX.Element | null => {
+ const { isPaidUser, isLoading: subscriptionLoading } = useSubscription();
+ const { data: session, status } = useSession();
+ const router = useRouter();
+
+ // fetch sessions from api
+ const {
+ data: sessions,
+ isLoading: sessionsLoading,
+ isError: sessionsError,
+ error: sessionsErrorData,
+ } = (trpc.sessions as any).getAll.useQuery(undefined, {
+ enabled: !!session?.user && status === "authenticated" && isPaidUser,
+ refetchOnWindowFocus: false,
+ staleTime: 5 * 60 * 1000, // consider data fresh for 5 minutes
+ });
+
+ useEffect(() => {
+ if (!subscriptionLoading && !isPaidUser) {
+ router.push("/pricing");
+ }
+ }, [isPaidUser, subscriptionLoading, router]);
+
+ const isLoading = subscriptionLoading || sessionsLoading;
+ const hasError = sessionsError;
+
+ if (isLoading) {
+ return (
+
+
+
+
Loading sessions...
+
+
+ );
+ }
+
+ if (!isPaidUser) {
+ return null;
+ }
+
+ if (hasError) {
+ return (
+
+
+
+
+
+
Back to Pro Dashboard
+
+
+
+
+ Failed to load sessions. Please try again later.
+
+ {sessionsErrorData && (
+
+ {(sessionsErrorData as any)?.message || "Unknown error"}
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+ {/* Back link */}
+
+
+
Back to Pro Dashboard
+
+
+
+
+ Opensox Pro Sessions
+
+
+
+ Recordings of Opensox Pro session meetings covering advanced open
+ source strategies, real-world examples, and insider tips to
+ accelerate your journey.
+
+
+
+ {/* Sessions Grid */}
+ {sessions && sessions.length > 0 ? (
+
+ {sessions.map((session: WeeklySession, index: number) => (
+
+ ))}
+
+ ) : (
+
+
+ No sessions available yet.
+
+
+ Check back soon for new session recordings.
+
+
+ )}
+
+ {/* Footer note */}
+
+
+ More sessions coming soon • Stay tuned for updates
+
+
+
+
+ );
+};
+
+export default ProSessionsPage;
diff --git a/apps/web/src/data/pro-sessions.ts b/apps/web/src/data/pro-sessions.ts
new file mode 100644
index 00000000..dacd0422
--- /dev/null
+++ b/apps/web/src/data/pro-sessions.ts
@@ -0,0 +1,112 @@
+/**
+ * Pro Sessions Data
+ *
+ * Contains all the Opensox Pro Session YouTube videos with their topics
+ */
+
+export interface ProSession {
+ id: number;
+ title: string;
+ youtubeUrl: string;
+ topicsCovered: string[];
+ duration?: string;
+}
+
+export const proSessions: ProSession[] = [
+ {
+ id: 1,
+ title: "Pro Session 01",
+ youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ",
+ topicsCovered: [
+ "Introduction to Open Source Contributions",
+ "Setting up your development environment",
+ "Understanding Git workflow basics",
+ ],
+ },
+ {
+ id: 2,
+ title: "Pro Session 02",
+ youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI",
+ topicsCovered: [
+ "Finding your first issue",
+ "Reading project documentation effectively",
+ "Communicating with maintainers",
+ ],
+ },
+ {
+ id: 3,
+ title: "Pro Session 03",
+ youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ",
+ topicsCovered: [
+ "Writing clean pull requests",
+ "Code review best practices",
+ "Handling feedback gracefully",
+ ],
+ },
+ {
+ id: 4,
+ title: "Pro Session 04",
+ youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI",
+ topicsCovered: [
+ "Building your Open Source portfolio",
+ "Showcasing contributions on GitHub",
+ "Networking in the OSS community",
+ ],
+ },
+ {
+ id: 5,
+ title: "Pro Session 05",
+ youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ",
+ topicsCovered: [
+ "Advanced Git techniques",
+ "Rebasing and resolving conflicts",
+ "Cherry-picking commits",
+ ],
+ },
+ {
+ id: 6,
+ title: "Pro Session 06",
+ youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI",
+ topicsCovered: [
+ "Understanding CI/CD pipelines",
+ "Writing effective tests",
+ "Debugging failed builds",
+ ],
+ },
+ {
+ id: 7,
+ title: "Pro Session 07",
+ youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ",
+ topicsCovered: [
+ "Contributing to documentation",
+ "Writing technical content",
+ "Documentation as code",
+ ],
+ },
+ {
+ id: 8,
+ title: "Pro Session 08",
+ youtubeUrl: "https://www.youtube.com/watch?v=fp6fiTce-fI",
+ topicsCovered: [
+ "Preparing for GSoC",
+ "Writing winning proposals",
+ "Building relationships with mentors",
+ ],
+ },
+ {
+ id: 9,
+ title: "Pro Session 09",
+ youtubeUrl: "https://www.youtube.com/watch?v=nZOLgP2P8aQ",
+ topicsCovered: [
+ "Landing your first OSS internship",
+ "Resume tips for developers",
+ "Leveraging OSS for career growth",
+ "Landing your first OSS internship",
+ "Resume tips for developers",
+ "Leveraging OSS for career growth",
+ "Landing your first OSS internship",
+ "Resume tips for developers",
+ "Leveraging OSS for career growth",
+ ],
+ },
+];