add back prettierrc

This commit is contained in:
austinkelsay 2025-04-20 17:50:08 -05:00
parent 0d9f42fb07
commit bb5356f10b
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
3 changed files with 403 additions and 0 deletions

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid"
}

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

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