diff --git a/.yarn/cache/@radix-ui-react-label-npm-2.1.0-5d222548ab-462f3b47f4.zip b/.yarn/cache/@radix-ui-react-label-npm-2.1.0-5d222548ab-462f3b47f4.zip deleted file mode 100644 index b68bb5d0e..000000000 Binary files a/.yarn/cache/@radix-ui-react-label-npm-2.1.0-5d222548ab-462f3b47f4.zip and /dev/null differ diff --git a/.yarn/cache/@radix-ui-react-select-npm-2.1.2-63aabdae67-cd662a5f0b.zip b/.yarn/cache/@radix-ui-react-select-npm-2.1.2-63aabdae67-cd662a5f0b.zip deleted file mode 100644 index d0729219a..000000000 Binary files a/.yarn/cache/@radix-ui-react-select-npm-2.1.2-63aabdae67-cd662a5f0b.zip and /dev/null differ diff --git a/.yarn/cache/@radix-ui-react-switch-npm-1.1.1-a3cfba4e9f-cf73981d0b.zip b/.yarn/cache/@radix-ui-react-switch-npm-1.1.1-a3cfba4e9f-cf73981d0b.zip deleted file mode 100644 index 222e93f57..000000000 Binary files a/.yarn/cache/@radix-ui-react-switch-npm-1.1.1-a3cfba4e9f-cf73981d0b.zip and /dev/null differ diff --git a/.yarn/cache/@radix-ui-react-tabs-npm-1.1.1-0eb30be792-9ceac8a655.zip b/.yarn/cache/@radix-ui-react-tabs-npm-1.1.1-0eb30be792-9ceac8a655.zip deleted file mode 100644 index faf12e054..000000000 Binary files a/.yarn/cache/@radix-ui-react-tabs-npm-1.1.1-0eb30be792-9ceac8a655.zip and /dev/null differ diff --git a/apps/web/.migrations/24-03-25_00-40-migrate-purchases-to-invoices.js b/apps/web/.migrations/24-03-25_00-40-migrate-purchases-to-invoices copy.js similarity index 100% rename from apps/web/.migrations/24-03-25_00-40-migrate-purchases-to-invoices.js rename to apps/web/.migrations/24-03-25_00-40-migrate-purchases-to-invoices copy.js diff --git a/apps/web/.migrations/30-03-25_00-40-migrate-hero-video.js b/apps/web/.migrations/30-03-25_00-40-migrate-hero-video.js new file mode 100644 index 000000000..768b889dc --- /dev/null +++ b/apps/web/.migrations/30-03-25_00-40-migrate-hero-video.js @@ -0,0 +1,126 @@ +import mongoose from "mongoose"; +import { nanoid } from "nanoid"; + +function generateUniqueId() { + return nanoid(); +} + +mongoose.connect(process.env.DB_CONNECTION_STRING, { + useNewUrlParser: true, + useUnifiedTopology: true, +}); + +const WidgetSchema = new mongoose.Schema({ + widgetId: { type: String, required: true, default: generateUniqueId }, + name: { type: String, required: true }, + deleteable: { type: Boolean, required: true, default: true }, + shared: { type: Boolean, required: true, default: false }, + settings: mongoose.Schema.Types.Mixed, +}); + +const MediaSchema = new mongoose.Schema({ + mediaId: { type: String, required: true }, + originalFileName: { type: String, required: true }, + mimeType: { type: String, required: true }, + size: { type: Number, required: true }, + access: { type: String, required: true, enum: ["public", "private"] }, + thumbnail: String, + caption: String, + file: String, +}); + +const PageSchema = new mongoose.Schema( + { + domain: { type: mongoose.Schema.Types.ObjectId, required: true }, + pageId: { type: String, required: true }, + type: { + type: String, + required: true, + enum: ["product", "site", "blog", "community"], + default: "product", + }, + creatorId: { type: String, required: true }, + name: { type: String, required: true }, + layout: { type: [WidgetSchema], default: [] }, + draftLayout: { type: [WidgetSchema], default: [] }, + entityId: { type: String }, + deleteable: { type: Boolean, required: true, default: false }, + title: { type: String }, + description: String, + socialImage: MediaSchema, + robotsAllowed: { type: Boolean, default: true }, + draftTitle: String, + draftDescription: String, + draftSocialImage: MediaSchema, + draftRobotsAllowed: Boolean, + deleted: { type: Boolean, default: false }, + }, + { + timestamps: true, + }, +); + +PageSchema.index( + { + domain: 1, + pageId: 1, + }, + { unique: true }, +); + +const Page = mongoose.model("Page", PageSchema); + +const updateHeroVideo = async (page) => { + console.log(`Updating homepage for domain: ${page.domain}`); + const heroWidgets = page.layout.filter((widget) => widget.name === "hero"); + for (const heroWidget of heroWidgets) { + heroWidget.settings.style = "normal"; + heroWidget.settings.mediaRadius = 2; + if ( + heroWidget && + heroWidget.settings.youtubeLink && + !heroWidget.settings.youtubeLink.startsWith( + "https://www.youtube.com/watch?v=", + ) + ) { + heroWidget.settings.youtubeLink = `https://www.youtube.com/watch?v=${heroWidget.settings.youtubeLink}`; + } + } + const heroWidgetsDraft = page.draftLayout.filter( + (widget) => widget.name === "hero", + ); + for (const heroWidget of heroWidgetsDraft) { + heroWidget.settings.style = "normal"; + heroWidget.settings.mediaRadius = 2; + if ( + heroWidget && + heroWidget.settings.youtubeLink && + !heroWidget.settings.youtubeLink.startsWith( + "https://www.youtube.com/watch?v=", + ) + ) { + heroWidget.settings.youtubeLink = `https://www.youtube.com/watch?v=${heroWidget.settings.youtubeLink}`; + } + } + page.markModified("layout"); + page.markModified("draftLayout"); + await page.save(); + console.log(`Updated homepage for domain: ${page.domain}\n`); +}; + +const migrateHeroVideo = async () => { + const pages = await Page.find({ pageId: "homepage" }); + for (const page of pages) { + try { + await updateHeroVideo(page); + } catch (error) { + console.error(`Error updating homepage for domain: ${page.domain}`); + console.error(error); + } + } +}; + +(async () => { + await migrateHeroVideo(); + mongoose.connection.close(); +})(); diff --git a/apps/web/app/(with-contexts)/(with-layout)/blog/blogs-list.tsx b/apps/web/app/(with-contexts)/(with-layout)/blog/blogs-list.tsx index 230c304e7..fb8ac0261 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/blog/blogs-list.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/blog/blogs-list.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { SkeletonCard } from "./skeleton-card"; -import { ContentCard } from "./content-card"; +import { BlogContentCard } from "./content-card"; import { PaginationControls } from "@components/public/pagination"; import { Constants, Course } from "@courselit/common-models"; import { useProducts } from "@/hooks/use-products"; @@ -34,7 +34,7 @@ export function BlogsList({ )) : products.map((product: Course) => ( - diff --git a/apps/web/app/(with-contexts)/(with-layout)/blog/content-card.tsx b/apps/web/app/(with-contexts)/(with-layout)/blog/content-card.tsx index b040ee9b3..a7c78f82c 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/blog/content-card.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/blog/content-card.tsx @@ -5,7 +5,7 @@ import { getPlanPrice } from "@courselit/utils"; import { truncate } from "@ui-lib/utils"; import Image from "next/image"; -export function ContentCard({ product }: { product: Course }) { +export function BlogContentCard({ product }: { product: Course }) { const defaultPlan = product.paymentPlans?.filter( (plan) => plan.planId === product.defaultPaymentPlan, )[0]; diff --git a/apps/web/app/(with-contexts)/(with-layout)/communities/communities-list.tsx b/apps/web/app/(with-contexts)/(with-layout)/communities/communities-list.tsx index 751d8f53d..612ace945 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/communities/communities-list.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/communities/communities-list.tsx @@ -1,7 +1,7 @@ "use client"; import { useCommunities } from "@/hooks/use-communities"; -import { ContentCard } from "./content-card"; +import { CommunityContentCard } from "./content-card"; import { PaginationControls } from "@components/public/pagination"; import { Community } from "@courselit/common-models"; import { Users } from "lucide-react"; @@ -63,7 +63,7 @@ export function CommunitiesList({ )) : communities.map((community: Community) => ( - - - - - {community.enabled ? ( - - ) : ( - - )} - - - {community.enabled ? "Enabled" : "Draft"} - - - + {!publicView && ( + + + + {community.enabled ? ( + + ) : ( + + )} + + + {community.enabled + ? "Enabled" + : "Draft"} + + + + )} diff --git a/apps/web/app/(with-contexts)/(with-layout)/products/content-card.tsx b/apps/web/app/(with-contexts)/(with-layout)/products/content-card.tsx index 5ba50a282..28339b4ab 100644 --- a/apps/web/app/(with-contexts)/(with-layout)/products/content-card.tsx +++ b/apps/web/app/(with-contexts)/(with-layout)/products/content-card.tsx @@ -8,7 +8,7 @@ import { SiteInfoContext } from "@components/contexts"; import { Badge } from "@components/ui/badge"; import { truncate } from "@ui-lib/utils"; -export function ContentCard({ product }: { product: Course }) { +export function ProductContentCard({ product }: { product: Course }) { const siteinfo = useContext(SiteInfoContext); const defaultPlan = product.paymentPlans?.filter( (plan) => plan.planId === product.defaultPaymentPlan, diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/my-content/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/my-content/page.tsx index 69c61ba13..5831f3258 100644 --- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/my-content/page.tsx +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/my-content/page.tsx @@ -1,8 +1,6 @@ "use client"; import { useState, useEffect, useContext } from "react"; -// import { ContentCard } from "@/components/admin/my-content/content-card"; -// import { SkeletonCard } from "@/components/admin/my-content/skeleton-card"; import type { ContentItem } from "@/components/admin/my-content/content"; import { AddressContext, ProfileContext } from "@components/contexts"; import { MY_CONTENT_HEADER } from "@ui-config/strings"; diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/page.tsx index 33df02076..5afd5df1e 100644 --- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/page.tsx +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/page.tsx @@ -315,10 +315,10 @@ export default function ContentPage() { (a: any, b: any) => ( section.lessonsOrder as any[] - ).indexOf(a.lessonId) - + )?.indexOf(a.lessonId) - ( section.lessonsOrder as any[] - ).indexOf(b.lessonId), + )?.indexOf(b.lessonId), ) .map((lesson: Lesson) => ({ id: lesson.lessonId, diff --git a/apps/web/components/admin/page-editor/index.tsx b/apps/web/components/admin/page-editor/index.tsx index c9c0b6793..79dedfc69 100644 --- a/apps/web/components/admin/page-editor/index.tsx +++ b/apps/web/components/admin/page-editor/index.tsx @@ -451,6 +451,7 @@ export default function PageEditor({ onDelete={deleteWidget} state={state as AppState} dispatch={dispatch || (() => {})} + key={selectedWidget} /> ), [selectedWidget], diff --git a/apps/web/components/admin/products/editor/content/lessons-list.tsx b/apps/web/components/admin/products/editor/content/lessons-list.tsx index 8de63d3d0..4231c4d3f 100644 --- a/apps/web/components/admin/products/editor/content/lessons-list.tsx +++ b/apps/web/components/admin/products/editor/content/lessons-list.tsx @@ -132,10 +132,10 @@ function LessonSection({ ) .sort( (a: any, b: any) => - (group.lessonsOrder as any[]).indexOf( + (group.lessonsOrder as any[])?.indexOf( a.lessonId, ) - - (group.lessonsOrder as any[]).indexOf( + (group.lessonsOrder as any[])?.indexOf( b.lessonId, ), ) diff --git a/apps/web/graphql/courses/helpers.ts b/apps/web/graphql/courses/helpers.ts index fcc95da95..ee5a60c76 100644 --- a/apps/web/graphql/courses/helpers.ts +++ b/apps/web/graphql/courses/helpers.ts @@ -177,7 +177,7 @@ export const setupCourse = async ({ ctx, }); page.entityId = course.courseId; - page.layout = getInitialLayout(); + page.layout = getInitialLayout(type); await page.save(); return course; @@ -205,8 +205,8 @@ export const setupBlog = async ({ return course; }; -const getInitialLayout = () => { - return [ +const getInitialLayout = (type: "course" | "download") => { + const layout: Record[] = [ { name: "header", deleteable: false, @@ -215,10 +215,20 @@ const getInitialLayout = () => { { name: "banner", }, - { - name: "footer", - deleteable: false, - shared: true, - }, ]; + if (type === Constants.CourseType.COURSE) { + layout.push({ + name: "content", + settings: { + title: "Curriculum", + headerAlignment: "center", + }, + }); + } + layout.push({ + name: "footer", + deleteable: false, + shared: true, + }); + return layout; }; diff --git a/apps/web/graphql/courses/logic.ts b/apps/web/graphql/courses/logic.ts index 08191a9ed..f698a183e 100644 --- a/apps/web/graphql/courses/logic.ts +++ b/apps/web/graphql/courses/logic.ts @@ -157,7 +157,7 @@ export const getCourse = async ( // course.groups = accessibleGroups; return await formatCourse(course.courseId, ctx); } else { - throw new Error(responses.item_not_found); + return null; } }; diff --git a/apps/web/graphql/lessons/helpers.ts b/apps/web/graphql/lessons/helpers.ts index 7717fb40a..e778ab314 100644 --- a/apps/web/graphql/lessons/helpers.ts +++ b/apps/web/graphql/lessons/helpers.ts @@ -103,8 +103,8 @@ export const getGroupedLessons = async ( ) .sort( (a: GroupLessonItem, b: GroupLessonItem) => - group.lessonsOrder.indexOf(a.lessonId) - - group.lessonsOrder.indexOf(b.lessonId), + group.lessonsOrder?.indexOf(a.lessonId) - + group.lessonsOrder?.indexOf(b.lessonId), ), ); } diff --git a/apps/web/graphql/pages/page-templates.ts b/apps/web/graphql/pages/page-templates.ts index 04f1c73db..bd1fbc3f1 100644 --- a/apps/web/graphql/pages/page-templates.ts +++ b/apps/web/graphql/pages/page-templates.ts @@ -72,11 +72,11 @@ export const homePageTemplate = [ }, buttonAction: "/products", buttonCaption: "Ask user to take action", - youtubeLink: "VLVcZB2-udk", + youtubeLink: "https://www.youtube.com/watch?v=VLVcZB2-udk", alignment: "right", - style: "card", + style: "normal", buttonForeground: "#fefbfb", - mediaRadius: 56, + mediaRadius: 2, horizontalPadding: 100, verticalPadding: 88, titleFontSize: 5, diff --git a/apps/web/pages/course/[slug]/[id]/index.tsx b/apps/web/pages/course/[slug]/[id]/index.tsx index 5917799a1..85a198d11 100644 --- a/apps/web/pages/course/[slug]/[id]/index.tsx +++ b/apps/web/pages/course/[slug]/[id]/index.tsx @@ -407,8 +407,8 @@ export function formatCourse( .filter((lesson: Lesson) => lesson.groupId === group.id) .sort( (a: any, b: any) => - group.lessonsOrder.indexOf(a.lessonId) - - group.lessonsOrder.indexOf(b.lessonId), + group.lessonsOrder?.indexOf(a.lessonId) - + group.lessonsOrder?.indexOf(b.lessonId), ); } diff --git a/packages/common-widgets/src/content/widget.tsx b/packages/common-widgets/src/content/widget.tsx index ab8f76300..5b16803c7 100644 --- a/packages/common-widgets/src/content/widget.tsx +++ b/packages/common-widgets/src/content/widget.tsx @@ -133,8 +133,8 @@ export default function Widget({ .filter((lesson: Lesson) => lesson.groupId === group.id) .sort( (a: any, b: any) => - group.lessonsOrder.indexOf(a.lessonId) - - group.lessonsOrder.indexOf(b.lessonId), + group.lessonsOrder?.indexOf(a.lessonId) - + group.lessonsOrder?.indexOf(b.lessonId), ); }); setFormattedCourse(formattedCourse); diff --git a/packages/common-widgets/src/hero/admin-widget.tsx b/packages/common-widgets/src/hero/admin-widget.tsx index 5b1e4e987..c0739e567 100644 --- a/packages/common-widgets/src/hero/admin-widget.tsx +++ b/packages/common-widgets/src/hero/admin-widget.tsx @@ -24,13 +24,15 @@ import { PageBuilderSlider, PageBuilderPropertyHeader, CssIdField, + AspectRatio, + ImageObjectFit, + Checkbox, } from "@courselit/components-library"; import { verticalPadding as defaultVerticalPadding, horizontalPadding as defaultHorizontalPadding, - mediaAspectRatio as defaultMediaAspectRatio, } from "./defaults"; -import { MediaAspectRatio } from "./types"; +import { isVideo } from "@courselit/utils"; interface AdminWidgetProps { name: string; @@ -67,9 +69,8 @@ export default function AdminWidget({ ); const [buttonAction, setButtonAction] = useState(settings.buttonAction); const [buttonCaption, setButtonCaption] = useState(settings.buttonCaption); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [mediaBorderRadius, setMediaBorderRadius] = useState( - settings.mediaRadius, + settings.mediaRadius || 2, ); const [youtubeLink, setYoutubeLink] = useState(settings.youtubeLink); const [alignment, setAlignment] = useState(settings.alignment || "left"); @@ -114,10 +115,16 @@ export default function AdminWidget({ const [contentAlignment, setContentAlignment] = useState( settings.contentAlignment || "center", ); - const [mediaAspectRatio, setMediaAspectRatio] = useState( - settings.mediaAspectRatio || defaultMediaAspectRatio, - ); const [cssId, setCssId] = useState(settings.cssId); + const [playVideoInModal, setPlayVideoInModal] = useState( + settings.playVideoInModal || false, + ); + const [aspectRatio, setAspectRatio] = useState( + settings.aspectRatio || "16/9", + ); + const [objectFit, setObjectFit] = useState( + settings.objectFit || "cover", + ); const onSettingsChanged = () => onChange({ @@ -127,7 +134,6 @@ export default function AdminWidget({ buttonCaption, youtubeLink, media, - mediaAspectRatio, alignment, backgroundColor, foregroundColor, @@ -145,6 +151,9 @@ export default function AdminWidget({ descriptionFontSize, contentAlignment, cssId, + playVideoInModal, + aspectRatio, + objectFit, }); useEffect(() => { @@ -162,7 +171,6 @@ export default function AdminWidget({ buttonBackground, buttonForeground, media, - mediaAspectRatio, mediaBorderRadius, horizontalPadding, verticalPadding, @@ -174,6 +182,9 @@ export default function AdminWidget({ descriptionFontSize, contentAlignment, cssId, + playVideoInModal, + aspectRatio, + objectFit, ]); return ( @@ -199,8 +210,8 @@ export default function AdminWidget({
setYoutubeLink(e.target.value)} /> @@ -228,7 +239,52 @@ export default function AdminWidget({ mediaId={media && media.mediaId} type="page" /> - {media && media.mediaId && ( + {isVideo(youtubeLink, media) ? ( +
+
+
+

Play video in a pop-up

+
+ + setPlayVideoInModal(value) + } + /> +
+ + setObjectFit(value) + } + /> +
+ )} + {/* {media && media.mediaId && ( + setAspectRatio(value) + } + /> + + ) : ( +
+