mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-05-31 06:12:02 +00:00
add back prettierrc
This commit is contained in:
parent
0d9f42fb07
commit
bb5356f10b
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
183
src/components/content/carousels/templates/CourseTemplate.js
Normal file
183
src/components/content/carousels/templates/CourseTemplate.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
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 { Message } from 'primereact/message';
|
||||||
|
import useWindowWidth from '@/hooks/useWindowWidth';
|
||||||
|
import GenericButton from '@/components/buttons/GenericButton';
|
||||||
|
import appConfig from '@/config/appConfig';
|
||||||
|
import { BookOpen } from 'lucide-react';
|
||||||
|
|
||||||
|
export function CourseTemplate({ course, showMetaTags = true }) {
|
||||||
|
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();
|
||||||
|
const windowWidth = useWindowWidth();
|
||||||
|
const isMobile = windowWidth < 768;
|
||||||
|
|
||||||
|
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?.d) {
|
||||||
|
const nAddress = nip19.naddrEncode({
|
||||||
|
pubkey: course.pubkey,
|
||||||
|
kind: course.kind,
|
||||||
|
identifier: course.d,
|
||||||
|
relays: appConfig.defaultRelayUrls,
|
||||||
|
});
|
||||||
|
setNAddress(nAddress);
|
||||||
|
}
|
||||||
|
}, [course]);
|
||||||
|
|
||||||
|
const shouldShowMetaTags = topic => {
|
||||||
|
if (!showMetaTags) {
|
||||||
|
return !['lesson', 'document', 'video', 'course'].includes(topic);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!nAddress)
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 w-full h-0 hover:opacity-70 cursor-pointer"
|
||||||
|
style={{ paddingBottom: '56.25%' }}
|
||||||
|
onClick={() => router.push(`/course/${nAddress}`)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt="video thumbnail"
|
||||||
|
src={returnImageProxy(course.image)}
|
||||||
|
quality={100}
|
||||||
|
layout="fill"
|
||||||
|
objectFit="cover"
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50" />
|
||||||
|
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||||
|
<BookOpen className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardHeader className="flex flex-row justify-between items-center p-4 border-b border-gray-700">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<CardTitle className="text-xl sm:text-2xl text-[#f8f8ff]">{course.name}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="text-[#f8f8ff]">
|
||||||
|
<ZapDisplay
|
||||||
|
zapAmount={zapAmount}
|
||||||
|
event={course}
|
||||||
|
zapsLoading={zapsLoading && zapAmount === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent
|
||||||
|
className={`${isMobile ? 'px-3' : ''
|
||||||
|
} pt-4 pb-2 w-full flex flex-row justify-between items-center`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap gap-2 max-w-[65%]">
|
||||||
|
{course &&
|
||||||
|
course.topics &&
|
||||||
|
course.topics.map(
|
||||||
|
(topic, index) =>
|
||||||
|
shouldShowMetaTags(topic) && (
|
||||||
|
<Tag size="small" key={index} className="px-2 py-1 text-sm text-[#f8f8ff]">
|
||||||
|
{topic}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{course?.price && course?.price > 0 ? (
|
||||||
|
<Message
|
||||||
|
className={`${isMobile ? 'py-1 text-xs' : 'py-2'} whitespace-nowrap`}
|
||||||
|
icon="pi pi-lock"
|
||||||
|
severity="info"
|
||||||
|
text={`${course.price} sats`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Message
|
||||||
|
className={`${isMobile ? 'py-1 text-xs' : 'py-2'} whitespace-nowrap`}
|
||||||
|
icon="pi pi-lock-open"
|
||||||
|
severity="success"
|
||||||
|
text="Free"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
<CardDescription
|
||||||
|
className={`${isMobile ? 'w-full p-3' : 'p-6'
|
||||||
|
} py-2 pt-0 text-base text-neutral-50/90 dark:text-neutral-900/90 overflow-hidden min-h-[4em] flex items-center max-w-[100%]`}
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
WebkitLineClamp: '2',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="line-clamp-2 text-wrap break-words">
|
||||||
|
{(course.summary || course.description)?.split('\n').map((line, index) => (
|
||||||
|
<span className="text-wrap break-words" key={index}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</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 ${isMobile ? 'px-3' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
211
src/components/content/courses/CourseLesson.js
Normal file
211
src/components/content/courses/CourseLesson.js
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Tag } from 'primereact/tag';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||||
|
import { getTotalFromZaps } from '@/utils/lightning';
|
||||||
|
import ZapDisplay from '@/components/zaps/ZapDisplay';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useZapsQuery } from '@/hooks/nostrQueries/zaps/useZapsQuery';
|
||||||
|
import { Toast } from 'primereact/toast';
|
||||||
|
import useTrackDocumentLesson from '@/hooks/tracking/useTrackDocumentLesson';
|
||||||
|
import useWindowWidth from '@/hooks/useWindowWidth';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import appConfig from '@/config/appConfig';
|
||||||
|
import MoreOptionsMenu from '@/components/ui/MoreOptionsMenu';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
const MDDisplay = dynamic(() => import('@uiw/react-markdown-preview'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }) => {
|
||||||
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
|
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson,
|
||||||
|
type: 'lesson' });
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
const toastRef = useRef(null);
|
||||||
|
const windowWidth = useWindowWidth();
|
||||||
|
const isMobileView = windowWidth <= 768;
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const readTime = lesson?.content ? Math.max(30, Math.ceil(lesson.content.length / 20)) : 60;
|
||||||
|
|
||||||
|
const { isCompleted, isTracking, markLessonAsCompleted } = useTrackDocumentLesson({
|
||||||
|
lessonId: lesson?.d,
|
||||||
|
courseId: course?.d,
|
||||||
|
readTime,
|
||||||
|
paidCourse: isPaid,
|
||||||
|
decryptionPerformed,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildMenuItems = () => {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
const hasAccess =
|
||||||
|
session?.user && (!isPaid || decryptionPerformed || session.user.role?.subscribed);
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
items.push({
|
||||||
|
label: 'Mark as completed',
|
||||||
|
icon: 'pi pi-check-circle',
|
||||||
|
command: async () => {
|
||||||
|
try {
|
||||||
|
await markLessonAsCompleted();
|
||||||
|
setCompleted && setCompleted(lesson.id);
|
||||||
|
toastRef.current.show({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Success',
|
||||||
|
detail: 'Lesson marked as completed',
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark lesson as completed:', error);
|
||||||
|
toastRef.current.show({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'Failed to mark lesson as completed',
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: 'Open lesson',
|
||||||
|
icon: 'pi pi-arrow-up-right',
|
||||||
|
command: () => {
|
||||||
|
window.open(`/details/${lesson.id}`, '_blank');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: 'View Nostr note',
|
||||||
|
icon: 'pi pi-globe',
|
||||||
|
command: () => {
|
||||||
|
if (lesson?.d) {
|
||||||
|
const addr = nip19.naddrEncode({
|
||||||
|
pubkey: lesson.pubkey,
|
||||||
|
kind: lesson.kind,
|
||||||
|
identifier: lesson.d,
|
||||||
|
relays: appConfig.defaultRelayUrls || [],
|
||||||
|
});
|
||||||
|
window.open(`https://habla.news/a/${addr}`, '_blank');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!zaps || zapsLoading || zapsError) return;
|
||||||
|
|
||||||
|
const total = getTotalFromZaps(zaps, lesson);
|
||||||
|
|
||||||
|
setZapAmount(total);
|
||||||
|
}, [zaps, zapsLoading, zapsError, lesson]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCompleted && !isTracking && setCompleted) {
|
||||||
|
setCompleted(lesson.id);
|
||||||
|
}
|
||||||
|
}, [isCompleted, isTracking, lesson.id, setCompleted]);
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isPaid && decryptionPerformed) {
|
||||||
|
return <MDDisplay className="p-4 rounded-lg w-full" source={lesson.content} />;
|
||||||
|
}
|
||||||
|
if (isPaid && !decryptionPerformed) {
|
||||||
|
return (
|
||||||
|
<p className="text-center text-xl text-red-500">
|
||||||
|
This content is paid and needs to be purchased before viewing.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (lesson?.content) {
|
||||||
|
return <MDDisplay className="p-4 rounded-lg w-full" source={lesson.content} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-24 pt-12 mx-auto mt-4 max-tab:px-0 max-mob:px-0 max-tab:pt-2 max-mob:pt-2">
|
||||||
|
<Toast ref={toastRef} />
|
||||||
|
<div className="w-full flex flex-row justify-between max-tab:flex-col max-mob:flex-col">
|
||||||
|
<div className="w-[75vw] mx-auto flex flex-row items-start justify-between max-tab:flex-col max-mob:flex-col max-tab:w-[95vw] max-mob:w-[95vw]">
|
||||||
|
<div className="flex flex-col items-start max-w-[45vw] max-tab:max-w-[100vw] max-mob:max-w-[100vw]">
|
||||||
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
|
<h1 className="text-4xl">{lesson?.title}</h1>
|
||||||
|
<ZapDisplay zapAmount={zapAmount} event={lesson} zapsLoading={zapsLoading} />
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 flex flex-row justify-start w-full mt-2 mb-4">
|
||||||
|
{lesson &&
|
||||||
|
lesson.topics &&
|
||||||
|
lesson.topics.length > 0 &&
|
||||||
|
lesson.topics.map((topic, index) => (
|
||||||
|
<Tag className="mr-2 text-white" key={index} value={topic}></Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between w-full mt-8">
|
||||||
|
<div className="flex flex-row w-fit items-center">
|
||||||
|
<Image
|
||||||
|
alt="avatar thumbnail"
|
||||||
|
src={returnImageProxy(lesson.author?.avatar, lesson.author?.pubkey)}
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
className="rounded-full mr-4"
|
||||||
|
/>
|
||||||
|
<p className="text-lg">
|
||||||
|
Created by{' '}
|
||||||
|
<a
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{lesson.author?.username || lesson.author?.pubkey}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<MoreOptionsMenu
|
||||||
|
menuItems={buildMenuItems()}
|
||||||
|
additionalLinks={lesson?.additionalLinks || []}
|
||||||
|
isMobileView={isMobileView}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col max-tab:mt-12 max-mob:mt-12">
|
||||||
|
{lesson && (
|
||||||
|
<div className="flex flex-col items-center justify-between rounded-lg h-72 p-4 bg-gray-700 drop-shadow-md">
|
||||||
|
<Image
|
||||||
|
alt="course thumbnail"
|
||||||
|
src={returnImageProxy(lesson.image)}
|
||||||
|
width={344}
|
||||||
|
height={194}
|
||||||
|
className="w-[344px] h-[194px] object-cover object-top rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseLesson;
|
Loading…
x
Reference in New Issue
Block a user