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 + - {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 +

+ +
+ )} + + {/* 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", + ], + }, +];