mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 08:42:02 +00:00
Full login /signup flow working, basic profile page and user avatar, pulling in resources from nostr and rendering
This commit is contained in:
parent
16513c71ec
commit
595c553dd2
@ -2,7 +2,7 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
domains: ['fakestoreapi.com'],
|
domains: ['localhost'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ CREATE TABLE "User" (
|
|||||||
"id" SERIAL NOT NULL,
|
"id" SERIAL NOT NULL,
|
||||||
"pubkey" TEXT NOT NULL,
|
"pubkey" TEXT NOT NULL,
|
||||||
"username" TEXT,
|
"username" TEXT,
|
||||||
|
"avatar" TEXT,
|
||||||
"roleId" INTEGER,
|
"roleId" INTEGER,
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
@ -11,6 +11,7 @@ model User {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
pubkey String @unique
|
pubkey String @unique
|
||||||
username String? @unique
|
username String? @unique
|
||||||
|
avatar String?
|
||||||
purchased Purchase[]
|
purchased Purchase[]
|
||||||
role Role? @relation(fields: [roleId], references: [id])
|
role Role? @relation(fields: [roleId], references: [id])
|
||||||
roleId Int?
|
roleId Int?
|
||||||
|
@ -1,32 +1,17 @@
|
|||||||
import React from 'react';
|
import React, {useRef} from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import UserAvatar from './user/UserAvatar';
|
||||||
import { Button } from 'primereact/button';
|
|
||||||
import { Menubar } from 'primereact/menubar';
|
import { Menubar } from 'primereact/menubar';
|
||||||
import { useSelector } from 'react-redux';
|
import { useRouter } from 'next/router';
|
||||||
import 'primereact/resources/primereact.min.css';
|
import 'primereact/resources/primereact.min.css';
|
||||||
import 'primeicons/primeicons.css';
|
import 'primeicons/primeicons.css';
|
||||||
import styles from './navbar.module.css';
|
import styles from './navbar.module.css';
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const user = useSelector((state) => state.user);
|
|
||||||
|
|
||||||
const end = (
|
|
||||||
(user && user?.username || user.pubkey) ?
|
|
||||||
<h1>{user.username || user.pubkey}</h1>
|
|
||||||
:
|
|
||||||
<Button
|
|
||||||
label={"Login"}
|
|
||||||
icon="pi pi-user"
|
|
||||||
className="text-[#f8f8ff]"
|
|
||||||
rounded
|
|
||||||
onClick={() => router.push('/login')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const start = (
|
const start = (
|
||||||
<div className={styles.titleContainer}>
|
<div onClick={() => router.push('/')} className={styles.titleContainer}>
|
||||||
<Image
|
<Image
|
||||||
alt="logo"
|
alt="logo"
|
||||||
src="/plebdevs-guy.jpg"
|
src="/plebdevs-guy.jpg"
|
||||||
@ -39,7 +24,7 @@ const Navbar = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menubar start={start} end={end} className='px-[5%]' />
|
<Menubar start={start} end={UserAvatar} className='px-[5%]' />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-left: 6%;
|
padding-left: 6%;
|
||||||
padding-right: 6%;
|
padding-right: 6%;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
81
src/components/navbar/user/UserAvatar.js
Normal file
81
src/components/navbar/user/UserAvatar.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, {useRef} from 'react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useImageProxy } from '@/hooks/useImageProxy';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { Menu } from 'primereact/menu';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { setUser } from '@/redux/reducers/userReducer';
|
||||||
|
import 'primereact/resources/primereact.min.css';
|
||||||
|
import 'primeicons/primeicons.css';
|
||||||
|
import styles from '../navbar.module.css';
|
||||||
|
|
||||||
|
const UserAvatar = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const user = useSelector((state) => state.user.user);
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
|
||||||
|
const menu = useRef(null);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
window.localStorage.removeItem('pubkey');
|
||||||
|
dispatch(setUser(null));
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
let userAvatar;
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
// User exists, show username or pubkey
|
||||||
|
const displayName = user.username || user.pubkey;
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: displayName,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Profile',
|
||||||
|
icon: 'pi pi-user',
|
||||||
|
command: () => router.push('/profile')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Logout',
|
||||||
|
icon: 'pi pi-power-off',
|
||||||
|
command: handleLogout
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
userAvatar = (
|
||||||
|
<>
|
||||||
|
<div onClick={(event) => menu.current.toggle(event)} className='flex flex-row items-center justify-between cursor-pointer hover:opacity-75'>
|
||||||
|
{user.avatar && (
|
||||||
|
<Image
|
||||||
|
alt="logo"
|
||||||
|
src={returnImageProxy(user.avatar)}
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
className={styles.logo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Menu model={items} popup ref={menu} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userAvatar = (
|
||||||
|
<Button
|
||||||
|
label="Login"
|
||||||
|
icon="pi pi-user"
|
||||||
|
className="text-[#f8f8ff]"
|
||||||
|
rounded
|
||||||
|
onClick={() => router.push('/login')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userAvatar;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserAvatar;
|
@ -3,60 +3,29 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { Button } from 'primereact/button';
|
import { Button } from 'primereact/button';
|
||||||
import { Carousel } from 'primereact/carousel';
|
import { Carousel } from 'primereact/carousel';
|
||||||
import { Tag } from 'primereact/tag';
|
import { Tag } from 'primereact/tag';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { parseResourceEvent } from '@/utils/nostr';
|
||||||
|
|
||||||
export default function ResourcesCarousel() {
|
export default function ResourcesCarousel() {
|
||||||
const [courses, setCourses] = useState([
|
const resources = useSelector((state) => state.events.resources);
|
||||||
{
|
|
||||||
"title": "Lightning Wallet Frontend",
|
|
||||||
"description": "Write your first code and learn Frontend from scratch to build a simple lightning wallet using HTML/CSS, Javascript, and React",
|
|
||||||
"thumbnail": 'https://emeralize.s3.amazonaws.com/course/cover_images/plebdev2_750__422_px_1200__630_px.jpg',
|
|
||||||
"price": 45000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lightning Wallet Backend",
|
|
||||||
"description": "Learn Backend from scratch and build a simple Lightning Wallet backend with a server, API, Database, and Lightning node using NodeJS",
|
|
||||||
"thumbnail": 'https://emeralize.s3.amazonaws.com/course/cover_images/plebdevs-thumbnail.png',
|
|
||||||
"price": 70000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lightning Wallet Frontend",
|
|
||||||
"description": "Write your first code and learn Frontend from scratch to build a simple lightning wallet using HTML/CSS, Javascript, and React",
|
|
||||||
"thumbnail": 'https://emeralize.s3.amazonaws.com/course/cover_images/plebdev2_750__422_px_1200__630_px.jpg',
|
|
||||||
"price": 45000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lightning Wallet Backend",
|
|
||||||
"description": "Learn Backend from scratch and build a simple Lightning Wallet backend with a server, API, Database, and Lightning node using NodeJS",
|
|
||||||
"thumbnail": 'https://emeralize.s3.amazonaws.com/course/cover_images/plebdevs-thumbnail.png',
|
|
||||||
"price": 70000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lightning Wallet Frontend",
|
|
||||||
"description": "Write your first code and learn Frontend from scratch to build a simple lightning wallet using HTML/CSS, Javascript, and React",
|
|
||||||
"thumbnail": 'https://emeralize.s3.amazonaws.com/course/cover_images/plebdev2_750__422_px_1200__630_px.jpg',
|
|
||||||
"price": 45000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Lightning Wallet Backend",
|
|
||||||
"description": "Learn Backend from scratch and build a simple Lightning Wallet backend with a server, API, Database, and Lightning node using NodeJS",
|
|
||||||
"thumbnail": 'https://emeralize.s3.amazonaws.com/course/cover_images/plebdevs-thumbnail.png',
|
|
||||||
"price": 70000
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const productTemplate = (course) => {
|
console.log('Resources:', resources);
|
||||||
|
|
||||||
|
const resourceTemplate = (resource) => {
|
||||||
|
const { content, title, summary, image, published_at } = parseResourceEvent(resource);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full px-4">
|
<div className="flex flex-col items-center w-full px-4">
|
||||||
<div className="w-86 h-60 bg-gray-200 overflow-hidden rounded-md shadow-lg">
|
<div className="w-86 h-60 bg-gray-200 overflow-hidden rounded-md shadow-lg">
|
||||||
<img src={course.thumbnail} alt={course.title} className="w-full h-full object-cover object-center" />
|
<img src={image} alt={title} className="w-full h-full object-cover object-center" />
|
||||||
</div>
|
</div>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<h4 className="mb-1 text-center">{course.title}</h4>
|
<h4 className="mb-1 font-bold text-center">{title}</h4>
|
||||||
<h6 className="mt-0 mb-3 text-center">{course.price} sats</h6>
|
<p className="text-center">{summary}</p>
|
||||||
<div className="flex flex-row items-center justify-center gap-2">
|
<div className="flex flex-row items-center justify-center gap-2">
|
||||||
<Button icon="pi pi-search" rounded />
|
<Button icon="pi pi-search" rounded />
|
||||||
<Button icon="pi pi-star-fill" rounded severity="success" />
|
<Button icon="pi pi-star-fill" rounded severity="success" />
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-center mt-2">Published on {published_at}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -65,7 +34,7 @@ export default function ResourcesCarousel() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-2xl font-bold ml-[6%] my-4">Resources</h1>
|
<h1 className="text-2xl font-bold ml-[6%] my-4">Resources</h1>
|
||||||
<Carousel value={courses} numVisible={3} itemTemplate={productTemplate} />
|
<Carousel value={resources} numVisible={3} itemTemplate={resourceTemplate} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
12
src/hooks/useImageProxy.js
Normal file
12
src/hooks/useImageProxy.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export const useImageProxy = () => {
|
||||||
|
|
||||||
|
const returnImageProxy = (image) => {
|
||||||
|
const proxyUrl = `${process.env.NEXT_PUBLIC_PROXY_URL}?imageUrl=${encodeURIComponent(image)}`;
|
||||||
|
|
||||||
|
return proxyUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { returnImageProxy };
|
||||||
|
}
|
@ -1,16 +1,18 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useNostr } from './useNostr';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { setPubkey, setUsername } from "@/redux/reducers/userReducer";
|
import { setUser } from "@/redux/reducers/userReducer";
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
import { findKind0Username } from "@/utils/nostr";
|
import { findKind0Fields } from "@/utils/nostr";
|
||||||
import { useToast } from './useToast';
|
import { useToast } from './useToast';
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { fetchKind0 } = useNostr();
|
||||||
|
|
||||||
// Attempt Auto Login on render
|
// Attempt Auto Login on render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,11 +24,26 @@ export const useLogin = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/users/${publicKey}`);
|
const response = await axios.get(`/api/users/${publicKey}`);
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
dispatch(setPubkey(publicKey));
|
dispatch(setUser(response.data));
|
||||||
if (response.data.username) {
|
} else if (response.status === 204) {
|
||||||
dispatch(setUsername(response.data.username));
|
// User not found, create a new user
|
||||||
|
const kind0 = await fetchKind0([{ authors: [publicKey], kinds: [0] }], {});
|
||||||
|
const fields = await findKind0Fields(kind0);
|
||||||
|
const payload = { pubkey: publicKey, ...fields };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createUserResponse = await axios.post(`/api/users`, payload);
|
||||||
|
if (createUserResponse.status === 201) {
|
||||||
|
;
|
||||||
|
window.localStorage.setItem('pubkey', publicKey);
|
||||||
|
dispatch(setUser(createUserResponse.data));
|
||||||
|
} else {
|
||||||
|
console.error('Error creating user:', createUserResponse);
|
||||||
|
}
|
||||||
|
} catch (createError) {
|
||||||
|
console.error('Error creating user:', createError);
|
||||||
|
showToast('error', 'Error Creating User', 'Failed to create user');
|
||||||
}
|
}
|
||||||
router.push('/');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during auto login:', error);
|
console.error('Error during auto login:', error);
|
||||||
@ -52,22 +69,21 @@ export const useLogin = () => {
|
|||||||
const response = await axios.get(`/api/users/${publicKey}`);
|
const response = await axios.get(`/api/users/${publicKey}`);
|
||||||
if (response.status !== 200) throw new Error('User not found');
|
if (response.status !== 200) throw new Error('User not found');
|
||||||
|
|
||||||
dispatch(setPubkey(publicKey));
|
|
||||||
window.localStorage.setItem('pubkey', publicKey);
|
window.localStorage.setItem('pubkey', publicKey);
|
||||||
if (response.data.username) dispatch(setUsername(response.data.username));
|
dispatch(setUser(response.data));
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// User not found, create a new user
|
// User not found, create a new user
|
||||||
const kind0 = await findKind0Username({ authors: [publicKey], kinds: [0] }); // Adjust based on actual implementation
|
const kind0 = await fetchKind0([{ authors: [publicKey], kinds: [0] }], {});
|
||||||
const username = kind0 ? kind0 : undefined;
|
const fields = await findKind0Fields(kind0);
|
||||||
const payload = { pubkey: publicKey, ...(username && { username }) };
|
const payload = { pubkey: publicKey, ...fields };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createUserResponse = await axios.post(`/api/users`, payload);
|
const createUserResponse = await axios.post(`/api/users`, payload);
|
||||||
if (createUserResponse.status === 201) {
|
if (createUserResponse.status === 201) {
|
||||||
dispatch(setPubkey(publicKey));
|
;
|
||||||
window.localStorage.setItem('pubkey', publicKey);
|
window.localStorage.setItem('pubkey', publicKey);
|
||||||
if (username) dispatch(setUsername(username));
|
dispatch(setUser(createUserResponse.data));
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} else {
|
} else {
|
||||||
console.error('Error creating user:', createUserResponse);
|
console.error('Error creating user:', createUserResponse);
|
||||||
@ -77,14 +93,14 @@ export const useLogin = () => {
|
|||||||
showToast('error', 'Error Creating User', 'Failed to create user');
|
showToast('error', 'Error Creating User', 'Failed to create user');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [dispatch, router, showToast]);
|
}, [dispatch, router, showToast, fetchKind0]);
|
||||||
|
|
||||||
const anonymousLogin = useCallback(() => {
|
const anonymousLogin = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
const secretKey = generateSecretKey();
|
const secretKey = generateSecretKey();
|
||||||
const publicKey = getPublicKey(secretKey);
|
const publicKey = getPublicKey(secretKey);
|
||||||
|
|
||||||
dispatch(setPubkey(publicKey));
|
dispatch(setUser({ pubkey: publicKey }));
|
||||||
window.localStorage.setItem('pubkey', publicKey);
|
window.localStorage.setItem('pubkey', publicKey);
|
||||||
window.localStorage.setItem('seckey', secretKey);
|
window.localStorage.setItem('seckey', secretKey);
|
||||||
router.push('/');
|
router.push('/');
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { SimplePool, relayInit, nip19 } from "nostr-tools";
|
import { SimplePool, relayInit, nip19 } from "nostr-tools";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
import { addResource } from "@/redux/reducers/eventsReducer";
|
||||||
import { initialRelays } from "@/redux/reducers/userReducer";
|
import { initialRelays } from "@/redux/reducers/userReducer";
|
||||||
|
|
||||||
export const useNostr = () => {
|
export const useNostr = () => {
|
||||||
@ -61,6 +62,36 @@ export const useNostr = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchResources = async () => {
|
||||||
|
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
|
||||||
|
|
||||||
|
const params = {seenOnEnabled: true};
|
||||||
|
const seenEventIds = new Set(); // Set to keep track of event IDs that have been processed
|
||||||
|
|
||||||
|
const hasPlebdevsTag = (eventData) => {
|
||||||
|
return eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
||||||
|
};
|
||||||
|
|
||||||
|
const sub = pool.current.subscribeMany(relays, filter, {
|
||||||
|
...params,
|
||||||
|
onevent: (event) => {
|
||||||
|
// Assuming event has a unique identifier under `id` field
|
||||||
|
if (!seenEventIds.has(event.id) && hasPlebdevsTag(event.tags)) {
|
||||||
|
seenEventIds.add(event.id); // Add event ID to the set to track it's been processed
|
||||||
|
dispatch(addResource(event));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: (error) => {
|
||||||
|
console.error("Error fetching resources:", error);
|
||||||
|
},
|
||||||
|
oneose: () => {
|
||||||
|
console.log("Subscription closed");
|
||||||
|
sub.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const fetchSingleEvent = async (id) => {
|
const fetchSingleEvent = async (id) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const sub = pool.current.subscribeMany(relays, [{ ids: [id] }]);
|
const sub = pool.current.subscribeMany(relays, [{ ids: [id] }]);
|
||||||
@ -101,6 +132,7 @@ export const useNostr = () => {
|
|||||||
fetchSingleEvent,
|
fetchSingleEvent,
|
||||||
publishEvent,
|
publishEvent,
|
||||||
fetchKind0,
|
fetchKind0,
|
||||||
|
fetchResources,
|
||||||
getRelayStatuses,
|
getRelayStatuses,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -3,7 +3,7 @@ import { Provider } from "react-redux";
|
|||||||
import { store } from "@/redux/store";
|
import { store } from "@/redux/store";
|
||||||
import Navbar from '@/components/navbar/Navbar';
|
import Navbar from '@/components/navbar/Navbar';
|
||||||
import { ToastProvider } from '@/hooks/useToast';
|
import { ToastProvider } from '@/hooks/useToast';
|
||||||
import Layout from '@/components/layout';
|
import Layout from '@/components/Layout';
|
||||||
import '@/styles/globals.css'
|
import '@/styles/globals.css'
|
||||||
import 'primereact/resources/themes/lara-dark-indigo/theme.css';
|
import 'primereact/resources/themes/lara-dark-indigo/theme.css';
|
||||||
|
|
||||||
|
27
src/pages/api/image-proxy.js
Normal file
27
src/pages/api/image-proxy.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
const { imageUrl } = req.query;
|
||||||
|
|
||||||
|
// Validate the imageUrl query parameter
|
||||||
|
if (!imageUrl) {
|
||||||
|
return res.status(400).json({ error: 'Missing imageUrl query parameter' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: imageUrl,
|
||||||
|
responseType: 'stream',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward the content type
|
||||||
|
res.setHeader('Content-Type', response.headers['content-type']);
|
||||||
|
|
||||||
|
// Stream the image from the external source to the client
|
||||||
|
response.data.pipe(res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Image proxy error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch image' });
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,17 @@
|
|||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
import CoursesCarousel from '@/components/courses/CoursesCarousel'
|
import CoursesCarousel from '@/components/courses/CoursesCarousel'
|
||||||
import ResourcesCarousel from '@/components/resources/ResourcesCarousel'
|
import ResourcesCarousel from '@/components/resources/ResourcesCarousel'
|
||||||
|
import { useNostr } from '@/hooks/useNostr'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { fetchResources } = useNostr();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Fetching resources');
|
||||||
|
fetchResources();
|
||||||
|
}, [fetchResources]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
55
src/pages/profile.js
Normal file
55
src/pages/profile.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "primereact/button";
|
||||||
|
import { DataTable } from 'primereact/datatable';
|
||||||
|
import { Column } from 'primereact/column';
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
const user = useSelector((state) => state.user.user);
|
||||||
|
const { returnImageProxy } = useImageProxy();
|
||||||
|
|
||||||
|
const purchases = [
|
||||||
|
{ code: '123', name: 'Product 1', category: 'Category 1', quantity: 1 },
|
||||||
|
{ code: '124', name: 'Product 2', category: 'Category 2', quantity: 2 },
|
||||||
|
{ code: '125', name: 'Product 3', category: 'Category 3', quantity: 3 },
|
||||||
|
{ code: '126', name: 'Product 4', category: 'Category 4', quantity: 4 },
|
||||||
|
{ code: '127', name: 'Product 5', category: 'Category 5', quantity: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
{user.avatar && (
|
||||||
|
<Image
|
||||||
|
alt="logo"
|
||||||
|
src={returnImageProxy(user.avatar)}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className="rounded-full mx-auto my-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h1 className="text-center text-2xl my-2">{user.username}</h1>
|
||||||
|
<h2 className="text-center text-xl my-2">{user.pubkey}</h2>
|
||||||
|
<div className="flex flex-row w-1/2 mx-auto my-4 justify-between">
|
||||||
|
<Button label="Edit" className="p-button-raised text-[#f8f8ff]" />
|
||||||
|
<Button label="Delete" className="p-button-raised p-button-danger text-[#f8f8ff]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||||
|
<h2>Subscription</h2>
|
||||||
|
<p>You currently have no active subscription</p>
|
||||||
|
<Button label="Subscribe" className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DataTable value={purchases} tableStyle={{ minWidth: '50rem' }}>
|
||||||
|
<Column field="code" header="Code"></Column>
|
||||||
|
<Column field="name" header="Name"></Column>
|
||||||
|
<Column field="category" header="Category"></Column>
|
||||||
|
<Column field="quantity" header="Quantity"></Column>
|
||||||
|
</DataTable>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile
|
@ -1,13 +1,13 @@
|
|||||||
import { createSlice, current } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
// Helper function to add single or multiple items
|
||||||
const addItems = (state, action, key) => {
|
const addItems = (state, action, key) => {
|
||||||
const items = current(state)[key];
|
|
||||||
if (Array.isArray(action.payload)) {
|
if (Array.isArray(action.payload)) {
|
||||||
// If payload is an array, spread it into the existing array
|
// If payload is an array, spread it into the existing array
|
||||||
state[key] = [...items, ...action.payload];
|
state[key] = [...state[key], ...action.payload];
|
||||||
} else {
|
} else {
|
||||||
// If payload is a single item, push it into the array
|
// If payload is a single item, push it into the array
|
||||||
items.push(action.payload);
|
state[key].push(action.payload);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,16 +18,22 @@ export const eventsSlice = createSlice({
|
|||||||
courses: [],
|
courses: [],
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setItems: (state, action) => {
|
addResource: (state, action) => {
|
||||||
if (action.payload.type === 'resource') {
|
addItems(state, action, 'resources');
|
||||||
addItems(state, action.payload.data, 'resources');
|
},
|
||||||
} else if (action.payload.type === 'course') {
|
addCourse: (state, action) => {
|
||||||
addItems(state, action.payload.data, 'courses');
|
addItems(state, action, 'courses');
|
||||||
}
|
},
|
||||||
|
// If you need to set the entire array at once, keep these or adjust as necessary
|
||||||
|
setResources: (state, action) => {
|
||||||
|
state.resources = action.payload;
|
||||||
|
},
|
||||||
|
setCourses: (state, action) => {
|
||||||
|
state.courses = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Exports
|
// Exports
|
||||||
export const { setItems } = eventsSlice.actions;
|
export const { addResource, addCourse, setResources, setCourses } = eventsSlice.actions;
|
||||||
export default eventsSlice.reducer;
|
export default eventsSlice.reducer;
|
||||||
|
@ -13,23 +13,19 @@ export const initialRelays = [
|
|||||||
export const userSlice = createSlice({
|
export const userSlice = createSlice({
|
||||||
name: "user",
|
name: "user",
|
||||||
initialState: {
|
initialState: {
|
||||||
pubkey: '',
|
user: {},
|
||||||
username: '',
|
|
||||||
relays: initialRelays,
|
relays: initialRelays,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setRelays: (state, action) => {
|
setRelays: (state, action) => {
|
||||||
state.relays = [...state.relays, action.payload];
|
state.relays = [...state.relays, action.payload];
|
||||||
},
|
},
|
||||||
setPubkey: (state, action) => {
|
setUser: (state, action) => {
|
||||||
state.pubkey = action.payload;
|
state.user = action.payload;
|
||||||
},
|
|
||||||
setUsername: (state, action) => {
|
|
||||||
state.username = action.payload;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setRelays, setPubkey, setUsername } = userSlice.actions;
|
export const { setRelays, setUser } = userSlice.actions;
|
||||||
|
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
@ -1,4 +1,6 @@
|
|||||||
export const findKind0Username = async (kind0) => {
|
export const findKind0Fields = async (kind0) => {
|
||||||
|
let fields = {}
|
||||||
|
|
||||||
const usernameProperties = ['name', 'displayName', 'display_name', 'username', 'handle', 'alias'];
|
const usernameProperties = ['name', 'displayName', 'display_name', 'username', 'handle', 'alias'];
|
||||||
|
|
||||||
const findTruthyPropertyValue = (object, properties) => {
|
const findTruthyPropertyValue = (object, properties) => {
|
||||||
@ -12,5 +14,47 @@ export const findKind0Username = async (kind0) => {
|
|||||||
|
|
||||||
const username = findTruthyPropertyValue(kind0, usernameProperties);
|
const username = findTruthyPropertyValue(kind0, usernameProperties);
|
||||||
|
|
||||||
return username;
|
if (username) {
|
||||||
}
|
fields.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatar = findTruthyPropertyValue(kind0, ['picture', 'avatar', 'profilePicture', 'profile_picture']);
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
|
fields.avatar = avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseResourceEvent = (event) => {
|
||||||
|
// Initialize an object to store the extracted data
|
||||||
|
const eventData = {
|
||||||
|
content: event.content || '',
|
||||||
|
title: '',
|
||||||
|
summary: '',
|
||||||
|
image: '',
|
||||||
|
published_at: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iterate over the tags array to extract data
|
||||||
|
event.tags.forEach(tag => {
|
||||||
|
switch (tag[0]) { // Check the key in each key-value pair
|
||||||
|
case 'title':
|
||||||
|
eventData.title = tag[1];
|
||||||
|
break;
|
||||||
|
case 'summary':
|
||||||
|
eventData.summary = tag[1];
|
||||||
|
break;
|
||||||
|
case 'image':
|
||||||
|
eventData.image = tag[1];
|
||||||
|
break;
|
||||||
|
case 'published_at':
|
||||||
|
eventData.published_at = tag[1];
|
||||||
|
break;
|
||||||
|
// Add cases for any other data you need to extract
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return eventData;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user