Rip out redux for local storage, reowkr hooks, more responsiveness fixes

This commit is contained in:
austinkelsay 2024-03-19 17:47:16 -05:00
parent 919f13c88c
commit 7abf1c8882
17 changed files with 219 additions and 298 deletions

View File

@ -2,7 +2,7 @@
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
images: { images: {
domains: ['localhost'], domains: ['localhost', 'secure.gravatar.com'],
}, },
} }

View File

@ -6,6 +6,15 @@ const HeroBanner = () => {
const [currentOption, setCurrentOption] = useState(0); const [currentOption, setCurrentOption] = useState(0);
const [isFlipping, setIsFlipping] = useState(false); const [isFlipping, setIsFlipping] = useState(false);
const getColorClass = (option) => {
switch (option) {
case 'Bitcoin': return 'text-orange-400';
case 'Lightning': return 'text-blue-500';
case 'Nostr': return 'text-purple-400';
default: return 'text-white';
}
};
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setIsFlipping(true); setIsFlipping(true);
@ -28,18 +37,20 @@ const HeroBanner = () => {
width={1920} width={1920}
height={1080} height={1080}
quality={100} quality={100}
className='opacity-70'
/> />
<div className="absolute text-center text-white text-xl"> <div className="absolute text-center text-white text-xl h-full flex flex-col justify-evenly">
<p className='text-4xl max-tab:text-xl max-mob:text-xl'>Learn how to code</p> <p className='text-2xl md:text-3xl lg:text-4xl xl:text-5xl'>Learn how to code</p>
<p className='text-4xl pt-4 max-tab:text-xl max-mob:text-xl max-tab:pt-2 max-mob:pt-2'> <p className='text-2xl md:text-3xl lg:text-4xl xl:text-5xl'>
Build{' '} Build{' '}
<span className={`text-4xl max-tab:text-xl max-mob:text-xl inline-block w-40 text-center max-tab:w-24 max-mob:w-24 ${isFlipping ? 'flip-enter-active' : ''}`}> <span className={`inline-block w-[35%] ${isFlipping ? 'flip-enter-active' : ''} ${getColorClass(options[currentOption])}`}>
{options[currentOption]} {options[currentOption]}
</span> </span>
{' '}apps {' '}apps
</p> </p>
<p className='text-4xl pt-4 max-tab:text-xl max-mob:text-xl max-tab:pt-2 max-mob:pt-2'>Become a Bitcoin developer</p> <p className='text-2xl md:text-3xl lg:text-4xl xl:text-5xl'>Become a Bitcoin developer</p>
</div> </div>
</div> </div>
); );
}; };

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, use } from 'react';
import { Carousel } from 'primereact/carousel'; import { Carousel } from 'primereact/carousel';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Image from 'next/image'; import Image from 'next/image';
import { useSelector } from 'react-redux';
import { useImageProxy } from '@/hooks/useImageProxy'; import { useImageProxy } from '@/hooks/useImageProxy';
import { parseEvent } from '@/utils/nostr'; import { parseEvent } from '@/utils/nostr';
import { formatTimestampToHowLongAgo } from '@/utils/time'; import { formatTimestampToHowLongAgo } from '@/utils/time';
import { useNostr } from '@/hooks/useNostr';
const responsiveOptions = [ const responsiveOptions = [
{ {
@ -27,13 +27,20 @@ const responsiveOptions = [
export default function CoursesCarousel() { export default function CoursesCarousel() {
const courses = useSelector((state) => state.events.courses);
const [processedCourses, setProcessedCourses] = useState([]); const [processedCourses, setProcessedCourses] = useState([]);
const [screenWidth, setScreenWidth] = useState(null);
const [courses, setCourses] = useState([]);
const router = useRouter();
const { fetchCourses, events } = useNostr();
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const router = useRouter(); useEffect(() => {
if (events && events.courses && events.courses.length > 0) {
const [screenWidth, setScreenWidth] = useState(null); setCourses(events.courses);
} else {
fetchCourses();
}
}, [events]);
useEffect(() => { useEffect(() => {
// Update the state to the current window width // Update the state to the current window width
@ -60,7 +67,7 @@ export default function CoursesCarousel() {
return { width: 344, height: 194 }; return { width: 344, height: 194 };
} else { } else {
// Small screens // Small screens
return { width: screenWidth - 50, height: (screenWidth - 50) * (9 / 16) }; return { width: screenWidth - 120, height: (screenWidth - 120) * (9 / 16) };
} }
}; };
@ -109,7 +116,7 @@ export default function CoursesCarousel() {
return ( return (
<> <>
<h2 className="ml-[6%] mt-4">courses</h2> <h2 className="ml-[6%] mt-4">courses</h2>
<Carousel value={processedCourses} numVisible={2} itemTemplate={courseTemplate} responsiveOptions={responsiveOptions} /> <Carousel value={[...processedCourses, ...processedCourses]} numVisible={2} itemTemplate={courseTemplate} responsiveOptions={responsiveOptions} />
</> </>
); );
} }

View File

@ -1,36 +1,43 @@
import React, { useRef } from 'react'; "use client";
import React, { useRef, useState, useEffect } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useImageProxy } from '@/hooks/useImageProxy'; import { useImageProxy } from '@/hooks/useImageProxy';
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu'; import { Menu } from 'primereact/menu';
import { useSelector, useDispatch } from 'react-redux';
import { setUser } from '@/redux/reducers/userReducer';
import useWindowWidth from '@/hooks/useWindowWidth'; import useWindowWidth from '@/hooks/useWindowWidth';
import useLocalStorage from '@/hooks/useLocalStorage';
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 UserAvatar = () => { const UserAvatar = () => {
const router = useRouter(); const router = useRouter();
const dispatch = useDispatch(); const [user, setUser] = useLocalStorage('user', {});
const user = useSelector((state) => state.user.user); const [isClient, setIsClient] = useState(false);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth(); const windowWidth = useWindowWidth();
const menu = useRef(null); const menu = useRef(null);
const handleLogout = () => { const handleLogout = () => {
window.localStorage.removeItem('pubkey'); window.localStorage.removeItem('user');
dispatch(setUser(null));
router.push('/'); router.push('/');
} }
let userAvatar; let userAvatar;
if (user && Object.keys(user).length > 0) { useEffect(() => {
setIsClient(true); // Component did mount, we're client-side
}, []);
// If not client, render nothing or a placeholder
if (!isClient) {
return null; // Or return a loader/spinner/placeholder
} else if (user && Object.keys(user).length > 0) {
console.log('ahhhhh s:', user);
// User exists, show username or pubkey // User exists, show username or pubkey
const displayName = user.username || user.pubkey; const displayName = user.username || user.pubkey.slice(0, 10) + '...';
const items = [ const items = [
{ {
@ -52,15 +59,13 @@ const UserAvatar = () => {
userAvatar = ( userAvatar = (
<> <>
<div onClick={(event) => menu.current.toggle(event)} className='flex flex-row items-center justify-between cursor-pointer hover:opacity-75'> <div onClick={(event) => menu.current.toggle(event)} className='flex flex-row items-center justify-between cursor-pointer hover:opacity-75'>
{user.avatar && (
<Image <Image
alt="logo" alt="logo"
src={returnImageProxy(user.avatar)} src={user?.avatar ? returnImageProxy(user.avatar) : `https://secure.gravatar.com/avatar/${user.pubkey}?s=90&d=identicon`}
width={50} width={50}
height={50} height={50}
className={styles.logo} className={styles.logo}
/> />
)}
</div> </div>
<Menu model={items} popup ref={menu} /> <Menu model={items} popup ref={menu} />
</> </>

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Carousel } from 'primereact/carousel'; import { Carousel } from 'primereact/carousel';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Image from 'next/image'; import Image from 'next/image';
import { useSelector } from 'react-redux'; import { useNostr } from '@/hooks/useNostr';
import { useImageProxy } from '@/hooks/useImageProxy'; import { useImageProxy } from '@/hooks/useImageProxy';
import { parseEvent } from '@/utils/nostr'; import { parseEvent } from '@/utils/nostr';
import { formatTimestampToHowLongAgo } from '@/utils/time'; import { formatTimestampToHowLongAgo } from '@/utils/time';
@ -26,12 +26,20 @@ const responsiveOptions = [
]; ];
export default function WorkshopsCarousel() { export default function WorkshopsCarousel() {
const workshops = useSelector((state) => state.events.resources);
const [processedWorkshops, setProcessedWorkshops] = useState([]); const [processedWorkshops, setProcessedWorkshops] = useState([]);
const [screenWidth, setScreenWidth] = useState(null); const [screenWidth, setScreenWidth] = useState(null);
const [workshops, setWorkshops] = useState([]);
const router = useRouter();
const { fetchWorkshops, events } = useNostr();
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const router = useRouter(); useEffect(() => {
if (events && events.workshops && events.workshops.length > 0) {
setWorkshops(events.workshops);
} else {
fetchWorkshops();
}
}, [events]);
useEffect(() => { useEffect(() => {
// Update the state to the current window width // Update the state to the current window width
@ -58,7 +66,7 @@ export default function WorkshopsCarousel() {
return { width: 344, height: 194 }; return { width: 344, height: 194 };
} else { } else {
// Small screens // Small screens
return { width: screenWidth - 50, height: (screenWidth - 50) * (9 / 16) }; return { width: screenWidth - 120, height: (screenWidth - 120) * (9 / 16) };
} }
}; };
@ -106,7 +114,7 @@ export default function WorkshopsCarousel() {
return ( return (
<> <>
<h2 className="ml-[6%] mt-4">workshops</h2> <h2 className="ml-[6%] mt-4">workshops</h2>
<Carousel value={processedWorkshops} numVisible={2} itemTemplate={workshopTemplate} responsiveOptions={responsiveOptions} /> <Carousel value={[...processedWorkshops, ...processedWorkshops]} numVisible={2} itemTemplate={workshopTemplate} responsiveOptions={responsiveOptions} />
</> </>
); );
} }

View File

@ -0,0 +1,35 @@
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
// Added a check to ensure the item is not only present but also a valid JSON string.
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
// Consider removing or correcting the invalid item in localStorage here.
window.localStorage.removeItem(key); // Optional: remove the item that caused the error.
return initialValue; // Revert to initial value if parsing fails.
}
});
const setValue = value => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;

View File

@ -1,15 +1,12 @@
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 { useNostr } from './useNostr'; import { useNostr } from './useNostr';
import axios from 'axios'; import axios from 'axios';
import { setUser } from "@/redux/reducers/userReducer";
import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { findKind0Fields } 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 router = useRouter(); const router = useRouter();
const { showToast } = useToast(); const { showToast } = useToast();
const { fetchKind0 } = useNostr(); const { fetchKind0 } = useNostr();
@ -17,7 +14,8 @@ export const useLogin = () => {
// Attempt Auto Login on render // Attempt Auto Login on render
useEffect(() => { useEffect(() => {
const autoLogin = async () => { const autoLogin = async () => {
const publicKey = window.localStorage.getItem('pubkey'); const user = window.localStorage.getItem('user');
const publicKey = JSON.parse(user)?.pubkey;
if (!publicKey) return; if (!publicKey) return;
@ -25,20 +23,26 @@ export const useLogin = () => {
const response = await axios.get(`/api/users/${publicKey}`); const response = await axios.get(`/api/users/${publicKey}`);
console.log('auto login response:', response); console.log('auto login response:', response);
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
dispatch(setUser(response.data)); window.localStorage.setItem('user', JSON.stringify(response.data));
} else if (response.status === 204) { } else if (response.status === 204) {
// User not found, create a new user // User not found, create a new user
const kind0 = await fetchKind0([{ authors: [publicKey], kinds: [0] }], {}); const kind0 = await fetchKind0([{ authors: [publicKey], kinds: [0] }], {});
console.log('kind0:', kind0); console.log('kind0:', kind0);
const fields = await findKind0Fields(kind0);
let fields = null;
if (kind0) {
fields = await findKind0Fields(kind0);
}
const payload = { pubkey: publicKey, ...fields }; const payload = { pubkey: publicKey, ...fields };
try { try {
const createUserResponse = await axios.post(`/api/users`, payload); const createUserResponse = await axios.post(`/api/users`, payload);
console.log('create user response:', createUserResponse); console.log('create user response:', createUserResponse);
if (createUserResponse.status === 201) { if (createUserResponse.status === 201) {
window.localStorage.setItem('pubkey', publicKey); window.localStorage.setItem('user', JSON.stringify(createUserResponse.data));
dispatch(setUser(createUserResponse.data));
} else { } else {
console.error('Error creating user:', createUserResponse); console.error('Error creating user:', createUserResponse);
} }
@ -70,9 +74,8 @@ 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) throw new Error('User not found'); if (response.status !== 200) throw new Error('User not found');
;
window.localStorage.setItem('pubkey', publicKey); window.localStorage.setItem('user', JSON.stringify(response.data));
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
@ -83,9 +86,7 @@ export const useLogin = () => {
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) {
; window.localStorage.setItem('user', JSON.stringify(createUserResponse.data));
window.localStorage.setItem('pubkey', publicKey);
dispatch(setUser(createUserResponse.data));
router.push('/'); router.push('/');
} else { } else {
console.error('Error creating user:', createUserResponse); console.error('Error creating user:', createUserResponse);
@ -95,22 +96,22 @@ export const useLogin = () => {
showToast('error', 'Error Creating User', 'Failed to create user'); showToast('error', 'Error Creating User', 'Failed to create user');
} }
} }
}, [dispatch, router, showToast, fetchKind0]); }, [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);
// need to fix with byteToHex
const hexSecretKey = secretKey.toString('hex');
dispatch(setUser({ pubkey: publicKey })); window.localStorage.setItem('user', JSON.stringify({ pubkey: publicKey, secretKey: hexSecretKey }));
window.localStorage.setItem('pubkey', publicKey);
window.localStorage.setItem('seckey', secretKey);
router.push('/'); router.push('/');
} catch (error) { } catch (error) {
console.error('Error during anonymous login:', error); console.error('Error during anonymous login:', error);
showToast('error', 'Error Logging In', 'Failed to log in'); showToast('error', 'Error Logging In', 'Failed to log in');
} }
}, [dispatch, router, showToast]); }, [router, showToast]);
return { nostrLogin, anonymousLogin }; return { nostrLogin, anonymousLogin };
}; };

View File

@ -1,14 +1,25 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { SimplePool, relayInit, nip19 } from "nostr-tools"; import { SimplePool } from "nostr-tools";
import { useDispatch } from "react-redux";
import { addResource, addCourse, addWorkshop, addStream } from "@/redux/reducers/eventsReducer"; const initialRelays = [
import { initialRelays } from "@/redux/reducers/userReducer"; "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/"
];
export const useNostr = () => { export const useNostr = () => {
const [relays, setRelays] = useState(initialRelays); const [relays, setRelays] = useState(initialRelays);
const [relayStatuses, setRelayStatuses] = useState({}); const [relayStatuses, setRelayStatuses] = useState({});
const [events, setEvents] = useState({
const dispatch = useDispatch(); resources: [],
workshops: [],
courses: [],
streams: []
});
const pool = useRef(new SimplePool({ seenOnEnabled: true })); const pool = useRef(new SimplePool({ seenOnEnabled: true }));
const subscriptions = useRef([]); const subscriptions = useRef([]);
@ -34,6 +45,56 @@ export const useNostr = () => {
await Promise.all(newRelays.map(relay => pool.current.ensureRelay(relay))); await Promise.all(newRelays.map(relay => pool.current.ensureRelay(relay)));
}; };
const fetchEvents = async (filter, updateDataField, hasRequiredTags) => {
try {
const sub = pool.current.subscribeMany(relays, filter, {
onevent: (event) => {
if (hasRequiredTags(event.tags)) {
setEvents(prevData => ({
...prevData,
[updateDataField]: [...prevData[updateDataField], event]
}));
}
},
onerror: (error) => {
setError(error);
console.error(`Error fetching ${updateDataField}:`, error);
},
oneose: () => {
console.log("Subscription closed");
sub.close();
}
});
} catch (error) {
setError(error);
}
};
// Fetch resources, workshops, courses, and streams with appropriate filters and update functions
const fetchResources = () => {
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs") && eventData.some(([tag, value]) => tag === "t" && value === "resource");
fetchEvents(filter, 'resources', hasRequiredTags);
};
const fetchWorkshops = () => {
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs") && eventData.some(([tag, value]) => tag === "t" && value === "resource");
fetchEvents(filter, 'workshops', hasRequiredTags);
}
const fetchCourses = () => {
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs") && eventData.some(([tag, value]) => tag === "t" && value === "course");
fetchEvents(filter, 'courses', hasRequiredTags);
}
const fetchStreams = () => {
const filter = [{kinds: [30311], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
fetchEvents(filter, 'streams', hasRequiredTags);
}
const fetchKind0 = async (criteria, params) => { const fetchKind0 = async (criteria, params) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const events = []; const events = [];
@ -62,110 +123,6 @@ export const useNostr = () => {
}); });
}; };
const fetchResources = async () => {
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const params = {seenOnEnabled: true};
const hasRequiredTags = (eventData) => {
return eventData.some(([tag, value]) => tag === "t" && value === "plebdevs") && eventData.some(([tag, value]) => tag === "t" && value === "resource");
};
const sub = pool.current.subscribeMany(relays, filter, {
...params,
onevent: (event) => {
if (hasRequiredTags(event.tags)) {
dispatch(addResource(event));
}
},
onerror: (error) => {
console.error("Error fetching resources:", error);
},
oneose: () => {
console.log("Subscription closed");
sub.close();
}
});
}
const fetchWorkshops = async () => {
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const params = {seenOnEnabled: true};
const hasRequiredTags = (eventData) => {
return eventData.some(([tag, value]) => tag === "t" && value === "plebdevs") && eventData.some(([tag, value]) => tag === "t" && value === "workshop");
};
const sub = pool.current.subscribeMany(relays, filter, {
...params,
onevent: (event) => {
if (hasRequiredTags(event.tags)) {
dispatch(addWorkshop(event));
}
},
onerror: (error) => {
console.error("Error fetching workshops:", error);
},
oneose: () => {
console.log("Subscription closed");
sub.close();
}
});
}
const fetchCourses = async () => {
const filter = [{kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const params = {seenOnEnabled: true};
const hasRequiredTags = (eventData) => {
return eventData.some(([tag, value]) => tag === "t" && value === "plebdevs") && eventData.some(([tag, value]) => tag === "t" && value === "course");
};
const sub = pool.current.subscribeMany(relays, filter, {
...params,
onevent: (event) => {
if (hasRequiredTags(event.tags)) {
dispatch(addCourse(event));
}
},
onerror: (error) => {
console.error("Error fetching courses:", error);
},
oneose: () => {
console.log("Subscription closed");
sub.close();
}
});
}
const fetchStreams = async () => {
const filter = [{kinds: [30311], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
const params = {seenOnEnabled: true};
const hasRequiredTags = (eventData) => {
return eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
};
const sub = pool.current.subscribeMany(relays, filter, {
...params,
onevent: (event) => {
if (hasRequiredTags(event.tags)) {
dispatch(addStream(event));
}
},
onerror: (error) => {
console.error("Error fetching streams:", 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] }], {
@ -181,7 +138,7 @@ export const useNostr = () => {
} }
}); });
}); });
}; }
const publishEvent = async (event) => { const publishEvent = async (event) => {
try { try {
@ -212,6 +169,8 @@ export const useNostr = () => {
fetchResources, fetchResources,
fetchCourses, fetchCourses,
fetchWorkshops, fetchWorkshops,
fetchStreams,
getRelayStatuses, getRelayStatuses,
events
}; };
}; };

View File

@ -1,6 +1,4 @@
import { PrimeReactProvider } from 'primereact/api'; import { PrimeReactProvider } from 'primereact/api';
import { Provider } from "react-redux";
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';
@ -12,7 +10,6 @@ export default function MyApp({
Component, pageProps: { ...pageProps } Component, pageProps: { ...pageProps }
}) { }) {
return ( return (
<Provider store={store}>
<PrimeReactProvider> <PrimeReactProvider>
<ToastProvider> <ToastProvider>
<Layout> <Layout>
@ -29,6 +26,5 @@ export default function MyApp({
</Layout> </Layout>
</ToastProvider> </ToastProvider>
</PrimeReactProvider> </PrimeReactProvider>
</Provider>
); );
} }

View File

@ -6,7 +6,7 @@ class MyDocument extends Document {
<Html> <Html>
<Head> <Head>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
<link href="https://fonts.googleapis.com/css2?family=Blinker:wght@100;200;300;400;600;700;800;900&family=Poppins&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Blinker:wght@100;200;300;400;600;700;800;900&family=Poppins&display=swap" rel="stylesheet" />
</Head> </Head>
<body> <body>

View File

@ -110,7 +110,7 @@ export default function Details() {
/> />
<Button <Button
icon="pi pi-bolt" icon="pi pi-bolt"
label="100 sats" label="Zap"
severity="success" severity="success"
outlined outlined
pt={{ pt={{

View File

@ -1,18 +1,10 @@
import Head from 'next/head' import Head from 'next/head'
import React, {useCallback, useEffect, useState} from 'react'; import React from 'react';
import CoursesCarousel from '@/components/courses/CoursesCarousel' import CoursesCarousel from '@/components/courses/CoursesCarousel'
import WorkshopsCarousel from '@/components/workshops/WorkshopsCarousel' import WorkshopsCarousel from '@/components/workshops/WorkshopsCarousel'
import HeroBanner from '@/components/banner/HeroBanner'; import HeroBanner from '@/components/banner/HeroBanner';
import { useNostr } from '@/hooks/useNostr'
export default function Home() { export default function Home() {
const { fetchResources, fetchCourses } = useNostr();
useEffect(() => {
fetchResources();
fetchCourses();
}, [fetchResources, fetchCourses]);
return ( return (
<> <>
<Head> <Head>

View File

@ -5,10 +5,11 @@ import { Menu } from 'primereact/menu';
import { Column } from 'primereact/column'; import { Column } from 'primereact/column';
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import useLocalStorage from "@/hooks/useLocalStorage";
import Image from "next/image"; import Image from "next/image";
const Profile = () => { const Profile = () => {
const user = useSelector((state) => state.user.user); const [user, setUser] = useLocalStorage('user', {});
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const menu = useRef(null); const menu = useRef(null);
@ -61,22 +62,20 @@ const Profile = () => {
<div className="max-tab:w-[100vw] max-mob:w-[100vw]"> <div className="max-tab:w-[100vw] max-mob:w-[100vw]">
<div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]"> <div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
<div className="relative flex w-full items-center justify-center"> <div className="relative flex w-full items-center justify-center">
{user.avatar && (
<Image <Image
alt="user's avatar" alt="user's avatar"
src={returnImageProxy(user.avatar)} src={user?.avatar ? returnImageProxy(user.avatar) : `https://secure.gravatar.com/avatar/${user.pubkey}?s=90&d=identicon`}
width={100} width={100}
height={100} height={100}
className="rounded-full my-4" className="rounded-full my-4"
/> />
)}
<i className="pi pi-ellipsis-h absolute right-24 text-2xl my-4 cursor-pointer hover:opacity-75" <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> onClick={(e) => menu.current.toggle(e)}></i>
<Menu model={menuItems} popup ref={menu} /> <Menu model={menuItems} popup ref={menu} />
</div> </div>
<h1 className="text-center text-2xl my-2">{user.username}</h1> <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> <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"> <div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
<h2>Subscription</h2> <h2>Subscription</h2>

View File

@ -1,52 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
// Helper function to add single or multiple items without duplicates
const addItems = (state, action, key) => {
const existingIds = new Set(state[key].map(item => item.id));
if (Array.isArray(action.payload)) {
console.log('action.payload', action.payload);
// Filter out duplicates based on the id
const uniqueItems = action.payload.filter(item => !existingIds.has(item.id));
// If payload is an array, spread it into the existing array without duplicates
state[key] = [...state[key], ...action.payload];
} else {
// If payload is a single item, push it into the array if it's not a duplicate
// if (!existingIds.has(action.payload.id)) {
state[key].push(action.payload);
// }
}
};
export const eventsSlice = createSlice({
name: 'events',
initialState: {
resources: [],
courses: [],
workshops: [],
streams: [],
},
reducers: {
addResource: (state, action) => {
addItems(state, action, 'resources');
},
addCourse: (state, action) => {
addItems(state, action, 'courses');
},
addWorkshop: (state, action) => {
addItems(state, action, 'workshops');
},
addStream: (state, action) => {
addItems(state, action, 'streams');
},
setResources: (state, action) => {
state.resources = action.payload;
},
setCourses: (state, action) => {
state.courses = action.payload;
},
},
});
export const { addResource, addCourse, setResources, setCourses, addWorkshop, addStream } = eventsSlice.actions;
export default eventsSlice.reducer;

View File

@ -1,31 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
export const initialRelays = [
"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/"
];
export const userSlice = createSlice({
name: "user",
initialState: {
user: {},
relays: initialRelays,
},
reducers: {
setRelays: (state, action) => {
state.relays = [...state.relays, action.payload];
},
setUser: (state, action) => {
state.user = action.payload;
},
},
});
export const { setRelays, setUser } = userSlice.actions;
export default userSlice.reducer;

View File

@ -1,10 +0,0 @@
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./reducers/userReducer";
import eventsReducer from "./reducers/eventsReducer";
export const store = configureStore({
reducer: {
user: userReducer,
events: eventsReducer,
}
});

View File

@ -10,6 +10,7 @@ module.exports = {
screens: { screens: {
'max-mob': {'max': '475px'}, 'max-mob': {'max': '475px'},
'max-tab': {'max': '768px'}, 'max-tab': {'max': '768px'},
'max-lap': {'max': '1440px'},
}, },
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',