Added generic button

This commit is contained in:
austinkelsay 2024-09-10 15:44:08 -05:00
parent 046a130efa
commit 7c8273d663
37 changed files with 368 additions and 427 deletions

View File

@ -6,6 +6,7 @@ import { initializeBitcoinConnect } from './BitcoinConnect';
import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
import GenericButton from '@/components/buttons/GenericButton';
import axios from 'axios'; // Import axios for API calls
const Payment = dynamic(
@ -75,7 +76,7 @@ const CoursePaymentButton = ({ lnAddress, amount, onSuccess, onError, courseId }
return (
<>
<Button
<GenericButton
label={`${amount} sats`}
onClick={() => setDialogVisible(true)}
disabled={!invoice}

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog';
import { initializeBitcoinConnect } from './BitcoinConnect';
import { LightningAddress } from '@getalby/lightning-tools';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
import axios from 'axios';
import GenericButton from '@/components/buttons/GenericButton';
const Payment = dynamic(
() => import('@getalby/bitcoin-connect-react').then((mod) => mod.Payment),
@ -73,7 +73,7 @@ const ResourcePaymentButton = ({ lnAddress, amount, onSuccess, onError, resource
return (
<>
<Button
<GenericButton
label={`${amount} sats`}
icon="pi pi-wallet"
onClick={() => setDialogVisible(true)}

View File

@ -10,6 +10,7 @@ import { useRouter } from 'next/router';
import { Divider } from 'primereact/divider';
import dynamic from 'next/dynamic';
import AlbyButton from '@/components/buttons/AlbyButton';
import GenericButton from '@/components/buttons/GenericButton';
import axios from 'axios';
import Image from 'next/image';
@ -208,7 +209,7 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio
{!invoice && (
<div className="w-full flex flex-row justify-between">
{(oneTime || (!oneTime && !recurring)) && (
<Button
<GenericButton
label="Pay as you go"
icon="pi pi-bolt"
onClick={async () => {
@ -220,7 +221,7 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio
/>
)}
{(recurring || (!oneTime && !recurring)) && (
<Button
<GenericButton
label="Setup Recurring Subscription"
icon={
<Image
@ -253,7 +254,7 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio
placeholder="Enter NWC URL"
className="w-full p-2 mb-4 border rounded"
/>
<Button
<GenericButton
label="Submit"
onClick={handleManualNwcSubmit}
className="mt-4 w-fit text-[#f8f8ff]"

View File

@ -0,0 +1,11 @@
import { Button } from 'primereact/button';
import useWindowWidth from '@/hooks/useWindowWidth';
const GenericButton = ({ label, icon, onClick, severity, size, className, outlined = false, rounded = false, disabled = false, tooltip = null, tooltipOptions = null }) => {
const windowWidth = useWindowWidth();
return (
<Button label={label} icon={icon} onClick={onClick} severity={severity} size={size || (windowWidth < 768 ? 'small' : 'normal')} className={className} outlined={outlined} rounded={rounded} disabled={disabled} tooltip={tooltip} tooltipOptions={tooltipOptions} />
);
}
export default GenericButton;

View File

@ -2,21 +2,19 @@ import React from "react";
import Image from "next/image";
import { useImageProxy } from "@/hooks/useImageProxy";
import { formatUnixTimestamp } from "@/utils/time";
import { Button } from 'primereact/button';
import GenericButton from "@/components/buttons/GenericButton";
const SelectedContentItem = ({ content, onRemove }) => {
const { returnImageProxy } = useImageProxy();
return (
<div className="w-full border-2 rounded-lg border-gray-700 p-2 rounded-tr-none rounded-br-none relative">
<Button
<GenericButton
icon="pi pi-times"
className="absolute top-2 right-2 py-1 px-2 w-auto h-auto"
severity="danger"
size="small"
rounded
onClick={onRemove}
aria-label="Remove"
/>
<div className="flex flex-row gap-4">
<Image

View File

@ -25,7 +25,7 @@ const CourseTemplate = ({ course }) => {
return (
<div
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md"
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md max-tab:px-0"
>
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
<div

View File

@ -26,7 +26,7 @@ const ResourceTemplate = ({ resource }) => {
return (
<div
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md"
className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md max-tab:px-0"
>
{/* Wrap the image in a div with a relative class with a padding-bottom of 56.25% representing the aspect ratio of 16:9 */}
<div

View File

@ -24,7 +24,7 @@ const WorkshopTemplate = ({ workshop }) => {
if (zapsError) return <div>Error: {zapsError}</div>;
return (
<div className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md">
<div className="flex flex-col items-center mx-auto px-4 mt-8 rounded-md max-tab:px-0">
<div
onClick={() => router.replace(`/details/${workshop.id}`)}
className="relative w-full h-0 hover:opacity-80 transition-opacity duration-300 cursor-pointer"

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback, useRef } from 'react';
import { useRouter } from 'next/router';
import { useImageProxy } from '@/hooks/useImageProxy';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import Image from 'next/image';
import dynamic from 'next/dynamic';
import axios from 'axios';
@ -407,9 +407,9 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
</div>
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
<div className='w-fit flex flex-row justify-between'>
<Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto m-2" />
<Button onClick={() => router.push(`/course/${draftId}/draft/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
<GenericButton onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto m-2" />
<GenericButton onClick={() => router.push(`/course/${draftId}/draft/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
</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]'>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { Tag } from "primereact/tag";
import { Message } from "primereact/message";
import { Button } from "primereact/button";
import GenericButton from "@/components/buttons/GenericButton";
import Image from "next/image";
import { useImageProxy } from "@/hooks/useImageProxy";
import { formatDateTime, formatUnixTimestamp } from "@/utils/time";
@ -87,14 +87,14 @@ const DraftCourseLesson = ({ lesson, course }) => {
{isPublished ? (
<>
<Message severity="success" text="published" className="w-auto m-2" />
<Button onClick={() => router.push(`/details/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
<Button onClick={() => router.push(`/details/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<GenericButton onClick={() => router.push(`/details/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
<GenericButton onClick={() => router.push(`/details/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
</>
) : (
<>
<Message severity="info" text="draft (unpublished)" />
<Button onClick={() => router.push(`/draft/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
<Button onClick={() => router.push(`/draft/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<GenericButton onClick={() => router.push(`/draft/${lesson.id}`)} label="View" outlined className="w-auto m-2" />
<GenericButton onClick={() => router.push(`/draft/${lesson.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
</>
)}
</div>

View File

@ -2,7 +2,7 @@ import React, {useEffect} from "react";
import Image from "next/image";
import { useImageProxy } from "@/hooks/useImageProxy";
import { formatUnixTimestamp } from "@/utils/time";
import { Button } from "primereact/button";
import GenericButton from "@/components/buttons/GenericButton";
const ContentDropdownItem = ({ content, onSelect }) => {
const { returnImageProxy } = useImageProxy();
@ -25,7 +25,7 @@ const ContentDropdownItem = ({ content, onSelect }) => {
</div>
</div>
<div className="flex flex-col justify-end">
<Button label="Select" onClick={() => onSelect(content)} />
<GenericButton label="Select" onClick={() => onSelect(content)} className="text-[#f8f8ff]" />
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import React, {useEffect} from "react";
import Image from "next/image";
import { Button } from "primereact/button";
import GenericButton from "@/components/buttons/GenericButton";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useRouter } from "next/router";
import { Divider } from 'primereact/divider';
@ -44,7 +44,7 @@ const ContentListItem = (content) => {
<span>{content.summary}</span>
</div>
<div className="text-right">
<Button
<GenericButton
onClick={handleClick}
label="Open"
outlined

View File

@ -1,16 +1,15 @@
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Avatar } from 'primereact/avatar';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import React from 'react';
import { ProgressSpinner } from 'primereact/progressspinner';
import { useDiscordQuery } from '@/hooks/communityQueries/useDiscordQuery';
import { useRouter } from 'next/router';
import { highlightText } from '@/utils/text';
import CommunityMessage from '@/components/feeds/messages/CommunityMessage';
import useWindowWidth from '@/hooks/useWindowWidth';
const DiscordFeed = ({ searchQuery }) => {
const router = useRouter();
const { data, error, isLoading } = useDiscordQuery({page: router.query.page});
const windowWidth = useWindowWidth();
if (isLoading) {
return (
@ -24,54 +23,22 @@ const DiscordFeed = ({ searchQuery }) => {
return <div className="text-red-500 text-center p-4">Failed to load messages. Please try again later.</div>;
}
const header = (message) => (
<div className="flex flex-row w-full items-center justify-between p-4 bg-gray-800 rounded-t-lg">
<div className="flex flex-row items-center">
<Avatar image={message.avatar} shape="circle" size="large" className="border-2 border-blue-400" />
<p className="pl-4 font-bold text-xl text-white">{message.author}</p>
</div>
<div className="flex flex-col items-start justify-between">
<div className="flex flex-row w-full justify-between items-center my-1 max-sidebar:flex-col max-sidebar:items-start">
<Tag value={message.channel} severity="primary" className="w-fit text-[#f8f8ff] bg-gray-600 mr-2 max-sidebar:mr-0" />
<Tag icon="pi pi-discord" value="discord" className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" />
</div>
</div>
</div>
);
const footer = (message) => (
<div className="w-full flex justify-between items-center">
<span className="bg-gray-800 rounded-lg p-2 text-sm text-gray-300">
{new Date(message.timestamp).toLocaleString()}
</span>
<Button
label="View in Discord"
icon="pi pi-external-link"
outlined
size="small"
className='my-2'
onClick={() => window.open(`https://discord.com/channels/${message.channelId}/${message.id}`, '_blank')}
/>
</div>
);
const filteredData = data.filter(message =>
message.content.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[86vw]">
<div className="mx-4 mt-4">
<div className="mx-4">
{filteredData && filteredData.length > 0 ? (
filteredData.map(message => (
<Card
key={message.id}
header={() => header(message)}
footer={() => footer(message)}
className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4"
>
<p className="m-0 text-lg text-gray-200">{highlightText(message.content, searchQuery)}</p>
</Card>
<CommunityMessage
key={message.id}
message={message}
searchQuery={searchQuery}
windowWidth={windowWidth}
platform="discord"
/>
))
) : (
<div className="text-gray-400 text-center p-4">No messages available.</div>

View File

@ -1,8 +1,4 @@
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Avatar } from 'primereact/avatar';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import { ProgressSpinner } from 'primereact/progressspinner';
import { useDiscordQuery } from '@/hooks/communityQueries/useDiscordQuery';
import { useQuery } from '@tanstack/react-query';
@ -14,8 +10,9 @@ import { findKind0Fields } from '@/utils/nostr';
import NostrIcon from '../../../public/images/nostr.png';
import Image from 'next/image';
import { useImageProxy } from '@/hooks/useImageProxy';
import { highlightText } from '@/utils/text';
import { nip19 } from 'nostr-tools';
import CommunityMessage from '@/components/feeds/messages/CommunityMessage';
import useWindowWidth from '@/hooks/useWindowWidth';
const StackerNewsIconComponent = () => (
<svg width="16" height="16" className='mr-2' viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -36,6 +33,7 @@ const GlobalFeed = ({searchQuery}) => {
const { communityNotes: nostrData, error: nostrError, isLoading: nostrLoading } = useCommunityNotes();
const { ndk } = useNDKContext();
const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth();
const [authorData, setAuthorData] = useState({});
@ -91,17 +89,6 @@ const GlobalFeed = ({searchQuery}) => {
return <div className="text-red-500 text-center p-4">Failed to load feed. Please try again later.</div>;
}
const getAvatarImage = (item) => {
if (item.type === 'discord') {
return item.avatar ? returnImageProxy(item.avatar) : null;
} else if (item.type === 'nostr') {
return authorData[item.pubkey]?.avatar ? returnImageProxy(authorData[item.pubkey]?.avatar) : null;
} else if (item.type === 'stackernews') {
return item.user.image ? returnImageProxy(item.user.image) : null;
}
return null;
};
const combinedFeed = [
...(discordData || []).map(item => ({ ...item, type: 'discord' })),
...(stackerNewsData || []).map(item => ({ ...item, type: 'stackernews' })),
@ -120,101 +107,38 @@ const GlobalFeed = ({searchQuery}) => {
return false;
});
const header = (item) => (
<div className="flex flex-row w-full items-center justify-between p-4 bg-gray-800 rounded-t-lg">
<div className="flex flex-row items-center">
<Avatar
image={getAvatarImage(item)}
icon={getAvatarImage(item) ? null : 'pi pi-user'}
shape="circle"
size="large"
className="border-2 border-blue-400"
/>
<p className="pl-4 font-bold text-xl text-white">
{item.type === 'discord' ? item.author :
item.type === 'stackernews' ? item.user.name :
authorData[item.pubkey]?.username || item.pubkey.substring(0, 12) + '...'}
</p>
</div>
<div className="flex flex-col items-start justify-between">
<div className="flex flex-row w-full justify-between items-center my-1 max-sidebar:flex-col max-sidebar:items-start">
{item.type === 'discord' && (
<>
<Tag value={item.channel} severity="primary" className="w-fit text-[#f8f8ff] bg-gray-600 mr-2 max-sidebar:mr-0" />
<Tag icon="pi pi-discord" value="discord" className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" />
</>
)}
{item.type === 'stackernews' && (
<>
<Tag value="~devs" severity="contrast" className="w-fit text-[#f8f8ff] mr-2 max-sidebar:mr-0" />
<Tag icon={<StackerNewsIconComponent />} value="stackernews" className="w-fit bg-gray-600 text-[#f8f8ff] max-sidebar:mt-1" />
</>
)}
{item.type === 'nostr' && (
<>
<Tag icon="pi pi-hashtag" value="plebdevs" severity="primary" className="w-fit text-[#f8f8ff] bg-gray-600 mr-2 max-sidebar:mr-0" />
<Tag icon={<Image src={NostrIcon} alt="Nostr" width={14} height={14} className='mr-[1px]' />} value="nostr" className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" />
</>
)}
</div>
</div>
</div>
);
const footer = (item) => (
<div className="w-full flex justify-between items-center">
<span className="bg-gray-800 rounded-lg p-2 text-sm text-gray-300">
{item.type === 'nostr'
? new Date(item.created_at * 1000).toLocaleString()
: new Date(item.timestamp || item.createdAt).toLocaleString()}
</span>
<Button
label={item.type === 'discord' ? "View in Discord" :
item.type === 'stackernews' ? "View on StackerNews" :
"View on Nostr"}
icon="pi pi-external-link"
outlined
severity={item.type === 'discord' ? "info" :
item.type === 'stackernews' ? "warning" :
"success"}
size="small"
className='my-2'
onClick={() => window.open(
item.type === 'discord' ? `https://discord.com/channels/${item.channelId}/${item.id}` :
item.type === 'stackernews' ? `https://stacker.news/items/${item.id}` :
`https://nostr.band/${nip19.noteEncode(item.id)}`,
'_blank'
)}
/>
</div>
);
return (
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[86vw]">
<div className="mx-4 mt-4">
{combinedFeed.length > 0 ? (
combinedFeed.map(item => (
<Card
key={item.id}
header={() => header(item)}
footer={() => footer(item)}
className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4"
>
{item.type === 'discord' || item.type === 'nostr' ? (
<p className="m-0 text-lg text-gray-200 overflow-hidden break-words">
{highlightText(item.content, searchQuery)}
</p>
) : (
<>
<h3 className="m-0 text-lg text-gray-200">
{highlightText(item.title, searchQuery)}
</h3>
<p className="text-sm text-gray-400">
Comments: {item.comments.length} | Sats: {item.sats}
</p>
</>
)}
</Card>
<CommunityMessage
key={item.id}
message={{
id: item.id,
author: item.type === 'discord' ? item.author :
item.type === 'stackernews' ? item.user.name :
authorData[item.pubkey]?.username || item.pubkey.substring(0, 12) + '...',
avatar: item.type === 'discord' ? item.avatar :
item.type === 'stackernews' ? item.user.image :
authorData[item.pubkey]?.avatar,
content: item.type === 'stackernews' ? item.title : item.content,
timestamp: item.type === 'nostr' ? item.created_at * 1000 : new Date(item.timestamp || item.createdAt),
channel: item.type === 'discord' ? item.channel :
item.type === 'stackernews' ? "~devs" :
"plebdevs"
}}
searchQuery={searchQuery}
windowWidth={windowWidth}
platform={item.type}
platformIcon={item.type === 'stackernews' ? <StackerNewsIconComponent /> :
item.type === 'nostr' ? <Image src={NostrIcon} alt="Nostr" width={14} height={14} className='mr-[1px]' /> :
null}
platformLink={item.type === 'discord' ? `https://discord.com/channels/${item.channelId}/${item.id}` :
item.type === 'stackernews' ? `https://stacker.news/items/${item.id}` :
`https://nostr.band/${nip19.noteEncode(item.id)}`}
additionalContent={item.type === 'stackernews' ? `Comments: ${item.comments.length} | Sats: ${item.sats}` : null}
/>
))
) : (
<div className="text-gray-400 text-center p-4">

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { InputTextarea } from 'primereact/inputtextarea';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { Panel } from 'primereact/panel';
import { useNDKContext } from "@/context/NDKContext";
import { NDKEvent } from "@nostr-dev-kit/ndk";
@ -8,8 +8,10 @@ import { useToast } from '@/hooks/useToast';
const MessageInput = ({ onMessageSent }) => {
const [message, setMessage] = useState('');
const [collapsed, setCollapsed] = useState(false);
const { ndk, addSigner } = useNDKContext();
const { showToast } = useToast();
const handleSubmit = async () => {
if (!message.trim() || !ndk) return;
@ -33,20 +35,23 @@ const MessageInput = ({ onMessageSent }) => {
}
};
const headerTemplate = (options) => {
return (
<div className="flex align-items-center justify-content-between my-1 py-2">
<GenericButton outlined severity="primary" size="small" className="py-1" onClick={options.onTogglerClick} icon={options.collapsed ? 'pi pi-chevron-down' : 'pi pi-chevron-up'} />
<h3 className="m-0 ml-2">New Message</h3>
</div>
);
};
return (
<Panel header={null} toggleable collapsed={false} className="w-full" pt={{
header: {
className: 'bg-transparent',
border: 'none',
},
toggler: {
className: 'bg-transparent',
border: 'none',
},
togglerIcon: {
display: 'none',
},
}}>
<Panel
headerTemplate={headerTemplate}
toggleable
collapsed={collapsed}
onToggle={(e) => setCollapsed(e.value)}
className="w-full"
>
<div className="w-full flex flex-col">
<InputTextarea
value={message}
@ -59,11 +64,12 @@ const MessageInput = ({ onMessageSent }) => {
/>
</div>
<div className="w-full flex flex-row justify-end mt-4">
<Button
<GenericButton
label="Send"
icon="pi pi-send"
outlined
onClick={handleSubmit}
className="w-fit py-2"
/>
</div>
</Panel>

View File

@ -1,8 +1,4 @@
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Avatar } from 'primereact/avatar';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import { ProgressSpinner } from 'primereact/progressspinner';
import { useNDKContext } from '@/context/NDKContext';
import { useSession } from 'next-auth/react';
@ -10,9 +6,10 @@ import { findKind0Fields } from '@/utils/nostr';
import NostrIcon from '../../../public/images/nostr.png';
import Image from 'next/image';
import { useImageProxy } from '@/hooks/useImageProxy';
import useWindowWidth from '@/hooks/useWindowWidth';
import { nip19 } from 'nostr-tools';
import { useCommunityNotes } from '@/hooks/nostr/useCommunityNotes';
import { highlightText } from '@/utils/text';
import CommunityMessage from '@/components/feeds/messages/CommunityMessage';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
const NostrFeed = ({ searchQuery }) => {
@ -23,6 +20,8 @@ const NostrFeed = ({ searchQuery }) => {
const [authorData, setAuthorData] = useState({});
const [npub, setNpub] = useState(null);
const windowWidth = useWindowWidth();
useEffect(() => {
const fetchAuthors = async () => {
const authorDataMap = {};
@ -68,61 +67,6 @@ const NostrFeed = ({ searchQuery }) => {
}
};
const getAvatarImage = (message) => {
return authorData[message.pubkey]?.avatar ? returnImageProxy(authorData[message.pubkey]?.avatar) : null;
};
const renderHeader = (message) => (
<div className="flex flex-row w-full items-center justify-between p-4 bg-gray-800 rounded-t-lg">
<div className="flex flex-row items-center">
<Avatar
image={getAvatarImage(message)}
icon={getAvatarImage(message) ? null : 'pi pi-user'}
shape="circle"
size="large"
className="border-2 border-blue-400"
/>
<p className="pl-4 font-bold text-xl text-white">
{authorData[message.pubkey]?.username || message.pubkey.substring(0, 12) + '...'}
</p>
</div>
<div className="flex flex-col items-start justify-between">
<div className="flex flex-row w-full justify-between items-center my-1 max-sidebar:flex-col max-sidebar:items-start">
<Tag icon="pi pi-hashtag" value="plebdevs" severity="primary" className="w-fit text-[#f8f8ff] bg-gray-600 mr-2 max-sidebar:mr-0" />
<Tag icon={<Image src={NostrIcon} alt="Nostr" width={14} height={14} className='mr-[1px]' />} value="nostr" className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" />
</div>
</div>
</div>
);
const footer = (message) => (
<>
<div className="w-full flex justify-between items-center">
<span className="bg-gray-800 rounded-lg p-2 text-sm text-gray-300">
{new Date(message.created_at * 1000).toLocaleString()}
</span>
<Button
label="View on Nostr"
icon="pi pi-external-link"
outlined
size="small"
className='my-2'
onClick={() => window.open(`https://nostr.band/${nip19.noteEncode(message.id)}`, '_blank')}
/>
</div>
<div className="w-full">
{session?.user?.pubkey && (
<ZapThreadsWrapper
anchor={nip19.noteEncode(message.id)}
user={npub}
relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://nostr.mutinywallet.com/, wss://relay.mutinywallet.com/, wss://relay.primal.net/"
disable=""
/>
)}
</div>
</>
);
if (isLoading) {
return (
<div className="h-[100vh] min-bottom-bar:w-[86vw] max-sidebar:w-[100vw]">
@ -144,16 +88,32 @@ const NostrFeed = ({ searchQuery }) => {
<div className="mx-4 mt-4">
{filteredNotes.length > 0 ? (
filteredNotes.map(message => (
<Card
<CommunityMessage
key={message.id}
header={renderHeader(message)}
footer={() => footer(message)}
className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4"
>
<p className="m-0 text-lg text-gray-200 overflow-hidden break-words">
{highlightText(message.content, searchQuery)}
</p>
</Card>
message={{
id: message.id,
author: authorData[message.pubkey]?.username || message.pubkey.substring(0, 12) + '...',
avatar: authorData[message.pubkey]?.avatar ? returnImageProxy(authorData[message.pubkey]?.avatar) : null,
content: message.content,
timestamp: message.created_at * 1000,
channel: "plebdevs"
}}
searchQuery={searchQuery}
windowWidth={windowWidth}
platform="nostr"
platformIcon={<Image src={NostrIcon} alt="Nostr" width={14} height={14} className='mr-[1px]' />}
platformLink={`https://nostr.band/${nip19.noteEncode(message.id)}`}
additionalFooter={
session?.user?.pubkey && (
<ZapThreadsWrapper
anchor={nip19.noteEncode(message.id)}
user={npub}
relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://nostr.mutinywallet.com/, wss://relay.mutinywallet.com/, wss://relay.primal.net/"
disable=""
/>
)
}
/>
))
) : (
<div className="text-gray-400 text-center p-4">No messages available.</div>

View File

@ -1,104 +1,72 @@
import React, { useEffect } from 'react';
import { Card } from 'primereact/card';
import { Avatar } from 'primereact/avatar';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import React from 'react';
import { ProgressSpinner } from 'primereact/progressspinner';
import { useQuery } from '@tanstack/react-query';
import { highlightText } from '@/utils/text';
import axios from 'axios';
import CommunityMessage from '@/components/feeds/messages/CommunityMessage';
import useWindowWidth from '@/hooks/useWindowWidth';
const StackerNewsIconComponent = () => (
<svg width="16" height="16" className='mr-2' viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#facc15" fillRule="evenodd" d="m41.7 91.4 41.644 59.22-78.966 69.228L129.25 155.94l-44.083-58.14 54.353-65.441Z"/>
<path fill="#facc15" fillRule="evenodd" d="m208.355 136.74-54.358-64.36-38.4 128.449 48.675-74.094 64.36 65.175L251.54 42.497Z"/>
</svg>
<svg width="16" height="16" className='mr-2' viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#facc15" fillRule="evenodd" d="m41.7 91.4 41.644 59.22-78.966 69.228L129.25 155.94l-44.083-58.14 54.353-65.441Z"/>
<path fill="#facc15" fillRule="evenodd" d="m208.355 136.74-54.358-64.36-38.4 128.449 48.675-74.094 64.36 65.175L251.54 42.497Z"/>
</svg>
);
const fetchStackerNews = async () => {
const response = await axios.get('/api/stackernews');
return response.data.data.items.items; // Note the change here
const response = await axios.get('/api/stackernews');
return response.data.data.items.items;
};
const StackerNewsFeed = ({ searchQuery }) => {
const { data: items, isLoading, error } = useQuery({queryKey: ['stackerNews'], queryFn: fetchStackerNews});
const { data: items, isLoading, error } = useQuery({queryKey: ['stackerNews'], queryFn: fetchStackerNews});
const windowWidth = useWindowWidth();
useEffect(() => {
console.log(items);
}, [items]);
if (isLoading) {
return (
<div className="h-[100vh] min-bottom-bar:w-[86vw] max-sidebar:w-[100vw]">
<ProgressSpinner className='w-full mt-24 mx-auto' />
</div>
);
}
if (isLoading) {
return (
<div className="h-[100vh] min-bottom-bar:w-[86vw] max-sidebar:w-[100vw]">
<ProgressSpinner className='w-full mt-24 mx-auto' />
</div>
);
}
if (error) {
console.error('Error fetching Stacker News:', error);
return <div className="text-red-500 text-center p-4">Error loading data. Please try again later.</div>;
}
if (error) {
console.error('Error fetching Stacker News:', error);
return <div className="text-red-500 text-center p-4">Error loading data. Please try again later.</div>;
}
const filteredItems = items.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase())
);
const header = (item) => (
<div className="flex flex-row w-full items-center justify-between p-4 bg-gray-800 rounded-t-lg">
<div className="flex flex-row items-center">
<Avatar icon="pi pi-user" shape="circle" size="large" className="border-2 border-blue-400" />
<p className="pl-4 font-bold text-xl text-white">{item.user.name}</p>
</div>
<div className="flex flex-col items-start justify-between">
<div className="flex flex-row w-full justify-between items-center my-1 max-sidebar:flex-col max-sidebar:items-start">
<Tag value="~devs" severity="contrast" className="w-fit text-[#f8f8ff] mr-2 max-sidebar:mr-0" />
<Tag icon={<StackerNewsIconComponent />} value="stackernews" className="w-fit bg-gray-600 text-[#f8f8ff] max-sidebar:mt-1" />
</div>
</div>
</div>
);
const footer = (item) => (
<div className="w-full flex justify-between items-center">
<span className="bg-gray-800 rounded-lg p-2 text-sm text-gray-300">
{new Date(item.createdAt).toLocaleString()}
</span>
<Button
label="View on Stacker News"
icon="pi pi-external-link"
severity="warning"
outlined
size="small"
className='my-2'
onClick={() => window.open(`https://stacker.news/items/${item.id}`, '_blank')}
/>
</div>
);
const filteredItems = items.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[86vw]">
<div className="mx-4 mt-4">
{filteredItems && filteredItems.length > 0 ? (
filteredItems.map(item => (
<Card
key={item.id}
header={() => header(item)}
footer={() => footer(item)}
className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4"
>
<h3 className="m-0 text-lg text-gray-200">{highlightText(item.title, searchQuery)}</h3>
<p className="text-sm text-gray-400">
Comments: {item.comments.length} | Sats: {item.sats}
</p>
</Card>
))
) : (
<div className="text-gray-400 text-center p-4">No items available.</div>
)}
</div>
</div>
);
return (
<div className="bg-gray-900 h-full w-full min-bottom-bar:w-[86vw]">
<div className="mx-4 mt-4">
{filteredItems && filteredItems.length > 0 ? (
filteredItems.map(item => (
<CommunityMessage
key={item.id}
message={{
id: item.id,
author: item.user.name,
avatar: item.user.image,
content: item.title,
timestamp: item.createdAt,
channel: "~devs",
additionalContent: `Comments: ${item.comments.length} | Sats: ${item.sats}`
}}
searchQuery={searchQuery}
windowWidth={windowWidth}
platform="stackernews"
platformIcon={<StackerNewsIconComponent />}
platformLink={`https://stacker.news/items/${item.id}`}
/>
))
) : (
<div className="text-gray-400 text-center p-4">No items available.</div>
)}
</div>
</div>
);
};
export default StackerNewsFeed;

View File

@ -0,0 +1,95 @@
import React from 'react';
import { Card } from 'primereact/card';
import { Avatar } from 'primereact/avatar';
import { Tag } from 'primereact/tag';
import GenericButton from '@/components/buttons/GenericButton';
import { highlightText } from '@/utils/text';
import NostrIcon from '../../../../public/images/nostr.png';
import Image from 'next/image';
import { nip19 } from 'nostr-tools';
const StackerNewsIconComponent = () => (
<svg width="16" height="16" className='mr-2' viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#facc15" fillRule="evenodd" d="m41.7 91.4 41.644 59.22-78.966 69.228L129.25 155.94l-44.083-58.14 54.353-65.441Z"/>
<path fill="#facc15" fillRule="evenodd" d="m208.355 136.74-54.358-64.36-38.4 128.449 48.675-74.094 64.36 65.175L251.54 42.497Z"/>
</svg>
);
const CommunityMessage = ({ message, searchQuery, windowWidth, platform }) => {
const header = (
<div className="flex flex-row w-full items-center justify-between p-4 bg-gray-800 rounded-t-lg">
<div className="flex flex-row items-center">
<Avatar image={message.avatar} shape="circle" size="large" className="border-2 border-blue-400" />
<p className="pl-4 font-bold text-xl text-white">{message.author}</p>
</div>
<div className="flex flex-col items-start justify-between">
<div className="flex flex-row w-full justify-between items-center my-1 max-sidebar:flex-col max-sidebar:items-start">
<Tag value={message.channel} severity="primary" className="w-fit text-[#f8f8ff] bg-gray-600 mr-2 max-sidebar:mr-0" />
<Tag icon={getPlatformIcon(platform)} value={platform} className="w-fit text-[#f8f8ff] bg-blue-400 max-sidebar:mt-1" />
</div>
</div>
</div>
);
const footer = (
<div className="w-full flex justify-between items-center">
<span className="rounded-lg text-sm text-gray-300">
{new Date(message.timestamp).toLocaleString()}
</span>
<GenericButton
label={windowWidth > 768 ? `View in ${platform}` : null}
icon="pi pi-external-link"
outlined
size="small"
className='my-2'
onClick={() => window.open(getPlatformLink(platform, message.id), '_blank')}
tooltip={windowWidth < 768 ? `View in ${platform}` : null}
tooltipOptions={{ position: 'left' }}
/>
</div>
);
return (
<Card
header={header}
footer={footer}
className="w-full bg-gray-700 shadow-lg hover:shadow-xl transition-shadow duration-300 mb-4"
pt={{
footer: {
className: 'mt-0 pt-0'
},
content: {
className: 'mt-0 pt-0'
}
}}
>
<p className="m-0 text-lg text-gray-200 break-words">{highlightText(message.content, searchQuery)}</p>
</Card>
);
};
const getPlatformLink = (platform, id) => {
switch (platform) {
case 'discord':
return "https://discord.gg/t8DCMcq39d";
case 'stackernews':
return `https://stacker.news/items/${id}`;
case 'nostr':
return `https://nostr.band/${nip19.noteEncode(id)}`;
default:
return "#";
}
};
const getPlatformIcon = (platform) => {
switch (platform) {
case 'stackernews':
return <StackerNewsIconComponent />;
case 'nostr':
return <Image src={NostrIcon} alt="Nostr" width={16} height={16} className='mr-1' />;
default:
return `pi pi-${platform}`;
}
};
export default CommunityMessage;

View File

@ -4,8 +4,8 @@ import { InputText } from "primereact/inputtext";
import { InputTextarea } from "primereact/inputtextarea";
import { InputNumber } from "primereact/inputnumber";
import { InputSwitch } from "primereact/inputswitch";
import { Button } from "primereact/button";
import { useRouter } from "next/router";;
import GenericButton from "@/components/buttons/GenericButton";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { useToast } from "@/hooks/useToast";
import { useNDKContext } from "@/context/NDKContext";
@ -263,12 +263,12 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
<div className="p-inputgroup flex-1" key={index}>
<InputText value={link} onChange={(e) => handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addAdditionalLink} />
<GenericButton icon="pi pi-plus" onClick={addAdditionalLink} />
</div>
<Tooltip target=".pi-info-circle" />
</div>
@ -277,16 +277,16 @@ const ResourceForm = ({ draft = null, isPublished = false }) => {
<div className="p-inputgroup flex-1" key={index}>
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addTopic} />
<GenericButton icon="pi pi-plus" onClick={addTopic} />
</div>
</div>
<div className="flex justify-center mt-8">
<Button type="submit" severity="success" outlined label={draft ? "Update Draft" : "Save Draft"} />
<GenericButton type="submit" severity="success" outlined label={draft ? "Update Draft" : "Save Draft"} />
</div>
</form>
);

View File

@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { InputSwitch } from 'primereact/inputswitch';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
import 'primeicons/primeicons.css';
@ -179,12 +179,12 @@ const WorkshopForm = ({ draft = null }) => {
<div className="p-inputgroup flex-1" key={index}>
<InputText value={link} onChange={(e) => handleLinkChange(index, e.target.value)} placeholder="https://example.com" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeLink(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeLink(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addLink} />
<GenericButton icon="pi pi-plus" onClick={addLink} />
</div>
<Tooltip target=".pi-info-circle" />
</div>
@ -193,16 +193,16 @@ const WorkshopForm = ({ draft = null }) => {
<div className="p-inputgroup flex-1" key={index}>
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addTopic} />
<GenericButton icon="pi pi-plus" onClick={addTopic} />
</div>
</div>
<div className="flex justify-center mt-8">
<Button type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
<GenericButton type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
</div>
</form>
);

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { InputSwitch } from 'primereact/inputswitch';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { ProgressSpinner } from 'primereact/progressspinner';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
@ -213,14 +213,14 @@ const CourseForm = ({ draft = null }) => {
<div key={index} className="p-inputgroup flex-1 mt-4">
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder={`Topic #${index + 1}`} className="w-full" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={() => removeTopic(index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={() => removeTopic(index)} />
)}
</div>
))}
<Button type="button" icon="pi pi-plus" onClick={addTopic} className="p-button-outlined mt-2" />
<GenericButton icon="pi pi-plus" onClick={addTopic} className="p-button-outlined mt-2" />
</div>
<div className="flex justify-center mt-8">
<Button type="submit" label={draft ? 'Update Course Draft' : 'Create Course Draft'} className="p-button-raised p-button-success" />
<GenericButton type="submit" severity="success" outlined label={draft ? 'Update Course Draft' : 'Create Course Draft'} />
</div>
</form>
);

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Dropdown } from 'primereact/dropdown';
import { Button } from 'primereact/button';
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';
@ -141,7 +141,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
return (
<div className="flex justify-between items-center">
<p>Lesson {index + 1}</p>
<Button icon="pi pi-times" className="p-button-danger" onClick={() => removeLesson(index)} />
<GenericButton icon="pi pi-times" className="p-button-danger" onClick={() => removeLesson(index)} />
</div>
);
};
@ -167,8 +167,8 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
<div className="flex mt-4">
{lesson.id ? null : (
<>
<Button label="New Resource" onClick={(e) => {e.preventDefault(); setShowResourceForm(true)}} className="mr-2" />
<Button label="New Workshop" onClick={(e) => {e.preventDefault(); setShowWorkshopForm(true)}} className="mr-2" />
<GenericButton label="New Resource" onClick={(e) => {e.preventDefault(); setShowResourceForm(true)}} className="mr-2" />
<GenericButton label="New Workshop" onClick={(e) => {e.preventDefault(); setShowWorkshopForm(true)}} className="mr-2" />
</>
)}
</div>
@ -183,7 +183,7 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent, onNewRe
</AccordionTab>
))}
</Accordion>
<Button
<GenericButton
label="Add New Lesson"
onClick={addNewLesson}
className="mt-4"

View File

@ -3,7 +3,7 @@ import axios from "axios";
import { InputText } from "primereact/inputtext";
import { InputNumber } from "primereact/inputnumber";
import { InputSwitch } from "primereact/inputswitch";
import { Button } from "primereact/button";
import GenericButton from "@/components/buttons/GenericButton";
import { useSession } from "next-auth/react";
import { useToast } from "@/hooks/useToast";
import { useNDKContext } from "@/context/NDKContext";
@ -194,12 +194,12 @@ const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPai
<div className="p-inputgroup flex-1" key={index}>
<InputText value={link} onChange={(e) => handleAdditionalLinkChange(index, e.target.value)} placeholder="https://plebdevs.com" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeAdditionalLink(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addAdditionalLink} />
<GenericButton icon="pi pi-plus" onClick={addAdditionalLink} />
</div>
<Tooltip target=".pi-info-circle" />
</div>
@ -208,16 +208,16 @@ const EmbeddedResourceForm = ({ draft = null, isPublished = false, onSave, isPai
<div className="p-inputgroup flex-1" key={index}>
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addTopic} />
<GenericButton icon="pi pi-plus" onClick={addTopic} />
</div>
</div>
<div className="flex justify-center mt-8">
<Button type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
<GenericButton type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
</div>
</form>
);

View File

@ -1,9 +1,8 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import GenericButton from '@/components/buttons/GenericButton';
import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { InputSwitch } from 'primereact/inputswitch';
import { Button } from 'primereact/button';
import { useToast } from '@/hooks/useToast';
import { useSession } from 'next-auth/react';
import 'primeicons/primeicons.css';
@ -156,12 +155,12 @@ const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
<div className="p-inputgroup flex-1" key={index}>
<InputText value={link} onChange={(e) => handleLinkChange(index, e.target.value)} placeholder="https://example.com" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeLink(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeLink(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addLink} />
<GenericButton icon="pi pi-plus" onClick={addLink} />
</div>
<Tooltip target=".pi-info-circle" />
</div>
@ -170,16 +169,16 @@ const EmbeddedWorkshopForm = ({ draft = null, onSave, isPaid }) => {
<div className="p-inputgroup flex-1" key={index}>
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
<GenericButton icon="pi pi-times" className="p-button-danger mt-2" onClick={(e) => removeTopic(e, index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addTopic} />
<GenericButton icon="pi pi-plus" onClick={addTopic} />
</div>
</div>
<div className="flex justify-center mt-8">
<Button type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
<GenericButton type="submit" severity="success" outlined label={draft ? "Update" : "Submit"} />
</div>
</form>
);

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Button } from 'primereact/button';
import { TabMenu } from 'primereact/tabmenu';
import GenericButton from '@/components/buttons/GenericButton';
import Image from 'next/image';
import StackerNewsIcon from '../../../public/images/sn.svg';
import NostrIcon from '../../../public/images/nostr.png';
@ -35,12 +35,12 @@ const CommunityMenuTab = ({ selectedTopic, onTabChange }) => {
return {
label: (
<Button
<GenericButton
className={`${selectedTopic === item ? 'bg-primary text-white' : ''}`}
onClick={() => onTabChange(item)}
outlined={selectedTopic !== item}
rounded
size='small'
size="small"
label={item}
icon={icon}
/>

View File

@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useImageProxy } from '@/hooks/useImageProxy';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { Menu } from 'primereact/menu';
import useWindowWidth from '@/hooks/useWindowWidth';
import { useSession, signOut } from 'next-auth/react';
@ -87,7 +87,7 @@ const UserAvatar = () => {
} else {
userAvatar = (
<div className='flex flex-row items-center justify-between'>
<Button severity='help' rounded label="About" className='text-[#f8f8ff] mr-4' onClick={() => setVisible(true)} size={windowWidth < 768 ? 'small' : 'normal'} />
<GenericButton severity='help' rounded label="About" className='text-[#f8f8ff] mr-4' onClick={() => setVisible(true)} size={windowWidth < 768 ? 'small' : 'normal'} />
<Dialog header="About" visible={visible} onHide={() => { if (!visible) return; setVisible(false); }} className='w-[50vw] max-tab:w-[95vw]'>
<div className="space-y-6">
<p className="text-lg"><i className="pi pi-info-circle mr-2"></i>PlebDevs is a custom-built education platform designed to help aspiring developers, with a special focus on Bitcoin Lightning and Nostr technologies.</p>
@ -136,7 +136,7 @@ const UserAvatar = () => {
<p className="italic text-lg"><i className="pi pi-flag mr-2"></i>PlebDevs aims to provide a comprehensive, decentralized learning experience for aspiring developers, with a strong emphasis on emerging technologies in the Bitcoin ecosystem.</p>
</div>
</Dialog>
<Button
<GenericButton
label="Login"
icon="pi pi-user"
className="text-[#f8f8ff]"

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { Button } from "primereact/button";
import GenericButton from "@/components/buttons/GenericButton";
import MenuTab from "@/components/menutab/MenuTab";
import { useCourses } from "@/hooks/nostr/useCourses";
import { useResources } from "@/hooks/nostr/useResources";
@ -13,6 +13,7 @@ import { useToast } from "@/hooks/useToast";
import { Divider } from "primereact/divider";
import ContentList from "@/components/content/lists/ContentList";
import { parseEvent } from "@/utils/nostr";
import { ProgressSpinner } from "primereact/progressspinner";
import { useNDKContext } from "@/context/NDKContext";
const AUTHOR_PUBKEY = process.env.NEXT_PUBLIC_AUTHOR_PUBKEY;
@ -122,7 +123,7 @@ const UserContent = () => {
activeIndex={activeIndex}
onTabChange={setActiveIndex}
/>
<Button
<GenericButton
onClick={() => router.push("/create")}
label="Create"
severity="success"
@ -134,7 +135,7 @@ const UserContent = () => {
<div className="w-full mx-auto my-8">
<div className="w-full mx-auto px-8 max-tab:px-0">
{isLoading ? (
<p>Loading...</p>
<ProgressSpinner className="w-full mx-auto" />
) : isError ? (
<p>Error loading content.</p>
) : content.length > 0 ? (

View File

@ -1,5 +1,5 @@
import React, { useRef, useState, useEffect } from "react";
import { Button } from "primereact/button";
import GenericButton from "@/components/buttons/GenericButton";
import { DataTable } from "primereact/datatable";
import { Column } from "primereact/column";
import { useImageProxy } from "@/hooks/useImageProxy";
@ -28,8 +28,13 @@ const UserSettings = () => {
"wss://relay.nostr.band/",
"wss://nostr.mutinywallet.com/",
"wss://relay.mutinywallet.com/",
"wss://relay.primal.net/"
];
"wss://relay.primal.net/",
"wss://nostr21.com/",
"wss://nostrue.com/",
"wss://nostr.band/",
"wss://nostr.land/",
"wss://purplerelay.com/",
];
const relayStatusBody = (url) => {
// Placeholder for relay status, replace with actual logic later
@ -42,8 +47,8 @@ const UserSettings = () => {
const relayActionsBody = () => {
return (
<div>
<Button icon="pi pi-plus" className="p-button-rounded p-button-success p-button-text mr-2" />
<Button icon="pi pi-trash" className="p-button-rounded p-button-danger p-button-text" />
<GenericButton icon="pi pi-plus" className="p-button-rounded p-button-success p-button-text mr-2" />
<GenericButton icon="pi pi-trash" className="p-button-rounded p-button-danger p-button-text" />
</div>
);
};
@ -51,7 +56,7 @@ const UserSettings = () => {
const header = (
<div className="flex flex-row justify-between">
<span className="text-xl text-900 font-bold text-[#f8f8ff]">Relays</span>
<Button icon="pi pi-plus" className="p-button-rounded p-button-success p-button-text mr-2" />
<GenericButton icon="pi pi-plus" className="p-button-rounded p-button-success p-button-text mr-2" />
</div>
);

View File

@ -8,7 +8,7 @@ import { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast';
import { Card } from 'primereact/card';
import { Badge } from 'primereact/badge';
import { Button } from "primereact/button";
import GenericButton from '@/components/buttons/GenericButton';
import { Menu } from "primereact/menu";
import { Message } from "primereact/message";
@ -126,7 +126,7 @@ const SubscribeModal = ({ user }) => {
{(!subscribed && !subscriptionExpiredAt) && (
<div className="flex flex-col">
<Message className="w-fit" severity="info" text="You currently have no active subscription" />
<Button
<GenericButton
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={() => setVisible(true)}
@ -136,7 +136,7 @@ const SubscribeModal = ({ user }) => {
{subscriptionExpiredAt && (
<div className="flex flex-col">
<Message className="w-fit" severity="warn" text={`Your subscription expired on ${subscriptionExpiredAt.toLocaleDateString()}`} />
<Button
<GenericButton
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={() => setVisible(true)}

View File

@ -5,6 +5,7 @@ import { useSession, signOut } from 'next-auth/react';
import { Button } from 'primereact/button';
import 'primeicons/primeicons.css';
import styles from "./sidebar.module.css";
import { Divider } from 'primereact/divider';
const Sidebar = () => {
const [isExpanded, setIsExpanded] = useState(true);
@ -102,6 +103,7 @@ const Sidebar = () => {
</div>
)}
</div>
<Divider className='pt-0 mt-0' />
<div className='mt-auto'>
{isExpanded ? (
<div className='mt-auto'>

View File

@ -11,7 +11,12 @@ const relayUrls = [
"wss://relay.nostr.band/",
"wss://nostr.mutinywallet.com/",
"wss://relay.mutinywallet.com/",
"wss://relay.primal.net/"
"wss://relay.primal.net/",
"wss://nostr21.com/",
"wss://nostrue.com/",
"wss://nostr.band/",
"wss://nostr.land/",
"wss://purplerelay.com/",
];
export const NDKProvider = ({ children }) => {

View File

@ -9,18 +9,18 @@ export function useDraftsQuery() {
const [user, setUser] = useState(null);
useEffect(() => {
if (session) {
setIsClient(true);
}, []);
useEffect(() => {
if (session?.user) {
setUser(session.user);
}
}, [session]);
useEffect(() => {
setIsClient(true);
}, []);
const fetchDraftsDB = async () => {
try {
if (!user.id) {
if (!user?.id) {
return [];
}
const response = await axios.get(`/api/drafts/all/${user.id}`);
@ -34,11 +34,9 @@ export function useDraftsQuery() {
};
const { data: drafts, isLoading: draftsLoading, error: draftsError, refetch: refetchDrafts } = useQuery({
queryKey: ['drafts', isClient],
queryKey: ['drafts', isClient, user?.id],
queryFn: fetchDraftsDB,
// staleTime: 1000 * 60 * 30, // 30 minutes
// refetchInterval: 1000 * 60 * 30, // 30 minutes
enabled: isClient && !!user.id, // Only enable if client-side and user ID is available
enabled: isClient && !!user?.id,
});
return { drafts, draftsLoading, draftsError, refetchDrafts };

View File

@ -1,7 +1,7 @@
import { signIn, useSession } from "next-auth/react"
import { useState, useEffect } from "react"
import { useNDKContext } from "@/context/NDKContext";
import { Button } from 'primereact/button';
import GenericButton from "@/components/buttons/GenericButton";
import { InputText } from 'primereact/inputtext';
export default function SignIn() {
@ -43,21 +43,21 @@ export default function SignIn() {
return (
<div className="w-[100vw] min-bottom-bar:w-[86vw] mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Sign In</h1>
<Button
<GenericButton
label={"login with nostr"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleNostrSignIn}
/>
<Button
<GenericButton
label={"login anonymously"}
icon="pi pi-user"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
rounded
onClick={handleAnonymousSignIn}
/>
<Button
<GenericButton
label={"login with email"}
icon="pi pi-envelope"
className="text-[#f8f8ff] w-[250px] my-4 mx-auto"
@ -73,7 +73,7 @@ export default function SignIn() {
placeholder="Enter your email"
className="w-[250px] my-4"
/>
<Button
<GenericButton
type="submit"
label={"Submit"}
icon="pi pi-check"

View File

@ -7,7 +7,7 @@ import { useCourses } from '@/hooks/nostr/useCourses';
import { TabMenu } from 'primereact/tabmenu';
import 'primeicons/primeicons.css';
import { InputText } from 'primereact/inputtext';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { useRouter } from 'next/router';
const MenuTab = ({ items, selectedTopic, onTabChange }) => {
@ -26,7 +26,7 @@ const MenuTab = ({ items, selectedTopic, onTabChange }) => {
return {
label: (
<Button
<GenericButton
className={`${isActive ? 'bg-primary text-white' : ''}`}
onClick={() => {
onTabChange(item);

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useRouter } from 'next/router';
import { parseEvent, findKind0Fields } from '@/utils/nostr';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { nip19, nip04 } from 'nostr-tools';
import { useSession } from 'next-auth/react';
import dynamic from 'next/dynamic';
@ -247,8 +247,8 @@ export default function Details() {
{authorView && (
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
<div className='w-fit flex flex-row justify-between'>
<Button onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
<GenericButton onClick={() => router.push(`/details/${processedEvent.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
</div>
</div>
)}

View File

@ -6,7 +6,7 @@ import { nip19, nip04 } from 'nostr-tools';
import { v4 as uuidv4 } from 'uuid';
import { useSession } from 'next-auth/react';
import { useImageProxy } from '@/hooks/useImageProxy';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { useToast } from '@/hooks/useToast';
import { Tag } from 'primereact/tag';
import { useNDKContext } from '@/context/NDKContext';
@ -314,9 +314,9 @@ export default function Draft() {
</div>
<div className='w-[75vw] mx-auto flex flex-row justify-end mt-12'>
<div className='w-fit flex flex-row justify-between'>
<Button onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto m-2" />
<Button onClick={() => router.push(`/draft/${draft?.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<Button onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
<GenericButton onClick={handleSubmit} label="Publish" severity='success' outlined className="w-auto m-2" />
<GenericButton onClick={() => router.push(`/draft/${draft?.id}/edit`)} label="Edit" severity='warning' outlined className="w-auto m-2" />
<GenericButton onClick={handleDelete} label="Delete" severity='danger' outlined className="w-auto m-2 mr-0" />
</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]'>

View File

@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
import MessageInput from '@/components/feeds/MessageInput';
import StackerNewsIcon from '../../public/images/sn.svg';
import NostrIcon from '../../public/images/nostr.png';
import { Button } from 'primereact/button';
import GenericButton from '@/components/buttons/GenericButton';
import { Divider } from 'primereact/divider';
const Feed = () => {
@ -59,7 +59,7 @@ const Feed = () => {
<div className="w-[100vw] min-bottom-bar:w-[86vw] px-4 pt-4 flex flex-col items-start">
<div className='mb-4 flex flex-row items-end'>
<h2 className="font-bold mb-0">Community</h2>
<Button
<GenericButton
icon={getTagIcon(title)}
className='ml-2 text-sm p-2 py-1 flex items-center cursor-default hover:bg-transparent'
outlined