Changed resource to document on the frontend

This commit is contained in:
austinkelsay 2024-09-15 15:15:58 -05:00
parent aa13faaf44
commit cb3f124c3a
31 changed files with 203 additions and 203 deletions

View File

@ -15,7 +15,7 @@ const BottomBar = () => {
<i className="pi pi-home text-2xl" />
</div>
<div onClick={() => router.push('/content?tag=all')} className={`hover:bg-gray-700 cursor-pointer px-4 py-3 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-video text-2xl" />
<i className="pi pi-play-circle text-2xl" />
</div>
<div onClick={() => router.push('/feed?channel=global')} className={`hover:bg-gray-700 cursor-pointer px-4 py-3 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-comments text-2xl" />

View File

@ -4,6 +4,7 @@ import { useImageProxy } from "@/hooks/useImageProxy";
import { formatUnixTimestamp } from "@/utils/time";
import GenericButton from "@/components/buttons/GenericButton";
const SelectedContentItem = ({ content, onRemove }) => {
console.log('content:', content);
const { returnImageProxy } = useImageProxy();
return (

View File

@ -1,10 +1,9 @@
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/templates/DocumentTemplate';
import TemplateSkeleton from '@/components/content/carousels/skeletons/TemplateSkeleton';
import { useResources } from '@/hooks/nostr/useResources';
import { useDocuments } from '@/hooks/nostr/useDocuments';
const responsiveOptions = [
{
@ -24,44 +23,42 @@ const responsiveOptions = [
}
];
export default function ResourcesCarousel() {
const [processedResources, setProcessedResources] = useState([]);
const { resources, resourcesLoading, resourcesError } = useResources()
export default function DocumentsCarousel() {
const [processedDocuments, setProcessedDocuments] = useState([]);
const { documents, documentsLoading, documentsError } = useDocuments()
useEffect(() => {
const fetch = async () => {
try {
if (resources && resources.length > 0) {
const processedResources = resources.map(resource => parseEvent(resource));
if (documents && documents.length > 0) {
const processedDocuments = documents.map(document => parseEvent(document));
// Sort resources by created_at in descending order (most recent first)
const sortedResources = processedResources.sort((a, b) => b.created_at - a.created_at);
// Sort documents by created_at in descending order (most recent first)
const sortedDocuments = processedDocuments.sort((a, b) => b.created_at - a.created_at);
console.log("Sorted resources:", sortedResources);
setProcessedResources(sortedResources);
setProcessedDocuments(sortedDocuments);
} else {
console.log('No resources fetched or empty array returned');
console.log('No documents fetched or empty array returned');
}
} catch (error) {
console.error('Error fetching resources:', error);
console.error('Error fetching documents:', error);
}
};
fetch();
}, [resources]);
}, [documents]);
if (resourcesError) {
return <div>Error: {resourcesError.message}</div>
if (documentsError) {
return <div>Error: {documentsError.message}</div>
}
return (
<>
<h3 className="ml-[6%] mt-4">Resources</h3>
<h3 className="ml-[6%] mt-4">Documents</h3>
<Carousel
value={resourcesLoading || !processedResources.length ? [{}, {}, {}] : [...processedResources]}
value={documentsLoading || !processedDocuments.length ? [{}, {}, {}] : [...processedDocuments]}
numVisible={2}
itemTemplate={(item) =>
processedResources.length > 0 ?
processedDocuments.length > 0 ?
<DocumentTemplate key={item.id} document={item} /> :
<TemplateSkeleton key={Math.random()} />
}

View File

@ -64,7 +64,7 @@ export default function GenericCarousel({items, selectedTopic, title}) {
value={carouselItems}
itemTemplate={(item) => {
if (carouselItems.length > 0) {
if (item.type === 'resource') {
if (item.type === 'document') {
return <DocumentTemplate key={item.id} document={item} />;
} else if (item.type === 'video') {
return <VideoTemplate key={item.id} video={item} />;

View File

@ -33,9 +33,9 @@ const promotions = [
},
{
id: 4,
category: "RESOURCES",
category: "DOCUMENTS",
title: "In-depth Resources and Documentation",
description: "Access our extensive library of resources, including guides, documentation, and best practices for Bitcoin development.",
description: "Access our extensive library of documents, including guides, resources, and best practices for Bitcoin development.",
icon: "pi pi-file",
image: "https://img.freepik.com/free-photo/programming-background-with-person-working-with-codes-computer_23-2150010125.jpg",
},
@ -101,9 +101,9 @@ const InteractivePromotionalCarousel = () => {
return (
<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":
case "DOCUMENTS":
return (
<GenericButton onClick={() => router.push('/content?tag=resources')} icon={<i className="pi pi-file pr-2 pb-1" />} label="View All Resources" className="w-fit py-2 font-semibold" size="small" outlined />
<GenericButton onClick={() => router.push('/content?tag=documents')} icon={<i className="pi pi-file pr-2 pb-1" />} label="View All Documents" className="w-fit py-2 font-semibold" size="small" outlined />
);
case "COMMUNITY":
return (
@ -149,9 +149,9 @@ const InteractivePromotionalCarousel = () => {
return (
<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":
case "DOCUMENTS":
return (
<GenericButton onClick={() => router.push('/content?tag=resources')} icon={<i className="pi pi-file pr-2 pb-1" />} label="View All Resources" className="py-2 font-semibold" size="small" outlined />
<GenericButton onClick={() => router.push('/content?tag=documents')} icon={<i className="pi pi-file pr-2 pb-1" />} label="View All Documents" className="py-2 font-semibold" size="small" outlined />
);
case "COMMUNITY":
return (

View File

@ -94,14 +94,14 @@ export function CourseTemplate({ course }) {
{course.description || course.summary && (
<>
{course.description && (
<div className="text-xl mt-4">
<div>
{course.description.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
)}
{course.summary && (
<div className="text-xl mt-4">
<div>
{course.summary.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}

View File

@ -82,14 +82,14 @@ export function DocumentTemplate({ document }) {
{document.description || document.summary && (
<>
{document.description && (
<div className="text-xl mt-4">
<div>
{document.description.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
)}
{document.summary && (
<div className="text-xl mt-4">
<div>
{document.summary.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}

View File

@ -83,14 +83,14 @@ export function VideoTemplate({ video }) {
{video.description || video.summary && (
<>
{video.description && (
<div className="text-xl mt-4">
<div>
{video.description.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
)}
{video.summary && (
<div className="text-xl mt-4">
<div>
{video.summary.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}

View File

@ -95,7 +95,7 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
{lesson && (
<div className='flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md'>
<Image
alt="resource thumbnail"
alt="course thumbnail"
src={returnImageProxy(lesson.image)}
width={344}
height={194}

View File

@ -129,9 +129,7 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
/>
</div>
</div>
<Divider />
</div>
{renderContent()}
<Divider />
{lesson?.additionalLinks && lesson.additionalLinks.length > 0 && (
<div className='mt-6 bg-gray-800/90 rounded-lg p-4'>
<h3 className='text-lg font-semibold mb-2 text-white'>External links:</h3>
@ -146,6 +144,8 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
</ul>
</div>
)}
</div>
{renderContent()}
</div>
)
}

View File

@ -212,7 +212,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
// Step 6: Show success message and redirect
showToast('success', 'Success', 'Course created successfully');
router.push(`/course/${courseEvent.id}`);
router.push("/");
} catch (error) {
console.error('Error creating course:', error);
@ -252,7 +252,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
console.log('Draft:', draft);
switch (draft?.type) {
case 'resource':
case 'document':
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);
@ -273,7 +273,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
];
type = 'resource';
type = 'document';
break;
case 'video':
if (draft?.price) {

View File

@ -110,7 +110,7 @@ const DraftCourseLesson = ({ lesson, course }) => {
{lesson && (
<div className='flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md'>
<Image
alt="resource thumbnail"
alt="course thumbnail"
src={returnImageProxy(lesson.image)}
width={344}
height={194}

View File

@ -22,7 +22,7 @@ import { Tooltip } from 'primereact/tooltip';
import 'primereact/resources/primereact.min.css';
// todo make the summarry save in a formatted way so we can keep this spaces and line breaks
const ResourceForm = ({ draft = null, isPublished = false }) => {
const DocumentForm = ({ draft = null, isPublished = false }) => {
const [title, setTitle] = useState(draft?.title || '');
const [summary, setSummary] = useState(draft?.summary || '');
const [isPaidResource, setIsPaidResource] = useState(draft?.price ? true : false);
@ -104,7 +104,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
content,
d: draft.d,
image: coverImage,
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'resource'])],
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'document'])],
additionalLinks: additionalLinks.filter(link => link.trim() !== '')
}
@ -127,14 +127,14 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
// update the resource with new noteId
const response = await axios.put(`/api/resources/${draft.d}`, { noteId: event.id });
console.log('response', response);
showToast('success', 'Success', 'Resource published successfully.');
showToast('success', 'Success', 'Document published successfully.');
router.push(`/details/${event.id}`);
} else {
showToast('error', 'Error', 'Failed to publish resource. Please try again.');
showToast('error', 'Error', 'Failed to publish document. Please try again.');
}
} catch (error) {
console.error(error);
showToast('error', 'Error', 'Failed to publish resource. Please try again.');
showToast('error', 'Error', 'Failed to publish document. Please try again.');
}
}
@ -151,11 +151,11 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
const payload = {
title,
summary,
type: 'resource',
type: 'document',
price: isPaidResource ? price : null,
content,
image: coverImage,
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'resource'])],
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'document'])],
additionalLinks: additionalLinks.filter(link => link.trim() !== '')
};
@ -171,7 +171,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
axios[method](url, payload)
.then(response => {
if (response.status === 200 || response.status === 201) {
showToast('success', 'Success', draft ? 'Resource updated successfully.' : 'Resource saved as draft.');
showToast('success', 'Success', draft ? 'Document updated successfully.' : 'Document saved as draft.');
if (response.data?.id) {
router.push(`/draft/${response.data.id}`);
@ -180,7 +180,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
})
.catch(error => {
console.error(error);
showToast('error', 'Error', 'Failed to save resource. Please try again.');
showToast('error', 'Error', 'Failed to save document. Please try again.');
});
}
};
@ -230,7 +230,7 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
</div>
<div className="p-inputgroup flex-1 mt-8 flex-col">
<p className="py-2">Paid Resource</p>
<p className="py-2">Paid Document</p>
<InputSwitch autoResize checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
{isPaidResource && (
<div className="p-inputgroup flex-1 py-4">
@ -292,4 +292,4 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
);
}
export default ResourceForm;
export default DocumentForm;

View File

@ -9,7 +9,7 @@ import { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast';
import { parseEvent } from '@/utils/nostr';
import { useDraftsQuery } from '@/hooks/apiQueries/useDraftsQuery';
import { useResources } from '@/hooks/nostr/useResources';
import { useDocuments } from '@/hooks/nostr/useDocuments';
import { useVideos } from '@/hooks/nostr/useVideos';
import axios from 'axios';
import LessonSelector from './LessonSelector';
@ -27,16 +27,17 @@ const CourseForm = ({ draft = null }) => {
const { data: session } = useSession();
const router = useRouter();
const { showToast } = useToast();
const { resources, resourcesLoading, resourcesError } = useResources();
const { documents, documentsLoading, documentsError } = useDocuments();
const { videos, videosLoading, videosError } = useVideos();
const { drafts, draftsLoading, draftsError } = useDraftsQuery();
useEffect(() => {
if (draft && resources && videos && drafts) {
if (draft && documents && videos && drafts) {
const populatedLessons = draft.draftLessons.map((lesson, index) => {
if (lesson?.resource) {
const matchingResource = resources.find((resource) => resource.d === lesson.resource.d);
return { ...parseEvent(matchingResource), index };
const matchingResource = documents.find((resource) => resource.d === lesson.resource.d);
const matchingParsedResource = parseEvent(matchingResource);
return { ...matchingParsedResource, index };
} else if (lesson?.draft) {
const matchingDraft = drafts.find((draft) => draft.id === lesson.draft.id);
return { ...matchingDraft, index };
@ -46,24 +47,15 @@ const CourseForm = ({ draft = null }) => {
setLessons(populatedLessons);
}
}, [draft, resources, videos, drafts]);
}, [draft, documents, videos, drafts]);
useEffect(() => {
console.log('allContent', allContent);
}, [allContent]);
useEffect(() => {
console.log('fasfsa', videos)
}, [videos])
useEffect(() => {
if (!resourcesLoading && !videosLoading && !draftsLoading) {
if (!documentsLoading && !videosLoading && !draftsLoading) {
let combinedContent = [];
if (resources) {
combinedContent = [...combinedContent, ...resources];
if (documents) {
combinedContent = [...combinedContent, ...documents];
}
if (videos) {
console.log('workssdfsdfdsf', videos)
combinedContent = [...combinedContent, ...videos];
}
if (drafts) {
@ -71,7 +63,7 @@ const CourseForm = ({ draft = null }) => {
}
setAllContent(combinedContent);
}
}, [resources, videos, drafts, resourcesLoading, videosLoading, draftsLoading]);
}, [documents, videos, drafts, documentsLoading, videosLoading, draftsLoading]);
const handleSubmit = async (event) => {
event.preventDefault();
@ -171,7 +163,7 @@ const CourseForm = ({ draft = null }) => {
}
};
if (resourcesLoading || videosLoading || draftsLoading) {
if (documentsLoading || videosLoading || draftsLoading) {
return <ProgressSpinner />;
}

View File

@ -3,14 +3,14 @@ import { Dropdown } from 'primereact/dropdown';
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 EmbeddedDocumentForm from '@/components/forms/course/embedded/EmbeddedDocumentForm';
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, onNewVideoCreate }) => {
const [showResourceForm, setShowResourceForm] = useState(false);
const [showDocumentForm, setShowDocumentForm] = useState(false);
const [showVideoForm, setShowVideoForm] = useState(false);
const [contentOptions, setContentOptions] = useState([]);
const [openTabs, setOpenTabs] = useState([]);
@ -47,7 +47,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
console.log('filtered content', filteredContent)
const draftResourceOptions = filteredContent.filter(content => content?.topics.includes('resource') && !content.kind).map(content => ({
const draftDocumentOptions = filteredContent.filter(content => content?.topics.includes('document') && !content.kind).map(content => ({
label: content.title,
value: content
}));
@ -57,7 +57,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
value: content
}));
const resourceOptions = filteredContent.filter(content => content?.topics.includes('resource') && content.kind).map(content => ({
const documentOptions = filteredContent.filter(content => content?.topics.includes('document') && content.kind).map(content => ({
label: content.title,
value: content
}));
@ -69,16 +69,16 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
setContentOptions([
{
label: 'Draft Resources',
items: draftResourceOptions
label: 'Draft Documents',
items: draftDocumentOptions
},
{
label: 'Draft Videos',
items: draftVideoOptions
},
{
label: 'Published Resources',
items: resourceOptions
label: 'Published Documents',
items: documentOptions
},
{
label: 'Published Videos',
@ -116,16 +116,15 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
setLessons([...lessons, { index: lessons.length }]);
};
const handleNewResourceSave = async (newResource) => {
const createdResource = await onNewResourceCreate(newResource);
if (createdResource) {
handleContentSelect(createdResource, lessons.length);
setShowResourceForm(false);
const handleNewDocumentSave = async (newDocument) => {
const createdDocument = await onNewDocumentCreate(newDocument);
if (createdDocument) {
handleContentSelect(createdDocument, lessons.length);
setShowDocumentForm(false);
}
};
const handleNewVideoSave = async (newVideo) => {
console.log('newVideo', newVideo);
const createdVideo = await onNewVideoCreate(newVideo);
if (createdVideo) {
handleContentSelect(createdVideo, lessons.length);
@ -167,7 +166,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
<div className="flex mt-4">
{lesson.id ? null : (
<>
<GenericButton label="New Resource" onClick={(e) => {e.preventDefault(); setShowResourceForm(true)}} className="mr-2" />
<GenericButton label="New Document" onClick={(e) => {e.preventDefault(); setShowDocumentForm(true)}} className="mr-2" />
<GenericButton label="New Video" onClick={(e) => {e.preventDefault(); setShowVideoForm(true)}} className="mr-2" />
</>
)}
@ -187,11 +186,11 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
label="Add New Lesson"
onClick={addNewLesson}
className="mt-4"
type="button" // Explicitly set type to "button"
type="button"
/>
<Dialog className='w-full max-w-screen-md' visible={showResourceForm} onHide={() => setShowResourceForm(false)} header="Create New Resource">
<EmbeddedResourceForm onSave={handleNewResourceSave} isPaid={isPaidCourse} />
<Dialog className='w-full max-w-screen-md' visible={showDocumentForm} onHide={() => setShowDocumentForm(false)} header="Create New Document">
<EmbeddedDocumentForm onSave={handleNewDocumentSave} isPaid={isPaidCourse} />
</Dialog>
<Dialog className='w-full max-w-screen-md' visible={showVideoForm} onHide={() => setShowVideoForm(false)} header="Create New Video">

View File

@ -19,7 +19,7 @@ import 'primeicons/primeicons.css';
import { Tooltip } from 'primereact/tooltip';
import 'primereact/resources/primereact.min.css';
const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPaid }) => {
const EmbeddedDocumentForm = ({ draft = null, isPublished = false, onSave, isPaid }) => {
const [title, setTitle] = useState(draft?.title || '');
const [summary, setSummary] = useState(draft?.summary || '');
const [isPaidResource, setIsPaidResource] = useState(isPaid);
@ -96,11 +96,11 @@ const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPai
const payload = {
title,
summary,
type: 'resource',
type: 'document',
price: isPaidResource ? price : null,
content,
image: coverImage,
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'resource'])],
topics: [...new Set([...topics.map(topic => topic.trim().toLowerCase()), 'document'])],
additionalLinks: additionalLinks.filter(link => link.trim() !== ''),
user: user?.id || user?.pubkey
};
@ -108,10 +108,10 @@ const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPai
if (onSave) {
try {
await onSave(payload);
showToast('success', 'Success', draft ? 'Resource updated successfully.' : 'Resource created successfully.');
showToast('success', 'Success', draft ? 'Document updated successfully.' : 'Document created successfully.');
} catch (error) {
console.error(error);
showToast('error', 'Error', 'Failed to save resource. Please try again.');
showToast('error', 'Error', 'Failed to save document. Please try again.');
}
}
};
@ -161,7 +161,7 @@ const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPai
</div>
<div className="p-inputgroup flex-1 mt-8 flex-col">
<p className="py-2">Paid Resource</p>
<p className="py-2">Paid Document</p>
<InputSwitch checked={isPaidResource} onChange={(e) => setIsPaidResource(e.value)} />
{isPaidResource && (
<div className="p-inputgroup flex-1 py-4">
@ -223,4 +223,4 @@ const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPai
);
}
export default EmbeddedResourceForm;
export default EmbeddedDocumentForm;

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
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 { useDocuments } from "@/hooks/nostr/useDocuments";
import { useVideos } from "@/hooks/nostr/useVideos";
import { useDraftsQuery } from "@/hooks/apiQueries/useDraftsQuery";
import { useCourseDraftsQuery } from "@/hooks/apiQueries/useCourseDraftsQuery";
@ -31,7 +31,7 @@ const UserContent = () => {
const { showToast } = useToast();
const {ndk, addSigner} = useNDKContext();
const { courses, coursesLoading, coursesError } = useCourses();
const { resources, resourcesLoading, resourcesError } = useResources();
const { documents, documentsLoading, documentsError } = useDocuments();
const { videos, videosLoading, videosError } = useVideos();
const { courseDrafts, courseDraftsLoading, courseDraftsError } = useCourseDraftsQuery();
const { drafts, draftsLoading, draftsError } = useDraftsQuery();
@ -51,7 +51,7 @@ const UserContent = () => {
{ label: "Published", icon: "pi pi-verified" },
{ label: "Drafts", icon: "pi pi-file-edit" },
{ label: "Draft Courses", icon: "pi pi-book" },
{ label: "Resources", icon: "pi pi-file" },
{ label: "Documents", icon: "pi pi-file" },
{ label: "Videos", icon: "pi pi-video" },
{ label: "Courses", icon: "pi pi-desktop" },
];
@ -98,8 +98,8 @@ const UserContent = () => {
case 2:
return courseDrafts || [];
case 3:
return resources?.map(parseEvent) || [];
case 3:
return documents?.map(parseEvent) || [];
case 4:
return videos?.map(parseEvent) || [];
case 4:
return courses?.map(parseEvent) || [];
@ -110,10 +110,10 @@ const UserContent = () => {
setContent(getContentByIndex(activeIndex));
}
}, [activeIndex, isClient, drafts, resources, videos, courses, publishedContent, courseDrafts])
}, [activeIndex, isClient, drafts, documents, videos, courses, publishedContent, courseDrafts])
const isLoading = coursesLoading || resourcesLoading || videosLoading || draftsLoading || contentIdsLoading || courseDraftsLoading;
const isError = coursesError || resourcesError || videosError || draftsError || contentIdsError || courseDraftsError;
const isLoading = coursesLoading || documentsLoading || videosLoading || draftsLoading || contentIdsLoading || courseDraftsLoading;
const isError = coursesError || documentsError || videosError || draftsError || contentIdsError || courseDraftsError;
return (
<div className="p-4">

View File

@ -40,38 +40,42 @@ const Sidebar = ({ course = false }) => {
if (router.isReady) {
const { slug } = router.query;
if (slug && course) {
const { data } = nip19.decode(slug)
try {
if (slug && course) {
const { data } = nip19.decode(slug)
if (!data) {
showToast('error', 'Error', 'Course not found');
return;
}
const id = data?.identifier;
const fetchCourse = async (id) => {
try {
await ndk.connect();
const filter = {
ids: [id]
}
const event = await ndk.fetchEvent(filter);
if (event) {
// all a tags are lessons
const lessons = event.tags.filter(tag => tag[0] === 'a');
const uniqueLessons = [...new Set(lessons.map(lesson => lesson[1]))];
setLessons(uniqueLessons);
}
} catch (error) {
console.error('Error fetching event:', error);
if (!data) {
showToast('error', 'Error', 'Course not found');
return;
}
const id = data?.identifier;
const fetchCourse = async (id) => {
try {
await ndk.connect();
const filter = {
ids: [id]
}
const event = await ndk.fetchEvent(filter);
if (event) {
// all a tags are lessons
const lessons = event.tags.filter(tag => tag[0] === 'a');
const uniqueLessons = [...new Set(lessons.map(lesson => lesson[1]))];
setLessons(uniqueLessons);
}
} catch (error) {
console.error('Error fetching event:', error);
}
};
if (ndk && id) {
fetchCourse(id);
}
};
if (ndk && id) {
fetchCourse(id);
}
} catch (err) {
console.error(err);
}
}
}, [router.isReady, router.query, ndk, course]);
@ -136,8 +140,8 @@ const Sidebar = ({ course = false }) => {
<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>
<div onClick={() => router.push('/content?tag=documents')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content?tag=documents') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold text-lg"><i className="pi pi-file text-sm pr-1"></i> Documents</p>
</div>
</AccordionTab>
</Accordion>

View File

@ -4,12 +4,12 @@ import { useContentIdsQuery } from '@/hooks/apiQueries/useContentIdsQuery';
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
export function useResources() {
export function useDocuments() {
const [isClient, setIsClient] = useState(false);
const [resources, setResources] = useState();
const [documents, setDocuments] = useState();
// Add new state variables for loading and error
const [resourcesLoading, setResourcesLoading] = useState(false);
const [resourcesError, setResourcesError] = useState(null);
const [documentsLoading, setDocumentsLoading] = useState(false);
const [documentsError, setDocumentsError] = useState(null);
const { contentIds } = useContentIdsQuery()
const {ndk, addSigner} = useNDKContext();
@ -19,18 +19,18 @@ export function useResources() {
}, []);
const hasRequiredProperties = (event, contentIds) => {
const hasResource = event.tags.some(([tag, value]) => tag === "t" && value === "resource");
const hasDocument = event.tags.some(([tag, value]) => tag === "t" && value === "document");
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
return hasResource && hasId;
return hasDocument && hasId;
};
const fetchResourcesFromNDK = async () => {
setResourcesLoading(true);
setResourcesError(null);
const fetchDocumentsFromNDK = async () => {
setDocumentsLoading(true);
setDocumentsError(null);
try {
if (!contentIds || contentIds.length === 0) {
console.log('No content IDs found');
setResourcesLoading(false);
setDocumentsLoading(false);
return []; // Return early if no content IDs are found
}
@ -41,29 +41,29 @@ export function useResources() {
if (events && events.size > 0) {
const eventsArray = Array.from(events);
const resources = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
setResourcesLoading(false);
return resources;
const documents = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
setDocumentsLoading(false);
return documents;
}
setResourcesLoading(false);
setDocumentsLoading(false);
return [];
} catch (error) {
console.error('Error fetching resources from NDK:', error);
setResourcesError(error);
setResourcesLoading(false);
console.error('Error fetching documents from NDK:', error);
setDocumentsError(error);
setDocumentsLoading(false);
return [];
}
};
useEffect(() => {
if (isClient && contentIds) {
fetchResourcesFromNDK().then(fetchedResources => {
if (fetchedResources && fetchedResources.length > 0) {
setResources(fetchedResources);
fetchDocumentsFromNDK().then(fetchedDocuments => {
if (fetchedDocuments && fetchedDocuments.length > 0) {
setDocuments(fetchedDocuments);
}
});
}
}, [isClient, contentIds]);
return { resources, resourcesLoading, resourcesError };
return { documents, documentsLoading, documentsError };
}

View File

@ -5,7 +5,7 @@ import axios from 'axios';
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
export function useResourcesQuery() {
export function useDocumentsQuery() {
const [isClient, setIsClient] = useState(false);
const {ndk, addSigner} = useNDKContext();
@ -14,12 +14,12 @@ export function useResourcesQuery() {
}, []);
const hasRequiredProperties = (event, contentIds) => {
const hasResource = event.tags.some(([tag, value]) => tag === "t" && value === "resource");
const hasDocument = event.tags.some(([tag, value]) => tag === "t" && value === "document");
const hasId = event.tags.some(([tag, value]) => tag === "d" && contentIds.includes(value));
return hasResource && hasId;
return hasDocument && hasId;
};
const fetchResourcesFromNDK = async () => {
const fetchDocumentsFromNDK = async () => {
try {
const response = await axios.get(`/api/content/all`);
const contentIds = response.data;
@ -36,23 +36,23 @@ export function useResourcesQuery() {
if (events && events.size > 0) {
const eventsArray = Array.from(events);
const resources = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
return resources;
const documents = eventsArray.filter(event => hasRequiredProperties(event, contentIds));
return documents;
}
return [];
} catch (error) {
console.error('Error fetching resources from NDK:', error);
console.error('Error fetching documents from NDK:', error);
return [];
}
};
const { data: resources, isLoading: resourcesLoading, error: resourcesError, refetch: refetchResources } = useQuery({
queryKey: ['resources', isClient],
queryFn: fetchResourcesFromNDK,
const { data: documents, isLoading: documentsLoading, error: documentsError, refetch: refetchDocuments } = useQuery({
queryKey: ['documents', isClient],
queryFn: fetchDocumentsFromNDK,
// staleTime: 1000 * 60 * 30, // 30 minutes
// refetchInterval: 1000 * 60 * 30, // 30 minutes
enabled: isClient,
});
return { resources, resourcesLoading, resourcesError, refetchResources };
return { documents, documentsLoading, documentsError, refetchDocuments };
}

View File

@ -29,7 +29,7 @@ export default function MyApp({
const router = useRouter();
useEffect(() => {
setIsCourseView(router.pathname.includes('course'));
setIsCourseView(router.pathname.includes('course') && !router.pathname.includes('draft'));
}, [router.pathname]);
// const [sidebarExpanded, setSidebarExpanded] = useState(true);

View File

@ -60,9 +60,9 @@ const AboutPage = () => {
title="Content Types"
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">Documents:</span> Markdown documents posted as NIP-23 long-form events on Nostr.</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>
<li><span className="font-bold">Courses:</span> Nostr lists that combine multiple documents and videos into a structured learning path.</li>
</ul>
}
/>

View File

@ -1,7 +1,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 { useDocuments } from '@/hooks/nostr/useDocuments';
import { useVideos } from '@/hooks/nostr/useVideos';
import { useCourses } from '@/hooks/nostr/useCourses';
import { TabMenu } from 'primereact/tabmenu';
@ -17,7 +17,7 @@ const MenuTab = ({ items, selectedTopic, onTabChange }) => {
const menuItems = allItems.map((item, index) => {
let icon = 'pi pi-tag';
if (item === 'All') icon = 'pi pi-eye';
else if (item === 'Resources') icon = 'pi pi-file';
else if (item === 'Documents') icon = 'pi pi-file';
else if (item === 'Videos') icon = 'pi pi-video';
else if (item === 'Courses') icon = 'pi pi-desktop';
@ -67,11 +67,11 @@ const MenuTab = ({ items, selectedTopic, onTabChange }) => {
const ContentPage = () => {
const router = useRouter();
const { resources, resourcesLoading } = useResources();
const { documents, documentsLoading } = useDocuments();
const { videos, videosLoading } = useVideos();
const { courses, coursesLoading } = useCourses();
const [processedResources, setProcessedResources] = useState([]);
const [processedDocuments, setProcessedDocuments] = useState([]);
const [processedVideos, setProcessedVideos] = useState([]);
const [processedCourses, setProcessedCourses] = useState([]);
const [allContent, setAllContent] = useState([]);
@ -92,11 +92,11 @@ const ContentPage = () => {
}, [router.query.tag]);
useEffect(() => {
if (resources && !resourcesLoading) {
const processedResources = resources.map(resource => ({...parseEvent(resource), type: 'resource'}));
setProcessedResources(processedResources);
if (documents && !documentsLoading) {
const processedDocuments = documents.map(document => ({...parseEvent(document), type: 'document'}));
setProcessedDocuments(processedDocuments);
}
}, [resources, resourcesLoading]);
}, [documents, documentsLoading]);
useEffect(() => {
if (videos && !videosLoading) {
@ -113,11 +113,11 @@ const ContentPage = () => {
}, [courses, coursesLoading]);
useEffect(() => {
const allContent = [...processedResources, ...processedVideos, ...processedCourses];
const allContent = [...processedDocuments, ...processedVideos, ...processedCourses];
setAllContent(allContent);
const uniqueTopics = new Set(allContent.map(item => item.topics).flat());
const priorityItems = ['All', 'Courses', 'Videos', 'Resources'];
const priorityItems = ['All', 'Courses', 'Videos', 'Documents'];
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, processedVideos, processedCourses]);
}, [processedDocuments, processedVideos, processedCourses]);
const filterContent = (topic, content) => {
let filtered = content;
if (topic !== 'All') {
const topicLower = topic.toLowerCase();
if (['courses', 'videos', 'resources'].includes(topicLower)) {
if (['courses', 'videos', 'documents'].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', 'Videos', 'Resources', ...allTopics.filter(topic => !['Courses', 'Videos', 'Resources'].includes(topic))]}
items={['Courses', 'Videos', 'Documents', ...allTopics.filter(topic => !['Courses', 'Videos', 'Documents'].includes(topic))]}
selectedTopic={selectedTopic}
onTabChange={handleTopicChange}
className="max-w-[90%] mx-auto"

View File

@ -44,7 +44,6 @@ const DraftCourse = () => {
axios.get(`/api/courses/drafts/${slug}`)
.then(res => {
setCourse(res.data);
console.log('coursesssss:', res.data);
setLessons(res.data.draftLessons);
})
.catch(err => {

View File

@ -53,16 +53,19 @@ const useCourseData = (ndk, fetchAuthor, router) => {
return { course, lessonIds };
};
const useLessons = (ndk, fetchAuthor, lessonIds) => {
const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
const [lessons, setLessons] = useState([]);
const [uniqueLessons, setUniqueLessons] = useState([]);
console.log('lessonIds', lessonIds);
useEffect(() => {
if (lessonIds.length > 0) {
const fetchLesson = async (lessonId) => {
console.log('lessonId', lessonId);
try {
await ndk.connect();
const filter = { "#d": [lessonId] };
const filter = { "#d": [lessonId], kinds:[30023, 30402], authors: [pubkey] };
const event = await ndk.fetchEvent(filter);
if (event) {
const author = await fetchAuthor(event.pubkey);
@ -83,6 +86,10 @@ const useLessons = (ndk, fetchAuthor, lessonIds) => {
setUniqueLessons(newUniqueLessons);
}, [lessons]);
useEffect(() => {
console.log('uniqueLessons', uniqueLessons);
}, [uniqueLessons]);
return { lessons, uniqueLessons, setLessons };
};
@ -139,7 +146,7 @@ const Course = () => {
}, [ndk]);
const { course, lessonIds } = useCourseData(ndk, fetchAuthor, router);
const { lessons, uniqueLessons, setLessons } = useLessons(ndk, fetchAuthor, lessonIds);
const { lessons, uniqueLessons, setLessons } = useLessons(ndk, fetchAuthor, lessonIds, course?.pubkey);
const { decryptionPerformed, loading } = useDecryption(session, paidCourse, course, lessons, setLessons);
useEffect(() => {

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import MenuTab from "@/components/menutab/MenuTab";
import ResourceForm from "@/components/forms/ResourceForm";
import DocumentForm from "@/components/forms/DocumentForm";
import VideoForm from "@/components/forms/VideoForm";
import CourseForm from "@/components/forms/course/CourseForm";
import { useIsAdmin } from "@/hooks/useIsAdmin";
@ -12,7 +12,7 @@ const Create = () => {
const { isAdmin, isLoading } = useIsAdmin();
const router = useRouter();
const homeItems = [
{ label: 'Resource', icon: 'pi pi-book' },
{ label: 'Document', icon: 'pi pi-file' },
{ label: 'Video', icon: 'pi pi-video' },
{ label: 'Course', icon: 'pi pi-desktop' }
];
@ -32,8 +32,8 @@ const Create = () => {
return <CourseForm />;
case 'Video':
return <VideoForm />;
case 'Resource':
return <ResourceForm />;
case 'Document':
return <DocumentForm />;
default:
return null; // or a default component
}

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { parseEvent } from "@/utils/nostr";
import ResourceForm from "@/components/forms/ResourceForm";
import DocumentForm from "@/components/forms/DocumentForm";
import VideoForm from "@/components/forms/VideoForm";
import CourseForm from "@/components/forms/course/CourseForm";
import { useNDKContext } from "@/context/NDKContext";
@ -41,7 +41,7 @@ export default function Edit() {
<h2 className="text-center mb-8">Edit Published Event</h2>
{event?.topics.includes('course') && <CourseForm draft={event} isPublished />}
{!event?.topics.includes('video') && <VideoForm draft={event} isPublished />}
{event?.topics.includes('resource') && <ResourceForm draft={event} isPublished />}
{event?.topics.includes('document') && <DocumentForm draft={event} isPublished />}
</div>
);
}

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import axios from "axios";
import ResourceForm from "@/components/forms/ResourceForm";
import DocumentForm from "@/components/forms/DocumentForm";
import VideoForm from "@/components/forms/VideoForm";
import CourseForm from "@/components/forms/course/CourseForm";
import { useIsAdmin } from "@/hooks/useIsAdmin";
@ -38,7 +38,7 @@ const Edit = () => {
<h2 className="text-center mb-8">Edit Draft</h2>
{draft?.type === 'course' && <CourseForm draft={draft} />}
{draft?.type === 'video' && <VideoForm draft={draft} />}
{draft?.type === 'resource' && <ResourceForm draft={draft} />}
{draft?.type === 'document' && <DocumentForm draft={draft} />}
</div>
);
};

View File

@ -32,6 +32,7 @@ export default function Draft() {
const { returnImageProxy } = useImageProxy();
const { data: session, status } = useSession();
const [user, setUser] = useState(null);
const [nAddress, setNAddress] = useState(null);
const [videoId, setVideoId] = useState(null);
const { width, height } = useResponsiveImageDimensions();
const router = useRouter();
@ -187,7 +188,7 @@ export default function Draft() {
console.log('NewDTag:', NewDTag);
switch (draft?.type) {
case 'resource':
case 'document':
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);
@ -208,7 +209,7 @@ export default function Draft() {
...(draft?.additionalLinks ? draft.additionalLinks.map(link => ['r', link]) : []),
];
type = 'resource';
type = 'document';
break;
case 'video':
if (draft?.price) {
@ -315,7 +316,7 @@ export default function Draft() {
</div>
<div className='flex flex-col max-tab:mt-12 max-mob:mt-12'>
{draft && (
<div style={{ width: width < 768 ? "auto" : width }} onClick={() => router.push(`/details/${draft.id}`)} className="flex flex-col items-center mx-auto cursor-pointer rounded-md shadow-lg">
<div style={{ width: width < 768 ? "auto" : width }} onClick={() => router.push(`/details/${nAddress}`)} className="flex flex-col items-center mx-auto cursor-pointer rounded-md shadow-lg">
<div style={{ maxWidth: width, minWidth: width }} className="max-tab:h-auto max-mob:h-auto">
<Image
alt="resource thumbnail"

View File

@ -2,7 +2,7 @@ import Head from 'next/head';
import React from 'react';
import CoursesCarousel from '@/components/content/carousels/CoursesCarousel';
import VideosCarousel from '@/components/content/carousels/VideosCarousel';
import ResourcesCarousel from '@/components/content/carousels/ResourcesCarousel';
import DocumentsCarousel from '@/components/content/carousels/DocumentsCarousel';
import InteractivePromotionalCarousel from '@/components/content/carousels/InteractivePromotionalCarousel';
export default function Home() {
@ -18,7 +18,7 @@ export default function Home() {
<InteractivePromotionalCarousel />
<CoursesCarousel />
<VideosCarousel />
<ResourcesCarousel />
<DocumentsCarousel />
</main>
</>
);

View File

@ -63,7 +63,7 @@ export const parseEvent = (event) => {
image: '',
published_at: '',
topics: [], // Added to hold all topics
type: 'resource', // Default type
type: 'document', // Default type
};
// Iterate over the tags array to extract data