+ {topics && topics.length > 0 && (
+ topics.map((topic, index) => (
+
+ ))
+ )}
+ {isLesson && }
{(summary)?.split('\n').map((line, index) => (
+
-
- {authorView ? (
-
- {renderPaymentMessage()}
-
- router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
-
- {
- window.open(`https://habla.news/a/${nAddress}`, '_blank');
- }}
- />
-
-
- ) : (
-
- {renderPaymentMessage()}
-
- {course && window.open(`/course/${course}`, '_blank')} label={isMobileView ? "Course" : "Open Course"} tooltip="This is a lesson in a course" tooltipOptions={{ position: 'top' }} />}
- {
- window.open(`https://habla.news/a/${nAddress}`, '_blank');
- }}
- />
-
-
- )}
+
+ {renderPaymentMessage()}
{renderContent()}
diff --git a/src/components/content/videos/VideoDetails.js b/src/components/content/videos/VideoDetails.js
index b2bd5bd..167e698 100644
--- a/src/components/content/videos/VideoDetails.js
+++ b/src/components/content/videos/VideoDetails.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useRef } from "react";
import axios from "axios";
import { useToast } from "@/hooks/useToast";
import { Tag } from "primereact/tag";
@@ -13,6 +13,8 @@ import { getTotalFromZaps } from "@/utils/lightning";
import { useSession } from "next-auth/react";
import useWindowWidth from "@/hooks/useWindowWidth";
import dynamic from "next/dynamic";
+import { Toast } from "primereact/toast";
+import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu";
const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"),
@@ -31,25 +33,8 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
const { showToast } = useToast();
const windowWidth = useWindowWidth();
const isMobileView = windowWidth <= 768;
-
- useEffect(() => {
- if (isLesson) {
- axios.get(`/api/resources/${processedEvent.d}`).then(res => {
- if (res.data && res.data.lessons[0]?.courseId) {
- setCourse(res.data.lessons[0]?.courseId);
- }
- }).catch(err => {
- console.error('err', err);
- });
- }
- }, [processedEvent.d, isLesson]);
-
- useEffect(() => {
- if (zaps.length > 0) {
- const total = getTotalFromZaps(zaps, processedEvent);
- setZapAmount(total);
- }
- }, [zaps, processedEvent]);
+ const menuRef = useRef(null);
+ const toastRef = useRef(null);
const handleDelete = async () => {
try {
@@ -70,6 +55,63 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
}
}
+ const authorMenuItems = [
+ {
+ label: 'Edit',
+ icon: 'pi pi-pencil',
+ command: () => router.push(`/details/${processedEvent.id}/edit`)
+ },
+ {
+ label: 'Delete',
+ icon: 'pi pi-trash',
+ command: handleDelete
+ },
+ {
+ label: 'View Nostr note',
+ icon: 'pi pi-globe',
+ command: () => {
+ window.open(`https://habla.news/a/${nAddress}`, '_blank');
+ }
+ }
+ ];
+
+ const userMenuItems = [
+ {
+ label: 'View Nostr note',
+ icon: 'pi pi-globe',
+ command: () => {
+ window.open(`https://habla.news/a/${nAddress}`, '_blank');
+ }
+ }
+ ];
+
+ if (course) {
+ userMenuItems.unshift({
+ label: isMobileView ? 'Course' : 'Open Course',
+ icon: 'pi pi-external-link',
+ command: () => window.open(`/course/${course}`, '_blank')
+ });
+ }
+
+ useEffect(() => {
+ if (isLesson) {
+ axios.get(`/api/resources/${processedEvent.d}`).then(res => {
+ if (res.data && res.data.lessons[0]?.courseId) {
+ setCourse(res.data.lessons[0]?.courseId);
+ }
+ }).catch(err => {
+ console.error('err', err);
+ });
+ }
+ }, [processedEvent.d, isLesson]);
+
+ useEffect(() => {
+ if (zaps.length > 0) {
+ const total = getTotalFromZaps(zaps, processedEvent);
+ setZapAmount(total);
+ }
+ }, [zaps, processedEvent]);
+
const renderPaymentMessage = () => {
if (session?.user && session.user?.role?.subscribed && decryptedContent) {
return
@@ -130,37 +172,9 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
return null;
}
- const renderAdditionalLinks = () => {
- if (processedEvent?.additionalLinks && processedEvent.additionalLinks.length > 0) {
- return (
-
-
Additional Links:
- {processedEvent.additionalLinks.map((link, index) => (
-
- ))}
-
- );
- }
- return null;
- };
-
return (
+
{renderContent()}
@@ -183,65 +197,39 @@ const VideoDetails = ({ processedEvent, topics, title, summary, image, price, au
-
+
{(summary)?.split('\n').map((line, index) => (
{line}
))}
- {renderAdditionalLinks()}
-
-
- {authorView ? (
-
- {renderPaymentMessage()}
-
- router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined />
-
- {
- window.open(`https://habla.news/a/${nAddress}`, '_blank');
- }}
- />
-
-
- ) : (
-
- {renderPaymentMessage()}
-
- {course && window.open(`/course/${course}`, '_blank')} label={isMobileView ? "Course" : "Open Course"} tooltip="This is a lesson in a course" tooltipOptions={{ position: 'top' }} />}
- {
- window.open(`https://habla.news/a/${nAddress}`, '_blank');
- }}
- />
-
-
- )}
+
+ {renderPaymentMessage()}
diff --git a/src/components/ui/MoreOptionsMenu.js b/src/components/ui/MoreOptionsMenu.js
new file mode 100644
index 0000000..de13f99
--- /dev/null
+++ b/src/components/ui/MoreOptionsMenu.js
@@ -0,0 +1,69 @@
+import React, { useRef } from "react";
+import { Menu } from "primereact/menu";
+import GenericButton from "@/components/buttons/GenericButton";
+
+/**
+ * A reusable component for displaying a "more options" menu with optional additional links section
+ *
+ * @param {Object} props - Component props
+ * @param {Array} props.menuItems - Array of primary menu items
+ * @param {Array} props.additionalLinks - Array of additional links to add to the menu
+ * @param {boolean} props.isMobileView - Whether the view is mobile
+ * @param {function} props.onLinkClick - Function to be called when a link is clicked
+ */
+const MoreOptionsMenu = ({
+ menuItems,
+ additionalLinks = [],
+ isMobileView = false,
+ onLinkClick = (url) => window.open(url, '_blank')
+}) => {
+ const menuRef = useRef(null);
+
+ // Create a copy of the menu items
+ const updatedMenuItems = [...menuItems];
+
+ // Add a separator and additional links if they exist
+ if (additionalLinks && additionalLinks.length > 0) {
+ // Add separator
+ updatedMenuItems.push({ separator: true, className: "my-2" });
+
+ // Add header for additional links
+ updatedMenuItems.push({
+ label: 'EXTERNAL LINKS',
+ disabled: true,
+ className: 'text-sm font-semibold text-gray-400'
+ });
+
+ // Add each additional link
+ additionalLinks.forEach((link, index) => {
+ let hostname;
+ try {
+ hostname = new URL(link).hostname;
+ } catch (e) {
+ hostname = link; // Fallback if URL parsing fails
+ }
+
+ updatedMenuItems.push({
+ label: `${hostname}`,
+ icon: 'pi pi-external-link',
+ command: () => onLinkClick(link)
+ });
+ });
+ }
+
+ return (
+
+
+ menuRef.current.toggle(e)}
+ aria-label="More options"
+ className="p-button-text"
+ tooltip={isMobileView ? null : "More options"}
+ tooltipOptions={{ position: 'top' }}
+ />
+
+ );
+};
+
+export default MoreOptionsMenu;
\ No newline at end of file
From 080ab4a4b5bb9091b25f6d2c0e303276893afb83 Mon Sep 17 00:00:00 2001
From: Austin Kelsay <53542748+AustinKelsay@users.noreply.github.com>
Date: Mon, 31 Mar 2025 09:41:05 -0500
Subject: [PATCH 4/6] Update src/components/content/courses/CourseLesson.js
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
src/components/content/courses/CourseLesson.js | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/src/components/content/courses/CourseLesson.js b/src/components/content/courses/CourseLesson.js
index 983000d..ad8830b 100644
--- a/src/components/content/courses/CourseLesson.js
+++ b/src/components/content/courses/CourseLesson.js
@@ -89,17 +89,6 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete
];
// Add additional links to menu items if they exist
- if (lesson?.additionalLinks && lesson.additionalLinks.length > 0) {
- lesson.additionalLinks.forEach((link, index) => {
- menuItems.push({
- label: `Link: ${new URL(link).hostname}`,
- icon: 'pi pi-external-link',
- command: () => {
- window.open(link, '_blank');
- }
- });
- });
- }
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
From 79b8cf1ff8cb6b519eec6d5ddad8645ee48a96c3 Mon Sep 17 00:00:00 2001
From: austinkelsay
Date: Mon, 31 Mar 2025 10:02:58 -0500
Subject: [PATCH 5/6] fix course payment button, remove test account from
admins
---
.../content/courses/CourseDetails.js | 18 ++++++++++--------
src/config/appConfig.js | 2 +-
2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/src/components/content/courses/CourseDetails.js b/src/components/content/courses/CourseDetails.js
index 6c3dc91..d61b595 100644
--- a/src/components/content/courses/CourseDetails.js
+++ b/src/components/content/courses/CourseDetails.js
@@ -125,13 +125,15 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec
if (paidCourse && !decryptionPerformed) {
return (
-
+
+
+
);
}
@@ -197,7 +199,7 @@ export default function CourseDetails({ processedEvent, paidCourse, lessons, dec
-
Date: Mon, 31 Mar 2025 10:22:25 -0500
Subject: [PATCH 6/6] Add checks for user logged in, paid course and or
subscriber for user being able to mark lesson as completed
---
.../content/courses/CombinedLesson.js | 76 +++++++++++-------
.../content/courses/CourseLesson.js | 78 +++++++++++--------
.../content/courses/DocumentLesson.js | 76 +++++++++++-------
src/components/content/courses/VideoLesson.js | 76 +++++++++++-------
.../content/documents/DocumentDetails.js | 2 +-
5 files changed, 185 insertions(+), 123 deletions(-)
diff --git a/src/components/content/courses/CombinedLesson.js b/src/components/content/courses/CombinedLesson.js
index 202e993..f026390 100644
--- a/src/components/content/courses/CombinedLesson.js
+++ b/src/components/content/courses/CombinedLesson.js
@@ -14,6 +14,7 @@ import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson';
import { Menu } from "primereact/menu";
import { Toast } from "primereact/toast";
import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu";
+import { useSession } from "next-auth/react";
const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"),
@@ -35,6 +36,7 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
const windowWidth = useWindowWidth();
const isMobileView = windowWidth <= 768;
const isVideo = lesson?.type === 'video';
+ const { data: session } = useSession();
const { isCompleted: videoCompleted, isTracking: videoTracking, markLessonAsCompleted } = useTrackVideoLesson({
lessonId: lesson?.d,
@@ -45,46 +47,60 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
decryptionPerformed
});
- const menuItems = [
- {
- label: 'Mark as completed',
- icon: 'pi pi-check-circle',
- command: async () => {
- try {
- await markLessonAsCompleted();
- 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
- });
+ 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(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: () => {
window.open(`https://habla.news/a/${nAddress}`, '_blank');
}
- }
- ];
+ });
+
+ return items;
+ };
useEffect(() => {
const handleYouTubeMessage = (event) => {
@@ -270,7 +286,7 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
diff --git a/src/components/content/courses/CourseLesson.js b/src/components/content/courses/CourseLesson.js
index ad8830b..34ba3a9 100644
--- a/src/components/content/courses/CourseLesson.js
+++ b/src/components/content/courses/CourseLesson.js
@@ -12,6 +12,7 @@ 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"),
@@ -28,6 +29,7 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete
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;
@@ -39,39 +41,51 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete
decryptionPerformed
});
- const menuItems = [
- {
- 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
- });
+ 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: () => {
@@ -85,10 +99,10 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete
window.open(`https://habla.news/a/${addr}`, '_blank');
}
}
- }
- ];
-
- // Add additional links to menu items if they exist
+ });
+
+ return items;
+ };
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
@@ -164,7 +178,7 @@ const CourseLesson = ({ lesson, course, decryptionPerformed, isPaid, setComplete
diff --git a/src/components/content/courses/DocumentLesson.js b/src/components/content/courses/DocumentLesson.js
index 6919fa0..0e25f4e 100644
--- a/src/components/content/courses/DocumentLesson.js
+++ b/src/components/content/courses/DocumentLesson.js
@@ -13,6 +13,7 @@ import appConfig from "@/config/appConfig";
import useTrackDocumentLesson from "@/hooks/tracking/useTrackDocumentLesson";
import { Toast } from "primereact/toast";
import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu";
+import { useSession } from "next-auth/react";
const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"),
@@ -32,6 +33,7 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
const toastRef = useRef(null);
// todo implement real read time needs to be on form
const readTime = 120;
+ const { data: session } = useSession();
const { isCompleted, isTracking, markLessonAsCompleted } = useTrackDocumentLesson({
lessonId: lesson?.d,
@@ -41,46 +43,60 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
decryptionPerformed: decryptionPerformed,
});
- const menuItems = [
- {
- 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
- });
+ 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: () => {
window.open(`https://habla.news/a/${nAddress}`, '_blank');
}
- }
- ];
+ });
+
+ return items;
+ };
useEffect(() => {
if (!zaps || zapsLoading || zapsError) return;
@@ -183,7 +199,7 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js
index 9086996..feb803b 100644
--- a/src/components/content/courses/VideoLesson.js
+++ b/src/components/content/courses/VideoLesson.js
@@ -13,6 +13,7 @@ import useWindowWidth from "@/hooks/useWindowWidth";
import useTrackVideoLesson from '@/hooks/tracking/useTrackVideoLesson';
import { Toast } from "primereact/toast";
import MoreOptionsMenu from "@/components/ui/MoreOptionsMenu";
+import { useSession } from "next-auth/react";
const MDDisplay = dynamic(
() => import("@uiw/react-markdown-preview"),
@@ -33,6 +34,7 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
const mdDisplayRef = useRef(null);
const menuRef = useRef(null);
const toastRef = useRef(null);
+ const { data: session } = useSession();
const { isCompleted, isTracking, markLessonAsCompleted } = useTrackVideoLesson({
lessonId: lesson?.d,
@@ -43,46 +45,60 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
decryptionPerformed
});
- const menuItems = [
- {
- label: 'Mark as completed',
- icon: 'pi pi-check-circle',
- command: async () => {
- try {
- await markLessonAsCompleted();
- 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
- });
+ 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(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: () => {
window.open(`https://habla.news/a/${nAddress}`, '_blank');
}
- }
- ];
+ });
+
+ return items;
+ };
useEffect(() => {
const handleYouTubeMessage = (event) => {
@@ -239,7 +255,7 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
diff --git a/src/components/content/documents/DocumentDetails.js b/src/components/content/documents/DocumentDetails.js
index 491bb30..9d28be7 100644
--- a/src/components/content/documents/DocumentDetails.js
+++ b/src/components/content/documents/DocumentDetails.js
@@ -141,7 +141,7 @@ const DocumentDetails = ({ processedEvent, topics, title, summary, image, price,
return (