From 7abf1c88823c773ed2a77ddf3e337152f7df78af Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Tue, 19 Mar 2024 17:47:16 -0500 Subject: [PATCH] Rip out redux for local storage, reowkr hooks, more responsiveness fixes --- next.config.js | 2 +- src/components/banner/HeroBanner.js | 25 ++- src/components/courses/CoursesCarousel.js | 23 ++- src/components/navbar/user/UserAvatar.js | 41 ++-- src/components/workshops/WorkshopsCarousel.js | 18 +- src/hooks/useLocalStorage.js | 35 ++++ src/hooks/useLogin.js | 39 ++-- src/hooks/useNostr.js | 181 +++++++----------- src/pages/_app.js | 32 ++-- src/pages/_document.js | 6 +- src/pages/details/[slug].js | 2 +- src/pages/index.js | 10 +- src/pages/profile.js | 9 +- src/redux/reducers/eventsReducer.js | 52 ----- src/redux/reducers/userReducer.js | 31 --- src/redux/store.js | 10 - tailwind.config.js | 1 + 17 files changed, 219 insertions(+), 298 deletions(-) create mode 100644 src/hooks/useLocalStorage.js delete mode 100644 src/redux/reducers/eventsReducer.js delete mode 100644 src/redux/reducers/userReducer.js delete mode 100644 src/redux/store.js diff --git a/next.config.js b/next.config.js index 5435e7d..6bfc31a 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,7 @@ const nextConfig = { reactStrictMode: true, images: { - domains: ['localhost'], + domains: ['localhost', 'secure.gravatar.com'], }, } diff --git a/src/components/banner/HeroBanner.js b/src/components/banner/HeroBanner.js index 88321fb..3d8ab97 100644 --- a/src/components/banner/HeroBanner.js +++ b/src/components/banner/HeroBanner.js @@ -6,6 +6,15 @@ const HeroBanner = () => { const [currentOption, setCurrentOption] = useState(0); 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(() => { const interval = setInterval(() => { setIsFlipping(true); @@ -16,9 +25,9 @@ const HeroBanner = () => { }, 400); // Start preparing to flip back a bit before the halfway point }, 400); // Update slightly before the midpoint for smoother transition }, 2500); // Increased to provide a slight pause between animations for readability - + return () => clearInterval(interval); - }, []); + }, []); return (
@@ -28,18 +37,20 @@ const HeroBanner = () => { width={1920} height={1080} quality={100} + className='opacity-70' /> -
-

Learn how to code

-

+

+

Learn how to code

+

Build{' '} - + {options[currentOption]} {' '}apps

-

Become a Bitcoin developer

+

Become a Bitcoin developer

+
); }; diff --git a/src/components/courses/CoursesCarousel.js b/src/components/courses/CoursesCarousel.js index e03d736..85459f8 100644 --- a/src/components/courses/CoursesCarousel.js +++ b/src/components/courses/CoursesCarousel.js @@ -1,11 +1,11 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, use } from 'react'; import { Carousel } from 'primereact/carousel'; import { useRouter } from 'next/router'; import Image from 'next/image'; -import { useSelector } from 'react-redux'; import { useImageProxy } from '@/hooks/useImageProxy'; import { parseEvent } from '@/utils/nostr'; import { formatTimestampToHowLongAgo } from '@/utils/time'; +import { useNostr } from '@/hooks/useNostr'; const responsiveOptions = [ { @@ -27,13 +27,20 @@ const responsiveOptions = [ export default function CoursesCarousel() { - const courses = useSelector((state) => state.events.courses); const [processedCourses, setProcessedCourses] = useState([]); + const [screenWidth, setScreenWidth] = useState(null); + const [courses, setCourses] = useState([]); + const router = useRouter(); + const { fetchCourses, events } = useNostr(); const { returnImageProxy } = useImageProxy(); - const router = useRouter(); - - const [screenWidth, setScreenWidth] = useState(null); + useEffect(() => { + if (events && events.courses && events.courses.length > 0) { + setCourses(events.courses); + } else { + fetchCourses(); + } + }, [events]); useEffect(() => { // Update the state to the current window width @@ -60,7 +67,7 @@ export default function CoursesCarousel() { return { width: 344, height: 194 }; } else { // 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 ( <>

courses

- + ); } diff --git a/src/components/navbar/user/UserAvatar.js b/src/components/navbar/user/UserAvatar.js index 1ee7f5e..ecad176 100644 --- a/src/components/navbar/user/UserAvatar.js +++ b/src/components/navbar/user/UserAvatar.js @@ -1,36 +1,43 @@ -import React, { useRef } from 'react'; +"use client"; +import React, { useRef, useState, useEffect } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { useImageProxy } from '@/hooks/useImageProxy'; import { Button } from 'primereact/button'; import { Menu } from 'primereact/menu'; -import { useSelector, useDispatch } from 'react-redux'; -import { setUser } from '@/redux/reducers/userReducer'; import useWindowWidth from '@/hooks/useWindowWidth'; +import useLocalStorage from '@/hooks/useLocalStorage'; 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 [user, setUser] = useLocalStorage('user', {}); + const [isClient, setIsClient] = useState(false); const { returnImageProxy } = useImageProxy(); const windowWidth = useWindowWidth(); const menu = useRef(null); const handleLogout = () => { - window.localStorage.removeItem('pubkey'); - dispatch(setUser(null)); + window.localStorage.removeItem('user'); router.push('/'); } 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 - const displayName = user.username || user.pubkey; + const displayName = user.username || user.pubkey.slice(0, 10) + '...'; const items = [ { @@ -52,15 +59,13 @@ const UserAvatar = () => { userAvatar = ( <>
menu.current.toggle(event)} className='flex flex-row items-center justify-between cursor-pointer hover:opacity-75'> - {user.avatar && ( - logo - )} + logo
diff --git a/src/components/workshops/WorkshopsCarousel.js b/src/components/workshops/WorkshopsCarousel.js index f8465ba..2425cbe 100644 --- a/src/components/workshops/WorkshopsCarousel.js +++ b/src/components/workshops/WorkshopsCarousel.js @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Carousel } from 'primereact/carousel'; import { useRouter } from 'next/router'; import Image from 'next/image'; -import { useSelector } from 'react-redux'; +import { useNostr } from '@/hooks/useNostr'; import { useImageProxy } from '@/hooks/useImageProxy'; import { parseEvent } from '@/utils/nostr'; import { formatTimestampToHowLongAgo } from '@/utils/time'; @@ -26,12 +26,20 @@ const responsiveOptions = [ ]; export default function WorkshopsCarousel() { - const workshops = useSelector((state) => state.events.resources); const [processedWorkshops, setProcessedWorkshops] = useState([]); const [screenWidth, setScreenWidth] = useState(null); + const [workshops, setWorkshops] = useState([]); + const router = useRouter(); + const { fetchWorkshops, events } = useNostr(); const { returnImageProxy } = useImageProxy(); - const router = useRouter(); + useEffect(() => { + if (events && events.workshops && events.workshops.length > 0) { + setWorkshops(events.workshops); + } else { + fetchWorkshops(); + } + }, [events]); useEffect(() => { // Update the state to the current window width @@ -58,7 +66,7 @@ export default function WorkshopsCarousel() { return { width: 344, height: 194 }; } else { // 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 ( <>

workshops

- + ); } diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js new file mode 100644 index 0000000..7e7671a --- /dev/null +++ b/src/hooks/useLocalStorage.js @@ -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; diff --git a/src/hooks/useLogin.js b/src/hooks/useLogin.js index 30d8dcb..c2107b3 100644 --- a/src/hooks/useLogin.js +++ b/src/hooks/useLogin.js @@ -1,15 +1,12 @@ import { useCallback, useEffect } from 'react'; import { useRouter } from 'next/router'; -import { useDispatch } from 'react-redux'; import { useNostr } from './useNostr'; import axios from 'axios'; -import { setUser } from "@/redux/reducers/userReducer"; import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { findKind0Fields } from "@/utils/nostr"; import { useToast } from './useToast'; export const useLogin = () => { - const dispatch = useDispatch(); const router = useRouter(); const { showToast } = useToast(); const { fetchKind0 } = useNostr(); @@ -17,7 +14,8 @@ export const useLogin = () => { // Attempt Auto Login on render useEffect(() => { const autoLogin = async () => { - const publicKey = window.localStorage.getItem('pubkey'); + const user = window.localStorage.getItem('user'); + const publicKey = JSON.parse(user)?.pubkey; if (!publicKey) return; @@ -25,20 +23,26 @@ export const useLogin = () => { const response = await axios.get(`/api/users/${publicKey}`); console.log('auto login response:', response); if (response.status === 200 && response.data) { - dispatch(setUser(response.data)); + window.localStorage.setItem('user', JSON.stringify(response.data)); } else if (response.status === 204) { // User not found, create a new user const kind0 = await fetchKind0([{ authors: [publicKey], kinds: [0] }], {}); + console.log('kind0:', kind0); - const fields = await findKind0Fields(kind0); + + let fields = null; + + if (kind0) { + fields = await findKind0Fields(kind0); + } + const payload = { pubkey: publicKey, ...fields }; try { const createUserResponse = await axios.post(`/api/users`, payload); console.log('create user response:', createUserResponse); if (createUserResponse.status === 201) { - window.localStorage.setItem('pubkey', publicKey); - dispatch(setUser(createUserResponse.data)); + window.localStorage.setItem('user', JSON.stringify(createUserResponse.data)); } else { console.error('Error creating user:', createUserResponse); } @@ -70,9 +74,8 @@ export const useLogin = () => { try { const response = await axios.get(`/api/users/${publicKey}`); if (response.status !== 200) throw new Error('User not found'); - - window.localStorage.setItem('pubkey', publicKey); - dispatch(setUser(response.data)); +; + window.localStorage.setItem('user', JSON.stringify(response.data)); router.push('/'); } catch (error) { // User not found, create a new user @@ -83,9 +86,7 @@ export const useLogin = () => { try { const createUserResponse = await axios.post(`/api/users`, payload); if (createUserResponse.status === 201) { - ; - window.localStorage.setItem('pubkey', publicKey); - dispatch(setUser(createUserResponse.data)); + window.localStorage.setItem('user', JSON.stringify(createUserResponse.data)); router.push('/'); } else { console.error('Error creating user:', createUserResponse); @@ -95,22 +96,22 @@ export const useLogin = () => { showToast('error', 'Error Creating User', 'Failed to create user'); } } - }, [dispatch, router, showToast, fetchKind0]); + }, [router, showToast, fetchKind0]); const anonymousLogin = useCallback(() => { try { const secretKey = generateSecretKey(); const publicKey = getPublicKey(secretKey); + // need to fix with byteToHex + const hexSecretKey = secretKey.toString('hex'); - dispatch(setUser({ pubkey: publicKey })); - window.localStorage.setItem('pubkey', publicKey); - window.localStorage.setItem('seckey', secretKey); + window.localStorage.setItem('user', JSON.stringify({ pubkey: publicKey, secretKey: hexSecretKey })); router.push('/'); } catch (error) { console.error('Error during anonymous login:', error); showToast('error', 'Error Logging In', 'Failed to log in'); } - }, [dispatch, router, showToast]); + }, [router, showToast]); return { nostrLogin, anonymousLogin }; }; diff --git a/src/hooks/useNostr.js b/src/hooks/useNostr.js index 8504447..f8dd705 100644 --- a/src/hooks/useNostr.js +++ b/src/hooks/useNostr.js @@ -1,14 +1,25 @@ import { useState, useEffect, useRef } from "react"; -import { SimplePool, relayInit, nip19 } from "nostr-tools"; -import { useDispatch } from "react-redux"; -import { addResource, addCourse, addWorkshop, addStream } from "@/redux/reducers/eventsReducer"; -import { initialRelays } from "@/redux/reducers/userReducer"; +import { SimplePool } from "nostr-tools"; + +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 useNostr = () => { const [relays, setRelays] = useState(initialRelays); const [relayStatuses, setRelayStatuses] = useState({}); - - const dispatch = useDispatch(); + const [events, setEvents] = useState({ + resources: [], + workshops: [], + courses: [], + streams: [] + }); const pool = useRef(new SimplePool({ seenOnEnabled: true })); const subscriptions = useRef([]); @@ -34,6 +45,56 @@ export const useNostr = () => { 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) => { return new Promise((resolve, reject) => { 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) => { return new Promise((resolve, reject) => { const sub = pool.current.subscribeMany(relays, [{ ids: [id] }], { @@ -181,7 +138,7 @@ export const useNostr = () => { } }); }); - }; + } const publishEvent = async (event) => { try { @@ -212,6 +169,8 @@ export const useNostr = () => { fetchResources, fetchCourses, fetchWorkshops, + fetchStreams, getRelayStatuses, + events }; }; \ No newline at end of file diff --git a/src/pages/_app.js b/src/pages/_app.js index 02148db..82766eb 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -1,6 +1,4 @@ import { PrimeReactProvider } from 'primereact/api'; -import { Provider } from "react-redux"; -import { store } from "@/redux/store"; import Navbar from '@/components/navbar/Navbar'; import { ToastProvider } from '@/hooks/useToast'; import Layout from '@/components/Layout'; @@ -12,23 +10,21 @@ export default function MyApp({ Component, pageProps: { ...pageProps } }) { return ( - - - + + -
- - {/*
*/} - {/* */} - {/*
*/} -
- -
- {/*
*/} +
+ + {/*
*/} + {/* */} + {/*
*/} +
+
- - - - + {/*
*/} +
+ + + ); } \ No newline at end of file diff --git a/src/pages/_document.js b/src/pages/_document.js index 61eaabe..771da81 100644 --- a/src/pages/_document.js +++ b/src/pages/_document.js @@ -5,9 +5,9 @@ class MyDocument extends Document { return ( - - - + + +
diff --git a/src/pages/details/[slug].js b/src/pages/details/[slug].js index ba91137..d083a91 100644 --- a/src/pages/details/[slug].js +++ b/src/pages/details/[slug].js @@ -110,7 +110,7 @@ export default function Details() { />