Rename from workshops to videos

This commit is contained in:
austinkelsay 2024-09-15 13:27:37 -05:00
parent 127e7d9029
commit aa13faaf44
40 changed files with 436 additions and 543 deletions

View File

@ -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';

View File

@ -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} />;

View File

@ -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 (

View File

@ -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';

View 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}
/>
</>
);
}

View File

@ -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}
/>
</>
);
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 !== "" ? (

View File

@ -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;

View File

@ -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 !== "" ? (

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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}

View File

@ -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`}

View File

@ -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">

View File

@ -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}`);
}

View File

@ -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>
)

View File

@ -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;

View File

@ -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) => (

View File

@ -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>
);

View File

@ -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;

View File

@ -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">

View File

@ -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>

View File

@ -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 };
}

View File

@ -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 [];
}
};

View File

@ -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 };
}

View File

@ -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>
}
/>

View File

@ -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"

View File

@ -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} />
}

View File

@ -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:

View File

@ -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>
);

View File

@ -205,7 +205,7 @@ export default function Details() {
return (
<div>
{processedEvent && processedEvent.type !== "workshop" ? (
{processedEvent && processedEvent.type !== "video" ? (
<DocumentDetails
processedEvent={processedEvent}
topics={processedEvent.topics}

View File

@ -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>
);

View File

@ -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>
)}

View File

@ -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>
</>

View File

@ -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]);
}