mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-05-29 21:32:03 +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