frontend tab changes

This commit is contained in:
austinkelsay 2024-09-04 17:09:46 -05:00
parent 6db4f4939c
commit c54f0cfed2
12 changed files with 704 additions and 295 deletions

View File

@ -18,7 +18,7 @@ const PaymentModal = dynamic(
{ ssr: false }
);
const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptionSuccess, setIsProcessing }) => {
const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptionSuccess, setIsProcessing, oneTime = false, recurring = false }) => {
const [invoice, setInvoice] = useState(null);
const [showRecurringOptions, setShowRecurringOptions] = useState(false);
const [nwcInput, setNwcInput] = useState('');
@ -207,31 +207,35 @@ const SubscriptionPaymentButtons = ({ onSuccess, onError, onRecurringSubscriptio
<>
{!invoice && (
<div className="w-full flex flex-row justify-between">
<Button
label="Pay as you go"
icon="pi pi-bolt"
onClick={async () => {
const invoice = await fetchInvoice();
setInvoice(invoice);
}}
severity='primary'
className="w-fit mt-4 text-[#f8f8ff]"
/>
<Button
label="Setup Recurring Subscription"
icon={
<Image
src="/images/nwc-logo.svg"
alt="NWC Logo"
width={16}
height={16}
className="mr-2"
/>
}
severity='help'
className="w-fit mt-4 text-[#f8f8ff] bg-purple-600"
onClick={() => setShowRecurringOptions(!showRecurringOptions)}
/>
{(oneTime || (!oneTime && !recurring)) && (
<Button
label="Pay as you go"
icon="pi pi-bolt"
onClick={async () => {
const invoice = await fetchInvoice();
setInvoice(invoice);
}}
severity='primary'
className="w-fit mt-4 text-[#f8f8ff]"
/>
)}
{(recurring || (!oneTime && !recurring)) && (
<Button
label="Setup Recurring Subscription"
icon={
<Image
src="/images/nwc-logo.svg"
alt="NWC Logo"
width={16}
height={16}
className="mr-2"
/>
}
severity='help'
className="w-fit mt-4 text-[#f8f8ff] bg-purple-600"
onClick={() => setShowRecurringOptions(!showRecurringOptions)}
/>
)}
</div>
)}
{showRecurringOptions && (

View File

@ -6,7 +6,7 @@ import { useNDKContext } from "@/context/NDKContext";
import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useToast } from '@/hooks/useToast';
const MessageInput = ({ collapsed, onToggle, onMessageSent }) => {
const MessageInput = ({ onMessageSent }) => {
const [message, setMessage] = useState('');
const { ndk, addSigner } = useNDKContext();
const { showToast } = useToast();
@ -34,7 +34,7 @@ const MessageInput = ({ collapsed, onToggle, onMessageSent }) => {
};
return (
<Panel header={null} toggleable collapsed={collapsed} onToggle={onToggle} className="w-full" pt={{
<Panel header={null} toggleable collapsed={false} className="w-full" pt={{
header: {
className: 'bg-transparent',
border: 'none',
@ -51,21 +51,20 @@ const MessageInput = ({ collapsed, onToggle, onMessageSent }) => {
<InputTextarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={5}
cols={30}
rows={2}
cols={10}
autoResize
placeholder="Type your message here..."
className="w-full"
/>
<div className="w-full flex flex-row justify-end">
<Button
label="Send"
icon="pi pi-send"
outlined
className='mt-2'
onClick={handleSubmit}
/>
</div>
</div>
<div className="w-full flex flex-row justify-end mt-4">
<Button
label="Send"
icon="pi pi-send"
outlined
onClick={handleSubmit}
/>
</div>
</Panel>
);

View File

@ -5,7 +5,7 @@ import { useImageProxy } from '@/hooks/useImageProxy';
import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu';
import useWindowWidth from '@/hooks/useWindowWidth';
import {useSession, signOut} from 'next-auth/react';
import { useSession, signOut } from 'next-auth/react';
import { Dialog } from 'primereact/dialog';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';
@ -55,7 +55,7 @@ const UserAvatar = () => {
{
label: 'Profile',
icon: 'pi pi-user',
command: () => router.push('/profile')
command: () => router.push('/profile?tab=profile')
},
{
label: 'Create',
@ -87,22 +87,62 @@ 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)} />
<Dialog header="Header" visible={visible} style={{ width: '50vw' }} onHide={() => {if (!visible) return; setVisible(false); }}>
<p className="m-0">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</Dialog>
<Button
label="Login"
icon="pi pi-user"
className="text-[#f8f8ff]"
rounded
onClick={() => router.push('/auth/signin')}
size={windowWidth < 768 ? 'small' : 'normal'}
<Button severity='help' rounded label="About" className='text-[#f8f8ff] mr-4' onClick={() => setVisible(true)} />
<Dialog header="About" visible={visible} style={{ width: '50vw' }} onHide={() => { if (!visible) return; setVisible(false); }}>
<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>
<div className="space-y-4">
<h3 className="text-xl font-semibold"><i className="pi pi-star mr-2"></i>Key Features:</h3>
<ul className="space-y-4">
<li><i className="pi pi-cloud mr-2"></i><span className="font-semibold">Content Distribution:</span> All educational content is published to Nostr and actively pulled from Nostr relays, ensuring decentralized and up-to-date information.</li>
<li>
<i className="pi pi-file-edit mr-2"></i><span className="font-semibold">Content Types:</span>
<ul className="list-disc list-inside ml-6 mt-2 space-y-1">
<li><span className="italic">Resources:</span> Markdown documents posted as NIP-23 long-form events on Nostr.</li>
<li><span className="italic">Workshops:</span> Enhanced markdown files with rich media support, including embedded videos, also saved as NIP-23 events.</li>
<li><span className="italic">Courses:</span> Nostr lists that combine multiple resources and workshops into a structured learning path.</li>
</ul>
</li>
<li>
<i className="pi pi-dollar mr-2"></i><span className="font-semibold">Monetization:</span>
<ul className="list-disc list-inside ml-6 mt-2 space-y-1">
<li>All content is zappable, allowing for micropayments.</li>
<li>Some content is &apos;paid&apos;, requiring either atomic payments or a subscription for access.</li>
<li>Subscription options include pay-as-you-go and recurring payments via Nostr Wallet Connect.</li>
</ul>
</li>
<li><i className="pi pi-users mr-2"></i><span className="font-semibold">Community Engagement:</span> A dedicated community section pulls in relevant PlebDevs channels. Users can read all PlebDevs content and interact with the community via Nostr.</li>
<li>
<i className="pi pi-check-circle mr-2"></i><span className="font-semibold">Subscription Benefits:</span>
<ul className="list-disc list-inside ml-6 mt-2 space-y-1">
<li>Access to all content, including paid resources.</li>
<li>Exclusive 1:1 calendar for personalized support.</li>
<li>Access to exclusive channels.</li>
<li>Personal mentorship to ensure success in becoming a developer.</li>
</ul>
</li>
<li><i className="pi pi-cog mr-2"></i><span className="font-semibold">Technology Stack:</span> The platform leverages Nostr for content distribution and community interaction, and Bitcoin Lightning Network for micropayments and subscriptions.</li>
<li><i className="pi pi-user mr-2"></i><span className="font-semibold">User Experience:</span> Seamless integration of learning resources, community engagement, and payment systems, with a focus on practical skills development in Bitcoin, Lightning, and Nostr technologies.</li>
</ul>
</div>
<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
label="Login"
icon="pi pi-user"
className="text-[#f8f8ff]"
rounded
onClick={() => router.push('/auth/signin')}
size={windowWidth < 768 ? 'small' : 'normal'}
/>
</div>
);

View File

@ -114,7 +114,7 @@ const UserContent = () => {
return (
<div className="w-full min-bottom-bar:w-[87vw] mx-auto">
<div className="border-y-2 border-gray-300 mt-12">
<div className="border-b-2 border-gray-300 mt-8">
<h2 className="text-center my-4">Your Content</h2>
</div>
<div className="flex flex-row w-full justify-between px-4">

View File

@ -0,0 +1,116 @@
import React, { useRef, useState, useEffect } from "react";
import { DataTable } from "primereact/datatable";
import { Button } from "primereact/button";
import { Menu } from "primereact/menu";
import { Column } from "primereact/column";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useSession } from 'next-auth/react';
import { ProgressSpinner } from "primereact/progressspinner";
import PurchasedListItem from "@/components/profile/PurchasedListItem";
import { useNDKContext } from "@/context/NDKContext";
import { formatDateTime } from "@/utils/time";
import { findKind0Fields } from "@/utils/nostr";
import Image from "next/image";
import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect";
import UserContent from "@/components/profile/UserContent";
import SubscribeModal from "@/components/profile/subscription/SubscribeModal";
const UserProfile = () => {
const [user, setUser] = useState(null);
const { data: session } = useSession();
const { returnImageProxy } = useImageProxy();
const { ndk, addSigner } = useNDKContext();
const menu = useRef(null);
useEffect(() => {
if (session?.user) {
setUser(session.user);
}
}, [session]);
const menuItems = [
{
label: "Edit",
icon: "pi pi-pencil",
command: () => {
// Add your edit functionality here
},
},
{
label: "Delete",
icon: "pi pi-trash",
command: () => {
// Add your delete functionality here
},
},
];
const header = (
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
<span className="text-xl text-900 font-bold text-[#f8f8ff]">Purchases</span>
</div>
);
return (
user && (
<div className="h-full w-full min-bottom-bar:w-[87vw] max-sidebar:w-[100vw] mx-auto">
<div className="w-full flex flex-col justify-center mx-auto">
<div className="relative flex w-full items-center justify-center">
<Image
alt="user's avatar"
src={returnImageProxy(user.avatar, user.pubkey)}
width={100}
height={100}
className="rounded-full my-4"
/>
<i
className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75"
onClick={(e) => menu.current.toggle(e)}
></i>
<Menu model={menuItems} popup ref={menu} />
</div>
<h1 className="text-center text-2xl my-2">
{user.username || "Anon"}
</h1>
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
{user.pubkey}
</h2>
{user && (
<SubscribeModal user={user} />
)}
</div>
{!session || !session?.user || !ndk ? (
<ProgressSpinner />
) : (
<DataTable
emptyMessage="No purchases"
value={session.user?.purchased}
header={header}
style={{ maxWidth: "90%", margin: "0 auto", borderRadius: "10px" }}
pt={{
wrapper: {
className: "rounded-lg rounded-t-none"
},
header: {
className: "rounded-t-lg"
}
}}
>
<Column field="amountPaid" header="Cost"></Column>
<Column
body={(rowData) => {
console.log("rowData", rowData);
return <PurchasedListItem eventId={rowData?.resource?.noteId || rowData?.course?.noteId} category={rowData?.course ? "courses" : "resources"} />
}}
header="Name"
></Column>
<Column body={session.user?.purchased?.some((item) => item.courseId) ? "course" : "resource"} header="Category"></Column>
<Column body={rowData => formatDateTime(rowData?.createdAt)} header="Date"></Column>
</DataTable>
)}
</div>
)
);
};
export default UserProfile;

View File

@ -0,0 +1,108 @@
import React, { useRef, useState, useEffect } from "react";
import { Button } from "primereact/button";
import { DataTable } from "primereact/datatable";
import { Column } from "primereact/column";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useSession } from 'next-auth/react';
import { ProgressSpinner } from "primereact/progressspinner";
import { useNDKContext } from "@/context/NDKContext";
import Image from "next/image";
import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect";
const UserSettings = () => {
const [user, setUser] = useState(null);
const { data: session } = useSession();
const { returnImageProxy } = useImageProxy();
const { ndk } = useNDKContext();
useEffect(() => {
if (session?.user) {
setUser(session.user);
}
}, [session]);
const relayUrls = [
"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/"
];
const relayStatusBody = (url) => {
// Placeholder for relay status, replace with actual logic later
const isConnected = Math.random() > 0.5;
return (
<i className={`pi ${isConnected ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'}`}></i>
);
};
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" />
</div>
);
};
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" />
</div>
);
return (
user && (
<div className="h-full w-full min-bottom-bar:w-[87vw] max-sidebar:w-[100vw] mx-auto">
<div className="w-full flex flex-col justify-center mx-auto">
<div className="relative flex w-full items-center justify-center">
<Image
alt="user's avatar"
src={returnImageProxy(user.avatar, user.pubkey)}
width={100}
height={100}
className="rounded-full my-4"
/>
</div>
<h1 className="text-center text-2xl my-2">
{user.username || "Anon"}
</h1>
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
{user.pubkey}
</h2>
<div className="flex flex-col w-1/2 mx-auto my-8 mb-12 justify-between items-center">
<h2 className="text-xl my-2">Connect Your Lightning Wallet</h2>
<BitcoinConnectButton />
</div>
</div>
{!session || !session?.user || !ndk ? (
<ProgressSpinner />
) : (
<DataTable value={relayUrls}
style={{ maxWidth: "90%", margin: "0 auto", borderRadius: "10px" }}
header={header}
pt={{
wrapper: {
className: "rounded-lg rounded-t-none"
},
header: {
className: "rounded-t-lg"
}
}}
>
<Column field={(url) => url} header="Relay URL"></Column>
<Column body={relayStatusBody} header="Status"></Column>
<Column body={relayActionsBody} header="Actions"></Column>
</DataTable>
)}
</div>
)
);
};
export default UserSettings;

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Dialog } from 'primereact/dialog';
import { ProgressSpinner } from 'primereact/progressspinner';
import SubscriptionPaymentButtons from '@/components/bitcoinConnect/SubscriptionPaymentButton';
@ -8,13 +8,33 @@ 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 { Menu } from "primereact/menu";
import { Message } from "primereact/message";
// todo encrypt nwc before saving in db
const SubscribeModal = ({ visible, onHide }) => {
const SubscribeModal = ({ user }) => {
const { data: session, update } = useSession();
const { showToast } = useToast();
const router = useRouter();
const [isProcessing, setIsProcessing] = useState(false);
const [visible, setVisible] = useState(false);
const [subscribed, setSubscribed] = useState(false);
const [subscribedUntil, setSubscribedUntil] = useState(null);
const [subscriptionExpiredAt, setSubscriptionExpiredAt] = useState(null);
const menu = useRef(null);
useEffect(() => {
if (user && user.role) {
setSubscribed(user.role.subscribed);
const subscribedAt = new Date(user.role.lastPaymentAt);
const subscribedUntil = new Date(subscribedAt.getTime() + 31 * 24 * 60 * 60 * 1000);
setSubscribedUntil(subscribedUntil);
if (user.role.subscriptionExpiredAt) {
const expiredAt = new Date(user.role.subscriptionExpiredAt)
setSubscriptionExpiredAt(expiredAt);
}
}
}, [user]);
const handleSubscriptionSuccess = async (response) => {
setIsProcessing(true);
@ -58,55 +78,121 @@ const SubscribeModal = ({ visible, onHide }) => {
}
};
const menuItems = [
{
label: "Renew Subscription",
icon: "pi pi-bolt",
command: () => {
// Add your renew functionality here
},
},
{
label: "Schedule 1:1",
icon: "pi pi-calendar",
command: () => {
// Add your schedule functionality here
},
},
{
label: "Cancel Subscription",
icon: "pi pi-trash",
command: () => {
// Add your cancel functionality here
},
},
];
const subscriptionCardTitle = (
<div className="w-full flex flex-row justify-between items-center">
<span className="text-xl text-900 font-bold text-white">Plebdevs Subscription</span>
<i
className="pi pi-ellipsis-h text-2xl cursor-pointer hover:opacity-75"
onClick={(e) => menu.current.toggle(e)}
></i>
<Menu model={menuItems} popup ref={menu} />
</div>
);
return (
<Dialog
header="Subscribe to PlebDevs"
visible={visible}
onHide={onHide}
className="p-fluid pb-0 w-fit"
>
{isProcessing ? (
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
<ProgressSpinner />
<span className="ml-2">Processing subscription...</span>
</div>
) : (
<Card className="shadow-lg">
<div className="text-center mb-4">
<h2 className="text-2xl font-bold text-primary">Unlock Premium Benefits</h2>
<p className="text-gray-400">Subscribe now and elevate your development journey!</p>
<>
<Card title={subscriptionCardTitle} className="w-fit m-8 mx-auto">
{subscribed && (
<div className="flex flex-col">
<Message className="w-fit" severity="success" text="Subscribed!" />
<p className="mt-8">Thank you for your support 🎉</p>
<p className="text-sm text-gray-400">Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="flex items-center">
<i className="pi pi-book text-2xl text-primary mr-2"></i>
<span>Access ALL current and future content</span>
</div>
<div className="flex items-center">
<i className="pi pi-users text-2xl text-primary mr-2"></i>
<span>Join PlebLab Bitcoin Hackerspace Slack</span>
</div>
<div className="flex items-center">
<i className="pi pi-calendar text-2xl text-primary mr-2"></i>
<span>Exclusive 1:1 booking calendar</span>
</div>
<div className="flex items-center">
<i className="pi pi-star text-2xl text-primary mr-2"></i>
<span>Personal mentorship & guidance</span>
</div>
)}
{(!subscribed && !subscriptionExpiredAt) && (
<div className="flex flex-col">
<Message className="w-fit" severity="info" text="You currently have no active subscription" />
<Button
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={() => setVisible(true)}
/>
</div>
<div className="text-center mb-4 flex flex-row justify-center">
<Badge value="BONUS" severity="success" className="mr-2"></Badge>
<span className="text-center font-bold">I WILL MAKE SURE YOU WIN HARD AND LEVEL UP AS A DEV!</span>
)}
{subscriptionExpiredAt && (
<div className="flex flex-col">
<Message className="w-fit" severity="warn" text={`Your subscription expired on ${subscriptionExpiredAt.toLocaleDateString()}`} />
<Button
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={() => setVisible(true)}
/>
</div>
<SubscriptionPaymentButtons
onSuccess={handleSubscriptionSuccess}
onRecurringSubscriptionSuccess={handleRecurringSubscriptionSuccess}
onError={handleSubscriptionError}
setIsProcessing={setIsProcessing}
/>
</Card>
)}
</Dialog>
)}
</Card>
<Dialog
header="Subscribe to PlebDevs"
visible={visible}
onHide={() => setVisible(false)}
className="p-fluid pb-0 w-fit"
>
{isProcessing ? (
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
<ProgressSpinner />
<span className="ml-2">Processing subscription...</span>
</div>
) : (
<Card className="shadow-lg">
<div className="text-center mb-4">
<h2 className="text-2xl font-bold text-primary">Unlock Premium Benefits</h2>
<p className="text-gray-400">Subscribe now and elevate your development journey!</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="flex items-center">
<i className="pi pi-book text-2xl text-primary mr-2"></i>
<span>Access ALL current and future content</span>
</div>
<div className="flex items-center">
<i className="pi pi-users text-2xl text-primary mr-2"></i>
<span>Join PlebLab Bitcoin Hackerspace Slack</span>
</div>
<div className="flex items-center">
<i className="pi pi-calendar text-2xl text-primary mr-2"></i>
<span>Exclusive 1:1 booking calendar</span>
</div>
<div className="flex items-center">
<i className="pi pi-star text-2xl text-primary mr-2"></i>
<span>Personal mentorship & guidance</span>
</div>
</div>
<div className="text-center mb-4 flex flex-row justify-center">
<Badge value="BONUS" severity="success" className="mr-2"></Badge>
<span className="text-center font-bold">I WILL MAKE SURE YOU WIN HARD AND LEVEL UP AS A DEV!</span>
</div>
<SubscriptionPaymentButtons
onSuccess={handleSubscriptionSuccess}
onRecurringSubscriptionSuccess={handleRecurringSubscriptionSuccess}
onError={handleSubscriptionError}
setIsProcessing={setIsProcessing}
/>
</Card>
)}
</Dialog>
</>
);
};

View File

@ -1,12 +1,20 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useState, useRef, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useToast } from '@/hooks/useToast';
import axios from 'axios';
import { Card } from 'primereact/card';
import { Button } from "primereact/button";
import { Menu } from "primereact/menu";
import { Message } from "primereact/message";
import { Card } from "primereact/card";
import SubscribeModal from "@/components/profile/subscription/SubscribeModal";
import { ProgressSpinner } from 'primereact/progressspinner';
import SubscriptionPaymentButtons from '@/components/bitcoinConnect/SubscriptionPaymentButton';
const UserSubscription = ({ user }) => {
const [subscribeModalVisible, setSubscribeModalVisible] = useState(false);
const { data: session, update } = useSession();
const { showToast } = useToast();
const router = useRouter();
const [isProcessing, setIsProcessing] = useState(false);
const [subscribed, setSubscribed] = useState(false);
const [subscribedUntil, setSubscribedUntil] = useState(null);
const [subscriptionExpiredAt, setSubscriptionExpiredAt] = useState(null);
@ -25,34 +33,77 @@ const UserSubscription = ({ user }) => {
}
}, [user]);
const handleSubscriptionSuccess = async (paymentResponse) => {
setIsProcessing(true);
try {
const response = await axios.post('/api/subscription/create', {
paymentResponse,
});
if (response.data.success) {
showToast('success', 'Subscription successful!');
await update();
router.push('/dashboard');
} else {
showToast('error', 'Subscription failed. Please try again.');
}
} catch (error) {
console.error('Subscription error:', error);
showToast('error', 'An error occurred. Please try again.');
} finally {
setIsProcessing(false);
}
};
const handleSubscriptionError = (error) => {
console.error('Subscription error:', error);
showToast('error', 'An error occurred during subscription. Please try again.');
};
const handleRecurringSubscriptionSuccess = async (paymentResponse) => {
setIsProcessing(true);
try {
const response = await axios.post('/api/subscription/recurring', {
paymentResponse,
});
if (response.data.success) {
showToast('success', 'Recurring subscription set up successfully!');
await update();
router.push('/dashboard');
} else {
showToast('error', 'Failed to set up recurring subscription. Please try again.');
}
} catch (error) {
console.error('Recurring subscription error:', error);
showToast('error', 'An error occurred. Please try again.');
} finally {
setIsProcessing(false);
}
};
const menuItems = [
{
label: "Renew Subscription",
icon: "pi pi-bolt",
command: () => {
// Add your edit functionality here
// Add your renew functionality here
},
},
{
label: "Schedule 1:1",
icon: "pi pi-calendar",
command: () => {
// Add your edit functionality here
// Add your schedule functionality here
},
},
{
label: "Cancel Subscription",
icon: "pi pi-trash",
command: () => {
// Add your delete functionality here
// Add your cancel functionality here
},
},
];
const openSubscribeModal = () => {
setSubscribeModalVisible(true);
};
const subscriptionCardTitle = (
<div className="w-full flex flex-row justify-between items-center">
<span className="text-xl text-900 font-bold text-white">Plebdevs Subscription</span>
@ -65,41 +116,90 @@ const UserSubscription = ({ user }) => {
);
return (
<>
<Card title={subscriptionCardTitle} className="w-fit m-8 mx-auto">
<div className="p-4">
<h1 className="text-3xl font-bold mb-6">Subscription Management</h1>
<Card title={subscriptionCardTitle} className="mb-6">
{subscribed && (
<div className="flex flex-col">
<Message className="w-fit" severity="success" text="Subscribed!" />
<p className="mt-8">Thank you for your support 🎉</p>
<p className="mt-4">Thank you for your support 🎉</p>
<p className="text-sm text-gray-400">Pay-as-you-go subscription will renew on {subscribedUntil.toLocaleDateString()}</p>
</div>
)}
{(!subscribed && !subscriptionExpiredAt) && (
<div className="flex flex-col">
<Message className="w-fit" severity="info" text="You currently have no active subscription" />
<Button
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={openSubscribeModal}
/>
</div>
)}
{subscriptionExpiredAt && (
<div className="flex flex-col">
<Message className="w-fit" severity="warn" text={`Your subscription expired on ${subscriptionExpiredAt.toLocaleDateString()}`} />
<Button
label="Subscribe"
className="w-auto mt-8 text-[#f8f8ff]"
onClick={openSubscribeModal}
/>
</div>
)}
</Card>
<SubscribeModal
visible={subscribeModalVisible}
onHide={() => setSubscribeModalVisible(false)}
/>
</>
<Card title="Subscribe to PlebDevs" className="mb-6">
{isProcessing ? (
<div className="w-full flex flex-col mx-auto justify-center items-center mt-4">
<ProgressSpinner />
<span className="ml-2">Processing subscription...</span>
</div>
) : (
<div className="flex flex-col">
<h2 className="text-2xl font-semibold mb-4">Choose your subscription plan:</h2>
<div className="flex flex-col gap-4">
<Card className='bg-gray-900 w-fit'>
<h3 className="text-xl font-semibold mb-2">Monthly Subscription</h3>
<p className="mb-4">Get access to all PlebDevs features / content one month at a time.</p>
<SubscriptionPaymentButtons
onSuccess={handleSubscriptionSuccess}
onError={handleSubscriptionError}
amount={10}
currency="USD"
buttonText="Subscribe for $10/month"
oneTime={true}
/>
</Card>
<Card className='bg-gray-900 w-fit'>
<h3 className="text-xl font-semibold mb-2">Recurring Monthly Subscription</h3>
<p className="mb-4">Setup auto recurring monthly payments for uninterrupted access.</p>
<SubscriptionPaymentButtons
onSuccess={handleRecurringSubscriptionSuccess}
onError={handleSubscriptionError}
amount={10}
currency="USD"
buttonText="Set up recurring $10/month"
recurring={true}
/>
</Card>
</div>
</div>
)}
</Card>
<Card title="Subscription Benefits" className="mb-6">
<ul className="list-disc pl-6">
<li>Access to exclusive content</li>
<li>Priority support</li>
<li>Early access to new features</li>
<li>Community forums</li>
</ul>
</Card>
<Card title="Frequently Asked Questions" className="mb-6">
<div className="flex flex-col gap-4">
<div>
<h3 className="text-lg font-semibold">How does the subscription work?</h3>
<p>Our subscription provides monthly access to all PlebDevs features. You can choose between a one-time payment or a recurring subscription.</p>
</div>
<div>
<h3 className="text-lg font-semibold">Can I cancel my subscription?</h3>
<p>Yes, you can cancel your subscription at any time. Your access will remain active until the end of the current billing period.</p>
</div>
{/* Add more FAQ items as needed */}
</div>
</Card>
</div>
);
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { useRouter } from 'next/router';
import { useSession, signOut } from 'next-auth/react';
import 'primeicons/primeicons.css';
import styles from "./sidebar.module.css";
@ -13,40 +14,59 @@ const Sidebar = () => {
return pathWithQuery === path;
};
const { data: session } = useSession();
return (
<div className='max-sidebar:hidden w-[13vw] bg-gray-800 p-2 fixed h-[100%]'>
<div onClick={() => router.push('/')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-home pl-5" /> <p className="pl-2 rounded-md font-bold">Home</p>
<div className='max-sidebar:hidden w-[13vw] bg-gray-800 p-2 fixed h-[100%] flex flex-col'>
<div className="flex-grow overflow-y-auto">
<div onClick={() => router.push('/')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-home pl-5" /> <p className="pl-2 rounded-md font-bold">Home</p>
</div>
<div onClick={() => router.push('/content')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-video pl-5" /> <p className="pl-2 rounded-md font-bold">Content</p>
</div>
<div onClick={() => router.push('/create')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/create') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-plus pl-5" /> <p className="pl-2 rounded-md font-bold">Create</p>
</div>
<div onClick={() => session ? router.push('/profile?tab=subscribe') : router.push('/auth/signin')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/profile?tab=subscribe') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-star pl-5" /> <p className="pl-2 rounded-md font-bold">Subscribe</p>
</div>
<Accordion activeIndex={0} className={styles['p-accordion']}>
<AccordionTab
pt={{
headerAction: ({ context }) => ({
className: `hover:bg-gray-700 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''} ${styles['p-accordion-header-link']}`
}),
content: styles['p-accordion-content']
}}
header={"Community"}>
<div onClick={() => router.push('/feed?channel=global')} className={`w-full cursor-pointer py-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=global') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> global</p>
</div>
<div onClick={() => router.push('/feed?channel=nostr')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=nostr') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> nostr</p>
</div>
<div onClick={() => router.push('/feed?channel=discord')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=discord') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> discord</p>
</div>
<div onClick={() => router.push('/feed?channel=stackernews')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=stackernews') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> stackernews</p>
</div>
</AccordionTab>
</Accordion>
</div>
<div onClick={() => router.push('/content')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/content') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-video pl-5" /> <p className="pl-2 rounded-md font-bold">Content</p>
<div className='mt-auto'>
<div onClick={() => router.push('/profile?tab=settings')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/profile?tab=settings') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-cog pl-5" /> <p className="pl-2 rounded-md font-bold">Settings</p>
</div>
<div onClick={() => session ? signOut() : router.push('/auth/signin')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg`}>
<i className={`pi ${session ? 'pi-sign-out' : 'pi-sign-in'} pl-5`} /> <p className="pl-2 rounded-md font-bold">{session ? 'Logout' : 'Login'}</p>
</div>
{/* todo: have to add this extra button to push the sidebar to the right space but it doesnt seem to cause any negative side effects? */}
<div onClick={signOut} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg`}>
<i className="pi pi-sign-out pl-5" /> <p className="pl-2 rounded-md font-bold">Logout</p>
</div>
</div>
<div onClick={() => router.push('/create')} className={`w-full flex flex-row items-center cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/create') ? 'bg-gray-700' : ''}`}>
<i className="pi pi-plus pl-5" /> <p className="pl-2 rounded-md font-bold">Create</p>
</div>
<Accordion activeIndex={0} className={styles['p-accordion']}>
<AccordionTab
pt={{
headerAction: ({ context }) => ({
className: `hover:bg-gray-700 rounded-lg ${isActive('/feed') ? 'bg-gray-700' : ''} ${styles['p-accordion-header-link']}`
}),
content: styles['p-accordion-content']
}}
header={"Community"}>
<div onClick={() => router.push('/feed?channel=global')} className={`w-full cursor-pointer py-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=global') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> global</p>
</div>
<div onClick={() => router.push('/feed?channel=nostr')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=nostr') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> nostr</p>
</div>
<div onClick={() => router.push('/feed?channel=discord')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=discord') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> discord</p>
</div>
<div onClick={() => router.push('/feed?channel=stackernews')} className={`w-full cursor-pointer py-2 my-2 hover:bg-gray-700 rounded-lg ${isActive('/feed?channel=stackernews') ? 'bg-gray-700' : ''}`}>
<p className="pl-3 rounded-md font-bold"><i className="pi pi-hashtag text-sm"></i> stackernews</p>
</div>
</AccordionTab>
</Accordion>
</div>
);
};

View File

@ -45,7 +45,7 @@ export default function SignIn() {
}
return (
<div className="w-[100vw] mx-auto mt-24 flex flex-col justify-center">
<div className="w-[100vw] min-bottom-bar:w-[83vw] mx-auto mt-24 flex flex-col justify-center">
<h1 className="text-center mb-8">Sign In</h1>
<Button
label={"login with nostr"}

View File

@ -18,7 +18,6 @@ const Feed = () => {
const [searchQuery, setSearchQuery] = useState('');
const [title, setTitle] = useState('Community');
const allTopics = ['global', 'nostr', 'discord', 'stackernews'];
const [isMessageInputCollapsed, setIsMessageInputCollapsed] = useState(true);
const router = useRouter();
@ -51,10 +50,6 @@ const Feed = () => {
}
};
const toggleMessageInput = (e) => {
setIsMessageInputCollapsed(e.value);
};
const handleMessageSent = () => {
setIsMessageInputCollapsed(true);
};
@ -85,16 +80,10 @@ const Feed = () => {
icon="pi pi-search"
className="w-fit"
/>
<Button
className='text-[#f8f8ff]'
icon={isMessageInputCollapsed ? "pi pi-plus" : "pi pi-minus"}
onClick={() => setIsMessageInputCollapsed(!isMessageInputCollapsed)}
/>
</div>
<Divider />
<MessageInput
collapsed={isMessageInputCollapsed}
onToggle={toggleMessageInput}
collapsed={false}
onMessageSent={handleMessageSent}
/>
</div>

View File

@ -1,130 +1,77 @@
import React, { useRef, useState, useEffect } from "react";
import { Button } from "primereact/button";
import { DataTable } from "primereact/datatable";
import { Menu } from "primereact/menu";
import { Column } from "primereact/column";
import { Message } from "primereact/message";
import { Card } from "primereact/card";
import { useImageProxy } from "@/hooks/useImageProxy";
import { useSession } from 'next-auth/react';
import { ProgressSpinner } from "primereact/progressspinner";
import PurchasedListItem from "@/components/profile/PurchasedListItem";
import { useNDKContext } from "@/context/NDKContext";
import { formatDateTime } from "@/utils/time";
import React, { useState, useEffect } from "react";
import { TabView, TabPanel } from "primereact/tabview";
import UserProfile from "@/components/profile/UserProfile";
import UserSettings from "@/components/profile/UserSettings";
import UserContent from "@/components/profile/UserContent";
import Image from "next/image";
import BitcoinConnectButton from "@/components/bitcoinConnect/BitcoinConnect";
import UserSubscription from "@/components/profile/subscription/UserSubscription";
import { useRouter } from "next/router";
const Profile = () => {
const [user, setUser] = useState(null);
const { data: session } = useSession();
const { returnImageProxy } = useImageProxy();
const { ndk } = useNDKContext();
const menu = useRef(null);
const router = useRouter();
const [activeTab, setActiveTab] = useState(0);
const tabs = ["profile", "settings", "content", "subscribe"];
useEffect(() => {
if (session?.user) {
setUser(session.user);
const { tab } = router.query;
if (tab) {
const index = tabs.indexOf(tab.toLowerCase());
if (index !== -1) {
setActiveTab(index);
}
}
}, [session]);
}, [router.query]);
const menuItems = [
{
label: "Edit",
icon: "pi pi-pencil",
command: () => {
// Add your edit functionality here
},
},
{
label: "Delete",
icon: "pi pi-trash",
command: () => {
// Add your delete functionality here
},
},
];
const header = (
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
<span className="text-xl text-900 font-bold text-white">Purchases</span>
</div>
);
const openSubscribeModal = () => {
setSubscribeModalVisible(true);
const onTabChange = (e) => {
const newIndex = e.index;
setActiveTab(newIndex);
router.push(`/profile?tab=${tabs[newIndex]}`, undefined, { shallow: true });
};
const subscriptionCardTitle = (
<div className="w-full flex flex-row justify-between items-center">
<span className="text-xl text-900 font-bold text-white">Plebdevs Subscription</span>
<i
className="pi pi-ellipsis-h text-2xlcursor-pointer hover:opacity-75"
onClick={(e) => menu.current.toggle(e)}
></i>
<Menu model={menuItems} popup ref={menu} />
</div>
);
return (
user && (
<div className="h-full w-full min-bottom-bar:w-[87vw] max-sidebar:w-[100vw] mx-auto">
<div className="w-full flex flex-col justify-center mx-auto">
<div className="relative flex w-full items-center justify-center">
<Image
alt="user's avatar"
src={returnImageProxy(user.avatar, user.pubkey)}
width={100}
height={100}
className="rounded-full my-4"
/>
<i
className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75"
onClick={(e) => menu.current.toggle(e)}
></i>
<Menu model={menuItems} popup ref={menu} />
</div>
<h1 className="text-center text-2xl my-2">
{user.username || "Anon"}
</h1>
<h2 className="text-center text-xl my-2 truncate max-tab:px-4 max-mob:px-4">
{user.pubkey}
</h2>
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
<h2 className="text-xl my-2">Connect Your Lightning Wallet</h2>
<BitcoinConnectButton />
</div>
{user && (
<UserSubscription user={user} />
)}
</div>
{!session || !session?.user || !ndk ? (
<ProgressSpinner />
) : (
<DataTable
emptyMessage="No purchases"
value={session.user?.purchased}
tableStyle={{ minWidth: "100%" }}
header={header}
>
<Column field="amountPaid" header="Cost"></Column>
<Column
body={(rowData) => {
console.log("rowData", rowData);
return <PurchasedListItem eventId={rowData?.resource?.noteId || rowData?.course?.noteId} category={rowData?.course ? "courses" : "resources"} />
}}
header="Name"
></Column>
<Column body={session.user?.purchased?.some((item) => item.courseId) ? "course" : "resource"} header="Category"></Column>
<Column body={rowData => formatDateTime(rowData?.createdAt)} header="Date"></Column>
</DataTable>
)}
<UserContent />
</div>
)
<div className="w-full min-h-full min-bottom-bar:w-[87vw] mx-auto">
<TabView
pt={{
root: {
className: "bg-transparent",
},
panelContainer: {
className: "bg-transparent m-0 p-0"
}
}}
onTabChange={onTabChange}
activeIndex={activeTab}
>
<TabPanel header="Profile" pt={{
headerAction: {
className: "bg-transparent"
},
}}>
<UserProfile />
</TabPanel>
<TabPanel header="Settings" pt={{
headerAction: {
className: "bg-transparent"
},
}}>
<UserSettings />
</TabPanel>
<TabPanel header="Content" pt={{
headerAction: {
className: "bg-transparent"
},
}}>
<UserContent />
</TabPanel>
<TabPanel header="Subscribe" pt={{
headerAction: {
className: "bg-transparent"
},
}}>
<UserSubscription />
</TabPanel>
</TabView>
</div>
);
};