mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Rename from workshops to videos
This commit is contained in:
parent
127e7d9029
commit
aa13faaf44
@ -2,7 +2,7 @@ import React, { useState, useEffect, use } from 'react';
|
||||
import { Carousel } from 'primereact/carousel';
|
||||
import { parseCourseEvent } from '@/utils/nostr';
|
||||
// import CourseTemplate from '@/components/content/carousels/templates/CourseTemplate';
|
||||
import { CourseTemplate } from '@/components/content/carousels/newTemplates/CourseTemplate';
|
||||
import { CourseTemplate } from '@/components/content/carousels/templates/CourseTemplate';
|
||||
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
||||
import { useCourses } from '@/hooks/nostr/useCourses';
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Carousel } from 'primereact/carousel';
|
||||
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
||||
import { VideoTemplate } from '@/components/content/carousels/newTemplates/VideoTemplate';
|
||||
import { DocumentTemplate } from '@/components/content/carousels/newTemplates/DocumentTemplate';
|
||||
import { CourseTemplate } from '@/components/content/carousels/newTemplates/CourseTemplate';
|
||||
import { VideoTemplate } from '@/components/content/carousels/templates/VideoTemplate';
|
||||
import { DocumentTemplate } from '@/components/content/carousels/templates/DocumentTemplate';
|
||||
import { CourseTemplate } from '@/components/content/carousels/templates/CourseTemplate';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const responsiveOptions = [
|
||||
@ -66,7 +66,7 @@ export default function GenericCarousel({items, selectedTopic, title}) {
|
||||
if (carouselItems.length > 0) {
|
||||
if (item.type === 'resource') {
|
||||
return <DocumentTemplate key={item.id} document={item} />;
|
||||
} else if (item.type === 'workshop') {
|
||||
} else if (item.type === 'video') {
|
||||
return <VideoTemplate key={item.id} video={item} />;
|
||||
} else if (item.type === 'course') {
|
||||
return <CourseTemplate key={item.id} course={item} />;
|
||||
|
@ -25,8 +25,8 @@ const promotions = [
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: "WORKSHOPS",
|
||||
title: "Hands-on Video Workshops",
|
||||
category: "VIDEOS",
|
||||
title: "Hands-on workshops and devleloper video content",
|
||||
description: "Watch and learn with our interactive video workshops. Get practical experience building real Bitcoin and Lightning applications.",
|
||||
icon: "pi pi-video",
|
||||
image: "https://newsroom.siliconslopes.com/content/images/2018/10/code.jpg",
|
||||
@ -97,9 +97,9 @@ const InteractivePromotionalCarousel = () => {
|
||||
return (
|
||||
<GenericButton onClick={() => router.push('/content?tag=courses')} icon={<i className="pi pi-book pr-2 pb-1" />} label="View All Courses" className="w-fit py-2 font-semibold" size="small" outlined />
|
||||
);
|
||||
case "WORKSHOPS":
|
||||
case "VIDEOS":
|
||||
return (
|
||||
<GenericButton onClick={() => router.push('/content?tag=workshops')} icon={<i className="pi pi-video pr-2" />} label="View All Workshops" className="w-fit py-2 font-semibold" size="small" outlined />
|
||||
<GenericButton onClick={() => router.push('/content?tag=videos')} icon={<i className="pi pi-video pr-2" />} label="View All Videos" className="w-fit py-2 font-semibold" size="small" outlined />
|
||||
);
|
||||
case "RESOURCES":
|
||||
return (
|
||||
@ -145,9 +145,9 @@ const InteractivePromotionalCarousel = () => {
|
||||
return (
|
||||
<GenericButton onClick={() => router.push('/content?tag=courses')} icon={<i className="pi pi-book pr-2 pb-1" />} label="View All Courses" className="py-2 font-semibold" size="small" outlined />
|
||||
);
|
||||
case "WORKSHOPS":
|
||||
case "VIDEOS":
|
||||
return (
|
||||
<GenericButton onClick={() => router.push('/content?tag=workshops')} icon={<i className="pi pi-video pr-2" />} label="View All Workshops" className="py-2 font-semibold" size="small" outlined />
|
||||
<GenericButton onClick={() => router.push('/content?tag=videos')} icon={<i className="pi pi-video pr-2" />} label="View All Videos" className="py-2 font-semibold" size="small" outlined />
|
||||
);
|
||||
case "RESOURCES":
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Carousel } from 'primereact/carousel';
|
||||
import { parseEvent } from '@/utils/nostr';
|
||||
// import ResourceTemplate from '@/components/content/carousels/templates/ResourceTemplate';
|
||||
import { DocumentTemplate } from '@/components/content/carousels/newTemplates/DocumentTemplate';
|
||||
import { DocumentTemplate } from '@/components/content/carousels/templates/DocumentTemplate';
|
||||
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
||||
import { useResources } from '@/hooks/nostr/useResources';
|
||||
|
||||
|
67
src/components/content/carousels/VideosCarousel.js
Normal file
67
src/components/content/carousels/VideosCarousel.js
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Carousel } from 'primereact/carousel';
|
||||
import { parseEvent } from '@/utils/nostr';
|
||||
import {VideoTemplate} from '@/components/content/carousels/templates/VideoTemplate';
|
||||
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
||||
import { useVideos } from '@/hooks/nostr/useVideos';
|
||||
|
||||
const responsiveOptions = [
|
||||
{
|
||||
breakpoint: '3000px',
|
||||
numVisible: 3,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '1462px',
|
||||
numVisible: 2,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '575px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
];
|
||||
|
||||
export default function VideosCarousel() {
|
||||
const [processedVideos, setProcessedVideos] = useState([]);
|
||||
const { videos, videosLoading, videosError } = useVideos();
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
if (videos && videos.length > 0) {
|
||||
const processedVideos = videos.map(video => parseEvent(video));
|
||||
|
||||
const sortedVideos = processedVideos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
console.log('Sorted videos:', sortedVideos);
|
||||
setProcessedVideos(sortedVideos);
|
||||
} else {
|
||||
console.log('No videos fetched or empty array returned');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching videos:', error);
|
||||
}
|
||||
};
|
||||
fetch();
|
||||
}, [videos]);
|
||||
|
||||
if (videosError) return <div>Error: {videosError}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="ml-[6%] mt-4">Videos</h3>
|
||||
<Carousel
|
||||
value={videosLoading || !processedVideos.length ? [{}, {}, {}] : [...processedVideos]}
|
||||
numVisible={2}
|
||||
itemTemplate={(item) =>
|
||||
!processedVideos.length ?
|
||||
<TemplateSkeleton key={Math.random()} /> :
|
||||
<VideoTemplate key={item.id} video={item} />
|
||||
}
|
||||
responsiveOptions={responsiveOptions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Carousel } from 'primereact/carousel';
|
||||
import { parseEvent } from '@/utils/nostr';
|
||||
// import WorkshopTemplate from '@/components/content/carousels/templates/WorkshopTemplate';
|
||||
import {VideoTemplate} from '@/components/content/carousels/newTemplates/VideoTemplate';
|
||||
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
|
||||
import { useWorkshops } from '@/hooks/nostr/useWorkshops';
|
||||
|
||||
const responsiveOptions = [
|
||||
{
|
||||
breakpoint: '3000px',
|
||||
numVisible: 3,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '1462px',
|
||||
numVisible: 2,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '575px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
];
|
||||
|
||||
export default function WorkshopsCarousel() {
|
||||
const [processedWorkshops, setProcessedWorkshops] = useState([]);
|
||||
const { workshops, workshopsLoading, workshopsError } = useWorkshops();
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
if (workshops && workshops.length > 0) {
|
||||
const processedWorkshops = workshops.map(workshop => parseEvent(workshop));
|
||||
|
||||
// Sort workshops by created_at in descending order (most recent first)
|
||||
const sortedWorkshops = processedWorkshops.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
console.log('Sorted workshops:', sortedWorkshops);
|
||||
setProcessedWorkshops(sortedWorkshops);
|
||||
} else {
|
||||
console.log('No workshops fetched or empty array returned');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching workshops:', error);
|
||||
}
|
||||
};
|
||||
fetch();
|
||||
}, [workshops]);
|
||||
|
||||
if (workshopsError) return <div>Error: {workshopsError}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="ml-[6%] mt-4">Workshops</h3>
|
||||
<Carousel
|
||||
value={workshopsLoading || !processedWorkshops.length ? [{}, {}, {}] : [...processedWorkshops]}
|
||||
numVisible={2}
|
||||
itemTemplate={(item) =>
|
||||
!processedWorkshops.length ?
|
||||
<TemplateSkeleton key={Math.random()} /> :
|
||||
<VideoTemplate key={item.id} video={item} />
|
||||
}
|
||||
responsiveOptions={responsiveOptions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Tag } from "primereact/tag";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import Image from "next/image"
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useRouter } from "next/router";
|
||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { defaultRelayUrls } from "@/context/NDKContext";
|
||||
|
||||
export function CourseTemplate({ course }) {
|
||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: course });
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const [lessonCount, setLessonCount] = useState(0);
|
||||
const [nAddress, setNAddress] = useState(null);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, course);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, course]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course && course?.tags) {
|
||||
const lessons = course.tags.filter(tag => tag[0] === "a");
|
||||
setLessonCount(lessons.length);
|
||||
}
|
||||
}, [course]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course && course?.id) {
|
||||
const nAddress = nip19.naddrEncode({
|
||||
pubkey: course.pubkey,
|
||||
kind: course.kind,
|
||||
identifier: course.id,
|
||||
relayUrls: defaultRelayUrls
|
||||
});
|
||||
setNAddress(nAddress);
|
||||
}
|
||||
}, [course]);
|
||||
|
||||
if (!nAddress) return <ProgressSpinner />;
|
||||
|
||||
if (zapsError) return <div>Error: {zapsError}</div>;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden group hover:shadow-xl transition-all duration-300 bg-gray-800 m-2 border-none">
|
||||
<div className="relative h-48 sm:h-64">
|
||||
<Image
|
||||
src={returnImageProxy(course.image)}
|
||||
alt="Course background"
|
||||
quality={100}
|
||||
layout="fill"
|
||||
className={`${router.pathname === "/content" ? "w-full h-full object-cover" : "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"}`}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
||||
<div className="absolute top-4 right-4 flex items-center gap-1 bg-black/50 text-white px-2 py-1 rounded-full z-10">
|
||||
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||
</div>
|
||||
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[70%] max-h-[60%]">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<i className="pi pi-book text-2xl text-[#f8f8ff]"></i>
|
||||
<div>
|
||||
<CardTitle className="text-2xl sm:text-3xl mb-2">{course.name || course.title}</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</div>
|
||||
<CardContent className="pt-6 pb-2 w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{course && course.topics && course.topics.map((topic, index) => (
|
||||
<Tag key={index} className="px-3 py-1 text-sm text-[#f8f8ff]">
|
||||
{topic}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
<p className="font-bold text-gray-300 min-w-[12%]">{lessonCount} lessons</p>
|
||||
</CardContent>
|
||||
<CardDescription className="p-6 py-2 text-base text-neutral-50/90 dark:text-neutral-900/90 overflow-hidden min-h-[4em] flex items-center"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "2"
|
||||
}}>
|
||||
{course.description || course.summary}
|
||||
</CardDescription>
|
||||
<CardFooter className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-t border-gray-700 pt-4">
|
||||
<p className="text-sm text-gray-300">{course?.published_at && course.published_at !== "" ? (
|
||||
formatTimestampToHowLongAgo(course.published_at)
|
||||
) : (
|
||||
formatTimestampToHowLongAgo(course.created_at)
|
||||
)}</p>
|
||||
<GenericButton onClick={() => router.push(`/course/${nAddress}`)} size="small" label="Start Learning" icon="pi pi-chevron-right" iconPos="right" outlined className="items-center py-2" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
@ -1,16 +1,23 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Tag } from "primereact/tag";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import Image from "next/image"
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useRouter } from "next/router";
|
||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import { Tag } from "primereact/tag";
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import { ProgressSpinner } from "primereact/progressspinner";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { defaultRelayUrls } from "@/context/NDKContext";
|
||||
|
||||
const CourseTemplate = ({ course }) => {
|
||||
export function CourseTemplate({ course }) {
|
||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: course });
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const [lessonCount, setLessonCount] = useState(0);
|
||||
const [nAddress, setNAddress] = useState(null);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
@ -21,61 +28,96 @@ const CourseTemplate = ({ course }) => {
|
||||
}
|
||||
}, [zaps, course]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course && course?.tags) {
|
||||
const lessons = course.tags.filter(tag => tag[0] === "a");
|
||||
setLessonCount(lessons.length);
|
||||
}
|
||||
}, [course]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course && course?.id) {
|
||||
const nAddress = nip19.naddrEncode({
|
||||
pubkey: course.pubkey,
|
||||
kind: course.kind,
|
||||
identifier: course.id,
|
||||
relayUrls: defaultRelayUrls
|
||||
});
|
||||
setNAddress(nAddress);
|
||||
}
|
||||
}, [course]);
|
||||
|
||||
if (!nAddress) return <ProgressSpinner />;
|
||||
|
||||
if (zapsError) return <div>Error: {zapsError}</div>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md max-tab:px-0"
|
||||
>
|
||||
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
|
||||
<div
|
||||
onClick={() => router.replace(`/course/${course.id}`)}
|
||||
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
|
||||
style={{ paddingBottom: "56.25%" }}
|
||||
>
|
||||
<Card className="overflow-hidden group hover:shadow-xl transition-all duration-300 bg-gray-800 m-2 border-none">
|
||||
<div className="relative h-48 sm:h-64">
|
||||
<Image
|
||||
alt="course thumbnail"
|
||||
src={returnImageProxy(course.image)}
|
||||
alt="Course background"
|
||||
quality={100}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
className={`${router.pathname === "/content" ? "w-full h-full object-cover" : "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start w-full mt-4">
|
||||
<h4 className="mb-1 font-bold text-lg font-blinker line-clamp-2">
|
||||
{course.name || course.title}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 line-clamp-2">{course.description || course.summary}</p>
|
||||
{course.price && course.price > 0 ? (
|
||||
<p className="text-sm text-gray-500 line-clamp-2">Price: {course.price} sats</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 line-clamp-2">Free</p>
|
||||
)}
|
||||
<div className="flex flex-row justify-between items-center mt-2">
|
||||
<p className="text-xs text-gray-400">
|
||||
{course?.published_at && course.published_at !== "" ? (
|
||||
formatTimestampToHowLongAgo(course.published_at)
|
||||
) : (
|
||||
formatTimestampToHowLongAgo(course.created_at)
|
||||
)}
|
||||
</p>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={course}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
||||
<div className="absolute top-4 right-4 flex items-center gap-1 bg-black/50 text-white px-2 py-1 rounded-full z-10">
|
||||
<ZapDisplay zapAmount={zapAmount} event={course} zapsLoading={zapsLoading && zapAmount === 0} />
|
||||
</div>
|
||||
{course?.topics && course?.topics.length > 0 && (
|
||||
<div className="flex flex-row justify-start items-center mt-2">
|
||||
{course && course.topics && course.topics.map((topic, index) => (
|
||||
<Tag key={index} value={topic} className="mr-2 text-white" />
|
||||
))}
|
||||
<CardHeader className="absolute bottom-[-8px] left-0 right-0 text-white bg-gray-800/70 w-fit rounded-lg rounded-bl-none rounded-tl-none rounded-br-none p-4 max-w-[70%] max-h-[60%]">
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<i className="pi pi-book text-2xl text-[#f8f8ff]"></i>
|
||||
<div>
|
||||
<CardTitle className="text-2xl sm:text-3xl mb-2">{course.name || course.title}</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseTemplate;
|
||||
<CardContent className="pt-6 pb-2 w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{course && course.topics && course.topics.map((topic, index) => (
|
||||
<Tag key={index} className="px-3 py-1 text-sm text-[#f8f8ff]">
|
||||
{topic}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
<p className="font-bold text-gray-300 min-w-[12%]">{lessonCount} lessons</p>
|
||||
</CardContent>
|
||||
<CardDescription className="p-6 py-2 text-base text-neutral-50/90 dark:text-neutral-900/90 overflow-hidden min-h-[4em] flex items-center"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "2"
|
||||
}}>
|
||||
{course.description || course.summary && (
|
||||
<>
|
||||
{course.description && (
|
||||
<div className="text-xl mt-4">
|
||||
{course.description.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{course.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{course.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardDescription>
|
||||
<CardFooter className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-t border-gray-700 pt-4">
|
||||
<p className="text-sm text-gray-300">{course?.published_at && course.published_at !== "" ? (
|
||||
formatTimestampToHowLongAgo(course.published_at)
|
||||
) : (
|
||||
formatTimestampToHowLongAgo(course.created_at)
|
||||
)}</p>
|
||||
<GenericButton onClick={() => router.push(`/course/${nAddress}`)} size="small" label="Start Learning" icon="pi pi-chevron-right" iconPos="right" outlined className="items-center py-2" />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
@ -79,7 +79,24 @@ export function DocumentTemplate({ document }) {
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "2"
|
||||
}}>
|
||||
{document.description || document.summary}
|
||||
{document.description || document.summary && (
|
||||
<>
|
||||
{document.description && (
|
||||
<div className="text-xl mt-4">
|
||||
{document.description.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{document.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{document.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardDescription>
|
||||
<CardFooter className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-t border-gray-700 pt-4">
|
||||
<p className="text-sm text-gray-300">{document?.published_at && document.published_at !== "" ? (
|
@ -1,78 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { Tag } from "primereact/tag";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
|
||||
const ResourceTemplate = ({ resource }) => {
|
||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: resource });
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, resource);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, resource]);
|
||||
|
||||
if (zapsError) return <div>Error: {zapsError}</div>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md max-tab:px-0"
|
||||
>
|
||||
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
|
||||
<div
|
||||
onClick={() => router.replace(`/details/${resource.id}`)}
|
||||
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
|
||||
style={{ paddingBottom: "56.25%" }}
|
||||
>
|
||||
<Image
|
||||
alt="resource thumbnail"
|
||||
src={returnImageProxy(resource.image)}
|
||||
quality={100}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start w-full mt-4">
|
||||
<h4 className="mb-1 font-bold text-lg font-blinker line-clamp-2">
|
||||
{resource.title}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 line-clamp-2">{resource.summary}</p>
|
||||
{resource.price && resource.price > 0 ? (
|
||||
<p className="text-sm text-gray-500 line-clamp-2">Price: {resource.price} sats</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 line-clamp-2">Free</p>
|
||||
)}
|
||||
<div className="flex flex-row justify-between items-center mt-2">
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatTimestampToHowLongAgo(resource.published_at)}
|
||||
</p>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={resource}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
</div>
|
||||
{resource?.topics && resource?.topics.length > 0 && (
|
||||
<div className="flex flex-row justify-start items-center mt-2">
|
||||
{resource.topics.map((topic, index) => (
|
||||
<Tag key={index} value={topic} className="mr-2 text-white" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceTemplate;
|
@ -21,13 +21,15 @@ export function VideoTemplate({ video }) {
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
useEffect(() => {
|
||||
if (video && video?.pubkey && video?.kind && video?.id) {
|
||||
const addr = nip19.naddrEncode({
|
||||
pubkey: video.pubkey,
|
||||
kind: video.kind,
|
||||
identifier: video.id,
|
||||
relayUrls: defaultRelayUrls
|
||||
})
|
||||
setNAddress(addr);
|
||||
setNAddress(addr);
|
||||
}
|
||||
}, [video]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -78,7 +80,24 @@ export function VideoTemplate({ video }) {
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: "2"
|
||||
}}>
|
||||
{video.description || video.summary}
|
||||
{video.description || video.summary && (
|
||||
<>
|
||||
{video.description && (
|
||||
<div className="text-xl mt-4">
|
||||
{video.description.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{video.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{video.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardDescription>
|
||||
<CardFooter className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-t border-gray-700 pt-4">
|
||||
<p className="text-sm text-gray-300">{video?.published_at && video.published_at !== "" ? (
|
@ -1,74 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { formatTimestampToHowLongAgo } from "@/utils/time";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { getTotalFromZaps } from "@/utils/lightning";
|
||||
import { useZapsSubscription } from "@/hooks/nostrQueries/zaps/useZapsSubscription";
|
||||
import ZapDisplay from "@/components/zaps/ZapDisplay";
|
||||
import { Tag } from "primereact/tag";
|
||||
|
||||
const WorkshopTemplate = ({ workshop }) => {
|
||||
const { zaps, zapsLoading, zapsError } = useZapsSubscription({ event: workshop });
|
||||
const [zapAmount, setZapAmount] = useState(0);
|
||||
const router = useRouter();
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
|
||||
useEffect(() => {
|
||||
if (zaps.length > 0) {
|
||||
const total = getTotalFromZaps(zaps, workshop);
|
||||
setZapAmount(total);
|
||||
}
|
||||
}, [zaps, workshop]);
|
||||
|
||||
if (zapsError) return <div>Error: {zapsError}</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md max-tab:px-0">
|
||||
<div
|
||||
onClick={() => router.replace(`/details/${workshop.id}`)}
|
||||
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"
|
||||
style={{ paddingBottom: "56.25%" }}
|
||||
>
|
||||
<Image
|
||||
alt="workshop thumbnail"
|
||||
src={returnImageProxy(workshop.image)}
|
||||
quality={100}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start w-full mt-4">
|
||||
<h4 className="mb-1 font-bold text-lg font-blinker line-clamp-2">
|
||||
{workshop.title}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 line-clamp-2">{workshop.summary}</p>
|
||||
{workshop.price && workshop.price > 0 ? (
|
||||
<p className="text-sm text-gray-500 line-clamp-2">Price: {workshop.price} sats</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 line-clamp-2">Free</p>
|
||||
)}
|
||||
<div className="flex flex-row justify-between items-center mt-2">
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatTimestampToHowLongAgo(workshop.published_at)}
|
||||
</p>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={workshop}
|
||||
zapsLoading={zapsLoading && zapAmount === 0}
|
||||
/>
|
||||
</div>
|
||||
{workshop?.topics && workshop?.topics.length > 0 && (
|
||||
<div className="flex flex-row justify-start items-center mt-2">
|
||||
{workshop.topics.map((topic, index) => (
|
||||
<Tag key={index} value={topic} className="mr-2 text-white" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkshopTemplate;
|
@ -53,7 +53,14 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
||||
)}
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{lesson?.title}</h1>
|
||||
<p className='text-xl mt-6'>{lesson?.summary}</p>
|
||||
<p className='text-xl mt-6'>{lesson?.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{lesson.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
{lesson?.additionalLinks && lesson.additionalLinks.length > 0 && (
|
||||
<div className='mt-6'>
|
||||
<h3 className='text-lg font-semibold mb-2'>External links:</h3>
|
||||
|
@ -87,7 +87,14 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className='text-xl text-gray-200 mb-4 mt-4'>{lesson.summary}</p>
|
||||
<p className='text-xl text-gray-200 mb-4 mt-4'>{lesson.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{lesson.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Image
|
||||
|
@ -275,7 +275,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
|
||||
|
||||
type = 'resource';
|
||||
break;
|
||||
case 'workshop':
|
||||
case 'video':
|
||||
if (draft?.price) {
|
||||
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
||||
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
|
||||
@ -296,7 +296,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
|
||||
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
|
||||
];
|
||||
|
||||
type = 'workshop';
|
||||
type = 'video';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
|
@ -43,7 +43,14 @@ const DraftCourseLesson = ({ lesson, course }) => {
|
||||
)}
|
||||
</div>
|
||||
<h1 className='text-4xl mt-6'>{lesson?.title}</h1>
|
||||
<p className='text-xl mt-6'>{lesson?.summary}</p>
|
||||
<p className='text-xl mt-6'>{lesson?.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{lesson.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
{lesson?.additionalLinks && lesson.additionalLinks.length > 0 && (
|
||||
<div className='mt-6'>
|
||||
<h3 className='text-lg font-semibold mb-2'>External links:</h3>
|
||||
|
@ -97,7 +97,14 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-row items-center justify-between w-full'>
|
||||
<p className='text-xl mt-4 text-gray-200'>{lesson.summary}</p>
|
||||
<p className='text-xl mt-4 text-gray-200'>{lesson.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{lesson.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={lesson}
|
||||
|
@ -135,7 +135,14 @@ const DocumentDetails = ({ processedEvent, topics, title, summary, image, price,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className='text-xl text-gray-200 mb-4 mt-4'>{summary}</p>
|
||||
<p className='text-xl text-gray-200 mb-4 mt-4'>{summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Image
|
||||
@ -162,7 +169,7 @@ const DocumentDetails = ({ processedEvent, topics, title, summary, image, price,
|
||||
{renderPaymentMessage()}
|
||||
{authorView ? (
|
||||
<div className='flex space-x-2 mt-4 sm:mt-0'>
|
||||
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||
<GenericButton onClick={() => router.push(`/details/${nAddress}/edit`)} label="Edit" severity='warning' outlined />
|
||||
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||
<GenericButton
|
||||
tooltip={`View Nostr Note`}
|
||||
|
@ -20,7 +20,14 @@ const ContentDropdownItem = ({ content, onSelect }) => {
|
||||
/>
|
||||
<div className="flex-1 max-w-[80vw]">
|
||||
<div className="text-lg text-900 font-bold">{content.title || content.name}</div>
|
||||
<div className="w-full text-sm text-600 text-wrap line-clamp-2">{content.summary || content.description}</div>
|
||||
<div className="w-full text-sm text-600 text-wrap line-clamp-2">{content.summary || content.description && (
|
||||
<div className="text-xl mt-4">
|
||||
{content.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{content.price && <div className="text-sm pt-6 text-gray-500">Price: {content.price}</div>}
|
||||
{content?.topics?.length > 0 && (
|
||||
<div className="text-sm pt-6 text-gray-500">
|
||||
|
@ -3,7 +3,9 @@ import Image from "next/image";
|
||||
import GenericButton from "@/components/buttons/GenericButton";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import { useRouter } from "next/router";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { defaultRelayUrls } from "@/context/NDKContext";
|
||||
|
||||
|
||||
const ContentListItem = (content) => {
|
||||
@ -11,17 +13,30 @@ const ContentListItem = (content) => {
|
||||
const router = useRouter();
|
||||
const isPublishedCourse = content?.kind === 30004;
|
||||
const isDraftCourse = !content?.kind && content?.draftLessons;
|
||||
const isResource = content?.kind && content?.kind === 30023;
|
||||
const isResource = content?.kind && content?.kind === 30023 || content?.kind === 30402;
|
||||
const isDraft = !content?.kind && !content?.draftLessons;
|
||||
|
||||
const handleClick = () => {
|
||||
console.log(content);
|
||||
console.log(content, "isDraftCourse", isDraftCourse, "isDraft", isDraft, "isResource", isResource, "isPublishedCourse", isPublishedCourse);
|
||||
let nAddress;
|
||||
if (isPublishedCourse) {
|
||||
router.push(`/course/${content.id}`);
|
||||
nAddress = nip19.naddrEncode({
|
||||
identifier: content.id,
|
||||
kind: content.kind,
|
||||
pubkey: content.pubkey,
|
||||
relayUrls: defaultRelayUrls
|
||||
});
|
||||
router.push(`/course/${nAddress}`);
|
||||
} else if (isDraftCourse) {
|
||||
router.push(`/course/${content.id}/draft`);
|
||||
} else if (isResource) {
|
||||
router.push(`/details/${content.id}`);
|
||||
nAddress = nip19.naddrEncode({
|
||||
identifier: content.id,
|
||||
kind: content.kind,
|
||||
pubkey: content.pubkey,
|
||||
relayUrls: defaultRelayUrls
|
||||
});
|
||||
router.push(`/details/${nAddress}`);
|
||||
} else if (isDraft) {
|
||||
router.push(`/draft/${content.id}`);
|
||||
}
|
||||
|
@ -129,7 +129,14 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
|
||||
}
|
||||
</div>
|
||||
<div className='flex flex-row items-center justify-between w-full'>
|
||||
<p className='text-xl mt-4'>{summary}</p>
|
||||
<p className='text-xl mt-4'>{summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<ZapDisplay
|
||||
zapAmount={zapAmount}
|
||||
event={processedEvent}
|
||||
@ -156,17 +163,20 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
|
||||
</div>
|
||||
{authorView ? (
|
||||
<div className='flex flex-row justify-center items-center space-x-2'>
|
||||
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
|
||||
<GenericButton onClick={() => router.push(`/details/${nAddress}/edit`)} label="Edit" severity='warning' outlined />
|
||||
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined />
|
||||
<GenericButton outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip="View Nostr Event" tooltipOptions={{ position: 'right' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-row justify-center items-center space-x-2'>
|
||||
<GenericButton outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip="View Nostr Event" tooltipOptions={{ position: 'right' }} />
|
||||
<GenericButton outlined icon="pi pi-external-link" onClick={() => window.open(`https://nostr.band/${nAddress}`, '_blank')} tooltip="View Nostr Event" tooltipOptions={{ position: paidResource ? 'left' : 'right' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end mt-4">
|
||||
{renderPaymentMessage()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'next/router';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { InputSwitch } from 'primereact/inputswitch';
|
||||
import GenericButton from '@/components/buttons/GenericButton';
|
||||
@ -11,7 +12,7 @@ import 'primeicons/primeicons.css';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
|
||||
const WorkshopForm = ({ draft = null }) => {
|
||||
const VideoForm = ({ draft = null }) => {
|
||||
const [title, setTitle] = useState(draft?.title || '');
|
||||
const [summary, setSummary] = useState(draft?.summary || '');
|
||||
const [price, setPrice] = useState(draft?.price || 0);
|
||||
@ -76,12 +77,12 @@ const WorkshopForm = ({ draft = null }) => {
|
||||
const payload = {
|
||||
title,
|
||||
summary,
|
||||
type: 'workshop',
|
||||
type: 'video',
|
||||
price: isPaidResource ? price : null,
|
||||
content: embedCode,
|
||||
image: coverImage,
|
||||
user: userResponse.data.id,
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'workshop'])],
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'video'])],
|
||||
additionalLinks: additionalLinks.filter(link => link.trim() !== ''),
|
||||
};
|
||||
|
||||
@ -92,7 +93,7 @@ const WorkshopForm = ({ draft = null }) => {
|
||||
axios[method](url, payload)
|
||||
.then(response => {
|
||||
if (response.status === 200 || response.status === 201) {
|
||||
showToast('success', 'Success', draft ? 'Workshop updated successfully.' : 'Workshop saved as draft.');
|
||||
showToast('success', 'Success', draft ? 'Video updated successfully.' : 'Video saved as draft.');
|
||||
|
||||
if (response.data?.id) {
|
||||
router.push(`/draft/${response.data.id}`);
|
||||
@ -101,7 +102,7 @@ const WorkshopForm = ({ draft = null }) => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
showToast('error', 'Error', 'Failed to save workshop. Please try again.');
|
||||
showToast('error', 'Error', 'Failed to save video. Please try again.');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -145,11 +146,11 @@ const WorkshopForm = ({ draft = null }) => {
|
||||
<InputText value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
||||
</div>
|
||||
<div className="p-inputgroup flex-1 mt-4">
|
||||
<InputText value={summary} onChange={(e) => setSummary(e.target.value)} placeholder="Summary" />
|
||||
<InputTextarea value={summary} onChange={(e) => setSummary(e.target.value)} placeholder="Summary" rows={5} cols={30} />
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4 flex-col">
|
||||
<p className="py-2">Paid Workshop</p>
|
||||
<p className="py-2">Paid Video</p>
|
||||
<InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
|
||||
{isPaidResource && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
@ -208,4 +209,4 @@ const WorkshopForm = ({ draft = null }) => {
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkshopForm;
|
||||
export default VideoForm;
|
@ -10,7 +10,7 @@ import { useToast } from '@/hooks/useToast';
|
||||
import { parseEvent } from '@/utils/nostr';
|
||||
import { useDraftsQuery } from '@/hooks/apiQueries/useDraftsQuery';
|
||||
import { useResources } from '@/hooks/nostr/useResources';
|
||||
import { useWorkshops } from '@/hooks/nostr/useWorkshops';
|
||||
import { useVideos } from '@/hooks/nostr/useVideos';
|
||||
import axios from 'axios';
|
||||
import LessonSelector from './LessonSelector';
|
||||
|
||||
@ -28,11 +28,11 @@ const CourseForm = ({ draft = null }) => {
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
const { resources, resourcesLoading, resourcesError } = useResources();
|
||||
const { workshops, workshopsLoading, workshopsError } = useWorkshops();
|
||||
const { videos, videosLoading, videosError } = useVideos();
|
||||
const { drafts, draftsLoading, draftsError } = useDraftsQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (draft && resources && workshops && drafts) {
|
||||
if (draft && resources && videos && drafts) {
|
||||
const populatedLessons = draft.draftLessons.map((lesson, index) => {
|
||||
if (lesson?.resource) {
|
||||
const matchingResource = resources.find((resource) => resource.d === lesson.resource.d);
|
||||
@ -46,32 +46,32 @@ const CourseForm = ({ draft = null }) => {
|
||||
|
||||
setLessons(populatedLessons);
|
||||
}
|
||||
}, [draft, resources, workshops, drafts]);
|
||||
}, [draft, resources, videos, drafts]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('allContent', allContent);
|
||||
}, [allContent]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('fasfsa', workshops)
|
||||
}, [workshops])
|
||||
console.log('fasfsa', videos)
|
||||
}, [videos])
|
||||
|
||||
useEffect(() => {
|
||||
if (!resourcesLoading && !workshopsLoading && !draftsLoading) {
|
||||
if (!resourcesLoading && !videosLoading && !draftsLoading) {
|
||||
let combinedContent = [];
|
||||
if (resources) {
|
||||
combinedContent = [...combinedContent, ...resources];
|
||||
}
|
||||
if (workshops) {
|
||||
console.log('workssdfsdfdsf', workshops)
|
||||
combinedContent = [...combinedContent, ...workshops];
|
||||
if (videos) {
|
||||
console.log('workssdfsdfdsf', videos)
|
||||
combinedContent = [...combinedContent, ...videos];
|
||||
}
|
||||
if (drafts) {
|
||||
combinedContent = [...combinedContent, ...drafts];
|
||||
}
|
||||
setAllContent(combinedContent);
|
||||
}
|
||||
}, [resources, workshops, drafts, resourcesLoading, workshopsLoading, draftsLoading]);
|
||||
}, [resources, videos, drafts, resourcesLoading, videosLoading, draftsLoading]);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
@ -156,22 +156,22 @@ const CourseForm = ({ draft = null }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewWorkshopCreate = async (newWorkshop) => {
|
||||
const handleNewVideoCreate = async (newVideo) => {
|
||||
try {
|
||||
console.log('newWorkshop', newWorkshop);
|
||||
const response = await axios.post('/api/drafts', newWorkshop);
|
||||
console.log('newVideo', newVideo);
|
||||
const response = await axios.post('/api/drafts', newVideo);
|
||||
console.log('response', response);
|
||||
const createdWorkshop = response.data;
|
||||
setAllContent(prevContent => [...prevContent, createdWorkshop]);
|
||||
return createdWorkshop;
|
||||
const createdVideo = response.data;
|
||||
setAllContent(prevContent => [...prevContent, createdVideo]);
|
||||
return createdVideo;
|
||||
} catch (error) {
|
||||
console.error('Error creating workshop draft:', error);
|
||||
showToast('error', 'Error', 'Failed to create workshop draft');
|
||||
console.error('Error creating video draft:', error);
|
||||
showToast('error', 'Error', 'Failed to create video draft');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (resourcesLoading || workshopsLoading || draftsLoading) {
|
||||
if (resourcesLoading || videosLoading || draftsLoading) {
|
||||
return <ProgressSpinner />;
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@ const CourseForm = ({ draft = null }) => {
|
||||
setLessons={setLessons}
|
||||
allContent={allContent}
|
||||
onNewResourceCreate={handleNewResourceCreate}
|
||||
onNewWorkshopCreate={handleNewWorkshopCreate}
|
||||
onNewVideoCreate={handleNewVideoCreate}
|
||||
/>
|
||||
<div className="mt-4 flex-col w-full">
|
||||
{topics.map((topic, index) => (
|
||||
|
@ -4,14 +4,14 @@ import GenericButton from '@/components/buttons/GenericButton';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Accordion, AccordionTab } from 'primereact/accordion';
|
||||
import EmbeddedResourceForm from '@/components/forms/course/embedded/EmbeddedResourceForm';
|
||||
import EmbeddedWorkshopForm from '@/components/forms/course/embedded/EmbeddedWorkshopForm';
|
||||
import EmbeddedVideoForm from '@/components/forms/course/embedded/EmbeddedVideoForm';
|
||||
import ContentDropdownItem from '@/components/content/dropdowns/ContentDropdownItem';
|
||||
import SelectedContentItem from '@/components/content/SelectedContentItem';
|
||||
import { parseEvent } from '@/utils/nostr';
|
||||
|
||||
const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewResourceCreate, onNewWorkshopCreate }) => {
|
||||
const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewResourceCreate, onNewVideoCreate }) => {
|
||||
const [showResourceForm, setShowResourceForm] = useState(false);
|
||||
const [showWorkshopForm, setShowWorkshopForm] = useState(false);
|
||||
const [showVideoForm, setShowVideoForm] = useState(false);
|
||||
const [contentOptions, setContentOptions] = useState([]);
|
||||
const [openTabs, setOpenTabs] = useState([]);
|
||||
|
||||
@ -52,7 +52,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
value: content
|
||||
}));
|
||||
|
||||
const draftWorkshopOptions = filteredContent.filter(content => content?.topics.includes('workshop') && !content.kind).map(content => ({
|
||||
const draftVideoOptions = filteredContent.filter(content => content?.topics.includes('video') && !content.kind).map(content => ({
|
||||
label: content.title,
|
||||
value: content
|
||||
}));
|
||||
@ -62,7 +62,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
value: content
|
||||
}));
|
||||
|
||||
const workshopOptions = filteredContent.filter(content => content?.type === "workshop" && content.kind).map(content => ({
|
||||
const videoOptions = filteredContent.filter(content => content?.type === "video" && content.kind).map(content => ({
|
||||
label: content.title,
|
||||
value: content
|
||||
}));
|
||||
@ -73,16 +73,16 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
items: draftResourceOptions
|
||||
},
|
||||
{
|
||||
label: 'Draft Workshops',
|
||||
items: draftWorkshopOptions
|
||||
label: 'Draft Videos',
|
||||
items: draftVideoOptions
|
||||
},
|
||||
{
|
||||
label: 'Published Resources',
|
||||
items: resourceOptions
|
||||
},
|
||||
{
|
||||
label: 'Published Workshops',
|
||||
items: workshopOptions
|
||||
label: 'Published Videos',
|
||||
items: videoOptions
|
||||
}
|
||||
]);
|
||||
};
|
||||
@ -124,12 +124,12 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewWorkshopSave = async (newWorkshop) => {
|
||||
console.log('newWorkshop', newWorkshop);
|
||||
const createdWorkshop = await onNewWorkshopCreate(newWorkshop);
|
||||
if (createdWorkshop) {
|
||||
handleContentSelect(createdWorkshop, lessons.length);
|
||||
setShowWorkshopForm(false);
|
||||
const handleNewVideoSave = async (newVideo) => {
|
||||
console.log('newVideo', newVideo);
|
||||
const createdVideo = await onNewVideoCreate(newVideo);
|
||||
if (createdVideo) {
|
||||
handleContentSelect(createdVideo, lessons.length);
|
||||
setShowVideoForm(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -168,7 +168,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
{lesson.id ? null : (
|
||||
<>
|
||||
<GenericButton label="New Resource" onClick={(e) => {e.preventDefault(); setShowResourceForm(true)}} className="mr-2" />
|
||||
<GenericButton label="New Workshop" onClick={(e) => {e.preventDefault(); setShowWorkshopForm(true)}} className="mr-2" />
|
||||
<GenericButton label="New Video" onClick={(e) => {e.preventDefault(); setShowVideoForm(true)}} className="mr-2" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -194,8 +194,8 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
|
||||
<EmbeddedResourceForm onSave={handleNewResourceSave} isPaid={isPaidCourse} />
|
||||
</Dialog>
|
||||
|
||||
<Dialog className='w-full max-w-screen-md' visible={showWorkshopForm} onHide={() => setShowWorkshopForm(false)} header="Create New Workshop">
|
||||
<EmbeddedWorkshopForm onSave={handleNewWorkshopSave} isPaid={isPaidCourse} />
|
||||
<Dialog className='w-full max-w-screen-md' visible={showVideoForm} onHide={() => setShowVideoForm(false)} header="Create New Video">
|
||||
<EmbeddedVideoForm onSave={handleNewVideoSave} isPaid={isPaidCourse} />
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ import 'primeicons/primeicons.css';
|
||||
import { Tooltip } from 'primereact/tooltip';
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
|
||||
const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
|
||||
const EmbeddedVideoForm = ({ draft = null, onSave, isPaid }) => {
|
||||
const [title, setTitle] = useState(draft?.title || '');
|
||||
const [summary, setSummary] = useState(draft?.summary || '');
|
||||
const [price, setPrice] = useState(draft?.price || 0);
|
||||
@ -62,11 +62,11 @@ const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
|
||||
const payload = {
|
||||
title,
|
||||
summary,
|
||||
type: 'workshop',
|
||||
type: 'vidoe',
|
||||
price: isPaidResource ? price : null,
|
||||
content: embedCode,
|
||||
image: coverImage,
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'workshop'])],
|
||||
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'video'])],
|
||||
additionalLinks: additionalLinks.filter(link => link.trim() !== ''),
|
||||
user: user?.id || user?.pubkey
|
||||
};
|
||||
@ -74,10 +74,10 @@ const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
|
||||
if (onSave) {
|
||||
try {
|
||||
await onSave(payload);
|
||||
showToast('success', 'Success', draft ? 'Workshop updated successfully.' : 'Workshop created successfully.');
|
||||
showToast('success', 'Success', draft ? 'Video updated successfully.' : 'Video created successfully.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('error', 'Error', 'Failed to save workshop. Please try again.');
|
||||
showToast('error', 'Error', 'Failed to save video. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -125,7 +125,7 @@ const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
|
||||
</div>
|
||||
|
||||
<div className="p-inputgroup flex-1 mt-4 flex-col">
|
||||
<p className="py-2">Paid Workshop</p>
|
||||
<p className="py-2">Paid Video</p>
|
||||
<InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
|
||||
{isPaidResource && (
|
||||
<div className="p-inputgroup flex-1 py-4">
|
||||
@ -184,4 +184,4 @@ const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
|
||||
);
|
||||
}
|
||||
|
||||
export default EmbeddedWorkshopForm;
|
||||
export default EmbeddedVideoForm;
|
@ -4,7 +4,7 @@ import GenericButton from "@/components/buttons/GenericButton";
|
||||
import MenuTab from "@/components/menutab/MenuTab";
|
||||
import { useCourses } from "@/hooks/nostr/useCourses";
|
||||
import { useResources } from "@/hooks/nostr/useResources";
|
||||
import { useWorkshops } from "@/hooks/nostr/useWorkshops";
|
||||
import { useVideos } from "@/hooks/nostr/useVideos";
|
||||
import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery";
|
||||
import { useCourseDraftsQuery } from "@/hooks/apiQueries/useCourseDraftsQuery";
|
||||
import { useContentIdsQuery } from "@/hooks/apiQueries/useContentIdsQuery";
|
||||
@ -32,7 +32,7 @@ const UserContent = () => {
|
||||
const {ndk, addSigner} = useNDKContext();
|
||||
const { courses, coursesLoading, coursesError } = useCourses();
|
||||
const { resources, resourcesLoading, resourcesError } = useResources();
|
||||
const { workshops, workshopsLoading, workshopsError } = useWorkshops();
|
||||
const { videos, videosLoading, videosError } = useVideos();
|
||||
const { courseDrafts, courseDraftsLoading, courseDraftsError } = useCourseDraftsQuery();
|
||||
const { drafts, draftsLoading, draftsError } = useDraftsQuery();
|
||||
const { contentIds, contentIdsLoading, contentIdsError, refetchContentIds } = useContentIdsQuery();
|
||||
@ -52,7 +52,7 @@ const UserContent = () => {
|
||||
{ label: "Drafts", icon: "pi pi-file-edit" },
|
||||
{ label: "Draft Courses", icon: "pi pi-book" },
|
||||
{ label: "Resources", icon: "pi pi-file" },
|
||||
{ label: "Workshops", icon: "pi pi-video" },
|
||||
{ label: "Videos", icon: "pi pi-video" },
|
||||
{ label: "Courses", icon: "pi pi-desktop" },
|
||||
];
|
||||
|
||||
@ -73,7 +73,7 @@ const UserContent = () => {
|
||||
console.log('uniqueEvents', uniqueEvents)
|
||||
return Array.from(uniqueEvents);
|
||||
} catch (error) {
|
||||
console.error('Error fetching workshops from NDK:', error);
|
||||
console.error('Error fetching videos from NDK:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@ -100,7 +100,7 @@ const UserContent = () => {
|
||||
case 3:
|
||||
return resources?.map(parseEvent) || [];
|
||||
case 3:
|
||||
return workshops?.map(parseEvent) || [];
|
||||
return videos?.map(parseEvent) || [];
|
||||
case 4:
|
||||
return courses?.map(parseEvent) || [];
|
||||
default:
|
||||
@ -110,10 +110,10 @@ const UserContent = () => {
|
||||
|
||||
setContent(getContentByIndex(activeIndex));
|
||||
}
|
||||
}, [activeIndex, isClient, drafts, resources, workshops, courses, publishedContent, courseDrafts])
|
||||
}, [activeIndex, isClient, drafts, resources, videos, courses, publishedContent, courseDrafts])
|
||||
|
||||
const isLoading = coursesLoading || resourcesLoading || workshopsLoading || draftsLoading || contentIdsLoading || courseDraftsLoading;
|
||||
const isError = coursesError || resourcesError || workshopsError || draftsError || contentIdsError || courseDraftsError;
|
||||
const isLoading = coursesLoading || resourcesLoading || videosLoading || draftsLoading || contentIdsLoading || courseDraftsLoading;
|
||||
const isError = coursesError || resourcesError || videosError || draftsError || contentIdsError || courseDraftsError;
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
|
@ -133,8 +133,8 @@ const Sidebar = ({ course = false }) => {
|
||||
<div onClick={() => router.push('/content?tag=courses')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content?tag=courses') ? 'bg-gray-700' : ''}`}>
|
||||
<p className="pl-3 rounded-md font-bold text-lg"><i className="pi pi-desktop text-sm pr-1"></i> Courses</p>
|
||||
</div>
|
||||
<div onClick={() => router.push('/content?tag=workshops')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content?tag=workshops') ? 'bg-gray-700' : ''}`}>
|
||||
<p className="pl-3 rounded-md font-bold text-lg"><i className="pi pi-video text-sm pr-1"></i> Workshops</p>
|
||||
<div onClick={() => router.push('/content?tag=videos')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content?tag=videos') ? 'bg-gray-700' : ''}`}>
|
||||
<p className="pl-3 rounded-md font-bold text-lg"><i className="pi pi-video text-sm pr-1"></i> Videos</p>
|
||||
</div>
|
||||
<div onClick={() => router.push('/content?tag=resources')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content?tag=resources') ? 'bg-gray-700' : ''}`}>
|
||||
<p className="pl-3 rounded-md font-bold text-lg"><i className="pi pi-file text-sm pr-1"></i> Resources</p>
|
||||
|
@ -4,12 +4,12 @@ import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
|
||||
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||
|
||||
export function useWorkshops() {
|
||||
export function useVideos() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [workshops, setWorkshops] = useState();
|
||||
const [videos, setVideos] = useState();
|
||||
// Add new state variables for loading and error
|
||||
const [workshopsLoading, setWorkshopsLoading] = useState(false);
|
||||
const [workshopsError, setWorkshopsError] = useState(null);
|
||||
const [videosLoading, setVideosLoading] = useState(false);
|
||||
const [videosError, setVideosError] = useState(null);
|
||||
|
||||
const { contentIds } = useContentIdsQuery()
|
||||
const {ndk, addSigner} = useNDKContext();
|
||||
@ -19,18 +19,18 @@ export function useWorkshops() {
|
||||
}, []);
|
||||
|
||||
const hasRequiredProperties = (event, contentIds) => {
|
||||
const hasWorkshop = event.tags.some(([tag, value]) => tag === "t" && value === "workshop");
|
||||
const hasVideo = event.tags.some(([tag, value]) => tag === "t" && value === "video");
|
||||
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
|
||||
return hasWorkshop && hasId;
|
||||
return hasVideo && hasId;
|
||||
};
|
||||
|
||||
const fetchWorkshopsFromNDK = async () => {
|
||||
setWorkshopsLoading(true);
|
||||
setWorkshopsError(null);
|
||||
const fetchVideosFromNDK = async () => {
|
||||
setVideosLoading(true);
|
||||
setVideosError(null);
|
||||
try {
|
||||
if (!contentIds || contentIds.length === 0) {
|
||||
console.log('No content IDs found');
|
||||
setWorkshopsLoading(false);
|
||||
setVideosLoading(false);
|
||||
return []; // Return early if no content IDs are found
|
||||
}
|
||||
|
||||
@ -41,30 +41,30 @@ export function useWorkshops() {
|
||||
|
||||
if (events && events.size > 0) {
|
||||
const eventsArray = Array.from(events);
|
||||
const workshops = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
setWorkshopsLoading(false);
|
||||
return workshops;
|
||||
const videos = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
setVideosLoading(false);
|
||||
return videos;
|
||||
}
|
||||
setWorkshopsLoading(false);
|
||||
setVideosLoading(false);
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching workshops from NDK:', error);
|
||||
setWorkshopsError(error);
|
||||
setWorkshopsLoading(false);
|
||||
console.error('Error fetching videos from NDK:', error);
|
||||
setVideosError(error);
|
||||
setVideosLoading(false);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isClient && contentIds) {
|
||||
fetchWorkshopsFromNDK().then(fetchedWorkshops => {
|
||||
if (fetchedWorkshops && fetchedWorkshops.length > 0) {
|
||||
console.log('fetchedworkshops', fetchedWorkshops)
|
||||
setWorkshops(fetchedWorkshops);
|
||||
fetchVideosFromNDK().then(fetchedVideos => {
|
||||
if (fetchedVideos && fetchedVideos.length > 0) {
|
||||
console.log('fetchedvideos', fetchedVideos)
|
||||
setVideos(fetchedVideos);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isClient, contentIds]);
|
||||
|
||||
return { workshops, workshopsLoading, workshopsError };
|
||||
return { videos, videosLoading, videosError };
|
||||
}
|
@ -24,7 +24,7 @@ const fetchAllContentFromNDK = async (ids) => {
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching workshops from NDK:', error);
|
||||
console.error('Error fetching videos from NDK:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
|
||||
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
|
||||
|
||||
export function useWorkshopsQuery() {
|
||||
export function useVideosQuery() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const {ndk, addSigner} = useNDKContext();
|
||||
|
||||
@ -14,12 +14,12 @@ export function useWorkshopsQuery() {
|
||||
}, []);
|
||||
|
||||
const hasRequiredProperties = (event, contentIds) => {
|
||||
const hasWorkshop = event.tags.some(([tag, value]) => tag === "t" && value === "workshop");
|
||||
const hasVideo = event.tags.some(([tag, value]) => tag === "t" && value === "video");
|
||||
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
|
||||
return hasWorkshop && hasId;
|
||||
return hasVideo && hasId;
|
||||
};
|
||||
|
||||
const fetchWorkshopsFromNDK = async () => {
|
||||
const fetchVideosFromNDK = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/content/all`);
|
||||
const contentIds = response.data;
|
||||
@ -36,23 +36,23 @@ export function useWorkshopsQuery() {
|
||||
|
||||
if (events && events.size > 0) {
|
||||
const eventsArray = Array.from(events);
|
||||
const workshops = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
return workshops;
|
||||
const videos = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
|
||||
return videos;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching workshops from NDK:', error);
|
||||
console.error('Error fetching videos from NDK:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const { data: workshops, isLoading: workshopsLoading, error: workshopsError, refetch: refetchWorkshops } = useQuery({
|
||||
queryKey: ['workshops', isClient],
|
||||
queryFn: fetchWorkshopsFromNDK,
|
||||
const { data: videos, isLoading: videosLoading, error: videosError, refetch: refetchVideos } = useQuery({
|
||||
queryKey: ['videos', isClient],
|
||||
queryFn: fetchVideosFromNDK,
|
||||
// staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
// refetchInterval: 1000 * 60 * 30, // 30 minutes
|
||||
enabled: isClient,
|
||||
});
|
||||
|
||||
return { workshops, workshopsLoading, workshopsError, refetchWorkshops };
|
||||
return { videos, videosLoading, videosError, refetchVideos };
|
||||
}
|
@ -61,8 +61,8 @@ const AboutPage = () => {
|
||||
description={
|
||||
<ul className="list-disc list-inside ml-6 space-y-2">
|
||||
<li><span className="font-bold">Resources:</span> Markdown documents posted as NIP-23 long-form events on Nostr.</li>
|
||||
<li><span className="font-bold">Workshops:</span> Enhanced markdown files with rich media support, including embedded videos, also saved as NIP-23 events.</li>
|
||||
<li><span className="font-bold">Courses:</span> Nostr lists that combine multiple resources and workshops into a structured learning path.</li>
|
||||
<li><span className="font-bold">Videos:</span> Enhanced markdown files with rich media support, including embedded videos, also saved as NIP-23 events.</li>
|
||||
<li><span className="font-bold">Courses:</span> Nostr lists that combine multiple resources and videos into a structured learning path.</li>
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo } from 'react';
|
||||
import GenericCarousel from '@/components/content/carousels/GenericCarousel';
|
||||
import { parseEvent, parseCourseEvent } from '@/utils/nostr';
|
||||
import { useResources } from '@/hooks/nostr/useResources';
|
||||
import { useWorkshops } from '@/hooks/nostr/useWorkshops';
|
||||
import { useVideos } from '@/hooks/nostr/useVideos';
|
||||
import { useCourses } from '@/hooks/nostr/useCourses';
|
||||
import { TabMenu } from 'primereact/tabmenu';
|
||||
import 'primeicons/primeicons.css';
|
||||
@ -18,7 +18,7 @@ const MenuTab = ({ items, selectedTopic, onTabChange }) => {
|
||||
let icon = 'pi pi-tag';
|
||||
if (item === 'All') icon = 'pi pi-eye';
|
||||
else if (item === 'Resources') icon = 'pi pi-file';
|
||||
else if (item === 'Workshops') icon = 'pi pi-video';
|
||||
else if (item === 'Videos') icon = 'pi pi-video';
|
||||
else if (item === 'Courses') icon = 'pi pi-desktop';
|
||||
|
||||
const queryParam = item === 'all' ? '' : `?tag=${item.toLowerCase()}`;
|
||||
@ -68,11 +68,11 @@ const MenuTab = ({ items, selectedTopic, onTabChange }) => {
|
||||
const ContentPage = () => {
|
||||
const router = useRouter();
|
||||
const { resources, resourcesLoading } = useResources();
|
||||
const { workshops, workshopsLoading } = useWorkshops();
|
||||
const { videos, videosLoading } = useVideos();
|
||||
const { courses, coursesLoading } = useCourses();
|
||||
|
||||
const [processedResources, setProcessedResources] = useState([]);
|
||||
const [processedWorkshops, setProcessedWorkshops] = useState([]);
|
||||
const [processedVideos, setProcessedVideos] = useState([]);
|
||||
const [processedCourses, setProcessedCourses] = useState([]);
|
||||
const [allContent, setAllContent] = useState([]);
|
||||
const [allTopics, setAllTopics] = useState([]);
|
||||
@ -99,11 +99,11 @@ const ContentPage = () => {
|
||||
}, [resources, resourcesLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workshops && !workshopsLoading) {
|
||||
const processedWorkshops = workshops.map(workshop => ({...parseEvent(workshop), type: 'workshop'}));
|
||||
setProcessedWorkshops(processedWorkshops);
|
||||
if (videos && !videosLoading) {
|
||||
const processedVideos = videos.map(video => ({...parseEvent(video), type: 'video'}));
|
||||
setProcessedVideos(processedVideos);
|
||||
}
|
||||
}, [workshops, workshopsLoading]);
|
||||
}, [videos, videosLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (courses && !coursesLoading) {
|
||||
@ -113,11 +113,11 @@ const ContentPage = () => {
|
||||
}, [courses, coursesLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const allContent = [...processedResources, ...processedWorkshops, ...processedCourses];
|
||||
const allContent = [...processedResources, ...processedVideos, ...processedCourses];
|
||||
setAllContent(allContent);
|
||||
|
||||
const uniqueTopics = new Set(allContent.map(item => item.topics).flat());
|
||||
const priorityItems = ['All', 'Courses', 'Workshops', 'Resources'];
|
||||
const priorityItems = ['All', 'Courses', 'Videos', 'Resources'];
|
||||
const otherTopics = Array.from(uniqueTopics).filter(topic => !priorityItems.includes(topic));
|
||||
const combinedTopics = [...priorityItems.slice(1), ...otherTopics];
|
||||
setAllTopics(combinedTopics);
|
||||
@ -125,13 +125,13 @@ const ContentPage = () => {
|
||||
if (selectedTopic) {
|
||||
filterContent(selectedTopic, allContent);
|
||||
}
|
||||
}, [processedResources, processedWorkshops, processedCourses]);
|
||||
}, [processedResources, processedVideos, processedCourses]);
|
||||
|
||||
const filterContent = (topic, content) => {
|
||||
let filtered = content;
|
||||
if (topic !== 'All') {
|
||||
const topicLower = topic.toLowerCase();
|
||||
if (['courses', 'workshops', 'resources'].includes(topicLower)) {
|
||||
if (['courses', 'videos', 'resources'].includes(topicLower)) {
|
||||
filtered = content.filter(item => item.type === topicLower.slice(0, -1));
|
||||
} else {
|
||||
filtered = content.filter(item => item.topics && item.topics.includes(topic.toLowerCase()));
|
||||
@ -166,7 +166,7 @@ const ContentPage = () => {
|
||||
<h1 className="text-3xl font-bold mb-4 ml-1">All Content</h1>
|
||||
</div>
|
||||
<MenuTab
|
||||
items={['Courses', 'Workshops', 'Resources', ...allTopics.filter(topic => !['Courses', 'Workshops', 'Resources'].includes(topic))]}
|
||||
items={['Courses', 'Videos', 'Resources', ...allTopics.filter(topic => !['Courses', 'Videos', 'Resources'].includes(topic))]}
|
||||
selectedTopic={selectedTopic}
|
||||
onTabChange={handleTopicChange}
|
||||
className="max-w-[90%] mx-auto"
|
||||
|
@ -225,7 +225,7 @@ const Course = () => {
|
||||
}
|
||||
>
|
||||
<div className="w-full py-4 rounded-b-lg">
|
||||
{lesson.type === 'workshop' ?
|
||||
{lesson.type === 'video' ?
|
||||
<VideoLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} /> :
|
||||
<DocumentLesson lesson={lesson} course={course} decryptionPerformed={decryptionPerformed} isPaid={paidCourse} />
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import MenuTab from "@/components/menutab/MenuTab";
|
||||
import ResourceForm from "@/components/forms/ResourceForm";
|
||||
import WorkshopForm from "@/components/forms/WorkshopForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import { useIsAdmin } from "@/hooks/useIsAdmin";
|
||||
import { useRouter } from "next/router";
|
||||
@ -13,7 +13,7 @@ const Create = () => {
|
||||
const router = useRouter();
|
||||
const homeItems = [
|
||||
{ label: 'Resource', icon: 'pi pi-book' },
|
||||
{ label: 'Workshop', icon: 'pi pi-video' },
|
||||
{ label: 'Video', icon: 'pi pi-video' },
|
||||
{ label: 'Course', icon: 'pi pi-desktop' }
|
||||
];
|
||||
|
||||
@ -30,8 +30,8 @@ const Create = () => {
|
||||
switch (homeItems[activeIndex].label) {
|
||||
case 'Course':
|
||||
return <CourseForm />;
|
||||
case 'Workshop':
|
||||
return <WorkshopForm />;
|
||||
case 'Video':
|
||||
return <VideoForm />;
|
||||
case 'Resource':
|
||||
return <ResourceForm />;
|
||||
default:
|
||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { parseEvent } from "@/utils/nostr";
|
||||
import ResourceForm from "@/components/forms/ResourceForm";
|
||||
import WorkshopForm from "@/components/forms/WorkshopForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import { useNDKContext } from "@/context/NDKContext";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
@ -40,7 +40,7 @@ export default function Edit() {
|
||||
<div className="w-[80vw] max-w-[80vw] mx-auto my-8 flex flex-col justify-center">
|
||||
<h2 className="text-center mb-8">Edit Published Event</h2>
|
||||
{event?.topics.includes('course') && <CourseForm draft={event} isPublished />}
|
||||
{event?.topics.includes('workshop') && <WorkshopForm draft={event} isPublished />}
|
||||
{!event?.topics.includes('video') && <VideoForm draft={event} isPublished />}
|
||||
{event?.topics.includes('resource') && <ResourceForm draft={event} isPublished />}
|
||||
</div>
|
||||
);
|
||||
|
@ -205,7 +205,7 @@ export default function Details() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{processedEvent && processedEvent.type !== "workshop" ? (
|
||||
{processedEvent && processedEvent.type !== "video" ? (
|
||||
<DocumentDetails
|
||||
processedEvent={processedEvent}
|
||||
topics={processedEvent.topics}
|
||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import axios from "axios";
|
||||
import ResourceForm from "@/components/forms/ResourceForm";
|
||||
import WorkshopForm from "@/components/forms/WorkshopForm";
|
||||
import VideoForm from "@/components/forms/VideoForm";
|
||||
import CourseForm from "@/components/forms/course/CourseForm";
|
||||
import { useIsAdmin } from "@/hooks/useIsAdmin";
|
||||
|
||||
@ -37,7 +37,7 @@ const Edit = () => {
|
||||
<div className="w-full min-bottom-bar:w-[86vw] max-sidebar:w-[100vw] px-8 mx-auto my-8 flex flex-col justify-center">
|
||||
<h2 className="text-center mb-8">Edit Draft</h2>
|
||||
{draft?.type === 'course' && <CourseForm draft={draft} />}
|
||||
{draft?.type === 'workshop' && <WorkshopForm draft={draft} />}
|
||||
{draft?.type === 'video' && <VideoForm draft={draft} />}
|
||||
{draft?.type === 'resource' && <ResourceForm draft={draft} />}
|
||||
</div>
|
||||
);
|
||||
|
@ -210,7 +210,7 @@ export default function Draft() {
|
||||
|
||||
type = 'resource';
|
||||
break;
|
||||
case 'workshop':
|
||||
case 'video':
|
||||
if (draft?.price) {
|
||||
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
|
||||
encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY, process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, draft.content);
|
||||
@ -245,7 +245,7 @@ export default function Draft() {
|
||||
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
|
||||
];
|
||||
|
||||
type = 'workshop';
|
||||
type = 'video';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
@ -269,7 +269,14 @@ export default function Draft() {
|
||||
})}
|
||||
</div>
|
||||
<h1 className='text-4xl mt-4'>{draft?.title}</h1>
|
||||
<p className='text-xl mt-4'>{draft?.summary}</p>
|
||||
<p className='text-xl mt-4'>{draft?.summary && (
|
||||
<div className="text-xl mt-4">
|
||||
{draft.summary.split('\n').map((line, index) => (
|
||||
<p key={index}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
{draft?.price && (
|
||||
<p className='text-lg mt-4'>Price: {draft.price}</p>
|
||||
)}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import CoursesCarousel from '@/components/content/carousels/CoursesCarousel';
|
||||
import WorkshopsCarousel from '@/components/content/carousels/WorkshopsCarousel';
|
||||
import VideosCarousel from '@/components/content/carousels/VideosCarousel';
|
||||
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
|
||||
import InteractivePromotionalCarousel from '@/components/content/carousels/InteractivePromotionalCarousel';
|
||||
|
||||
@ -17,7 +17,7 @@ export default function Home() {
|
||||
<main>
|
||||
<InteractivePromotionalCarousel />
|
||||
<CoursesCarousel />
|
||||
<WorkshopsCarousel />
|
||||
<VideosCarousel />
|
||||
<ResourcesCarousel />
|
||||
</main>
|
||||
</>
|
||||
|
@ -104,8 +104,8 @@ export const parseEvent = (event) => {
|
||||
eventData.d = tag[1];
|
||||
break;
|
||||
case 't':
|
||||
if (tag[1] === 'workshop') {
|
||||
eventData.type = 'workshop';
|
||||
if (tag[1] === 'video') {
|
||||
eventData.type = 'video';
|
||||
} else if (tag[1] !== "plebdevs") {
|
||||
eventData.topics.push(tag[1]);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user