mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-04-19 19:01:19 +00:00
Rip out redux for local storage, reowkr hooks, more responsiveness fixes
This commit is contained in:
parent
919f13c88c
commit
7abf1c8882
@ -2,7 +2,7 @@
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
domains: ['localhost', 'secure.gravatar.com'],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<div className="relative flex justify-center items-center">
|
||||
@ -28,18 +37,20 @@ const HeroBanner = () => {
|
||||
width={1920}
|
||||
height={1080}
|
||||
quality={100}
|
||||
className='opacity-70'
|
||||
/>
|
||||
<div className="absolute text-center text-white text-xl">
|
||||
<p className='text-4xl max-tab:text-xl max-mob:text-xl'>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'>
|
||||
<div className="absolute text-center text-white text-xl h-full flex flex-col justify-evenly">
|
||||
<p className='text-2xl md:text-3xl lg:text-4xl xl:text-5xl'>Learn how to code</p>
|
||||
<p className='text-2xl md:text-3xl lg:text-4xl xl:text-5xl'>
|
||||
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]}
|
||||
</span>
|
||||
{' '}apps
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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 = (
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
alt="logo"
|
||||
src={user?.avatar ? returnImageProxy(user.avatar) : `https://secure.gravatar.com/avatar/${user.pubkey}?s=90&d=identicon`}
|
||||
width={50}
|
||||
height={50}
|
||||
className={styles.logo}
|
||||
/>
|
||||
</div>
|
||||
<Menu model={items} popup ref={menu} />
|
||||
</>
|
||||
|
@ -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 (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
35
src/hooks/useLocalStorage.js
Normal file
35
src/hooks/useLocalStorage.js
Normal 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;
|
@ -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 };
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
};
|
@ -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 (
|
||||
<Provider store={store}>
|
||||
<PrimeReactProvider>
|
||||
<ToastProvider>
|
||||
<PrimeReactProvider>
|
||||
<ToastProvider>
|
||||
<Layout>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar />
|
||||
{/* <div className='flex'> */}
|
||||
{/* <Sidebar /> */}
|
||||
{/* <div className='max-w-[100vw] pl-[15vw]'> */}
|
||||
<div className='max-w-[100vw]'>
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
{/* </div> */}
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar />
|
||||
{/* <div className='flex'> */}
|
||||
{/* <Sidebar /> */}
|
||||
{/* <div className='max-w-[100vw] pl-[15vw]'> */}
|
||||
<div className='max-w-[100vw]'>
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</Layout>
|
||||
</ToastProvider>
|
||||
</PrimeReactProvider>
|
||||
</Provider>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</Layout>
|
||||
</ToastProvider>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
}
|
@ -5,9 +5,9 @@ class MyDocument extends Document {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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 rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<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" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
@ -110,7 +110,7 @@ export default function Details() {
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-bolt"
|
||||
label="100 sats"
|
||||
label="Zap"
|
||||
severity="success"
|
||||
outlined
|
||||
pt={{
|
||||
|
@ -1,18 +1,10 @@
|
||||
import Head from 'next/head'
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React from 'react';
|
||||
import CoursesCarousel from '@/components/courses/CoursesCarousel'
|
||||
import WorkshopsCarousel from '@/components/workshops/WorkshopsCarousel'
|
||||
import HeroBanner from '@/components/banner/HeroBanner';
|
||||
import { useNostr } from '@/hooks/useNostr'
|
||||
|
||||
export default function Home() {
|
||||
const { fetchResources, fetchCourses } = useNostr();
|
||||
|
||||
useEffect(() => {
|
||||
fetchResources();
|
||||
fetchCourses();
|
||||
}, [fetchResources, fetchCourses]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -5,10 +5,11 @@ import { Menu } from 'primereact/menu';
|
||||
import { Column } from 'primereact/column';
|
||||
import { useSelector } from "react-redux";
|
||||
import { useImageProxy } from "@/hooks/useImageProxy";
|
||||
import useLocalStorage from "@/hooks/useLocalStorage";
|
||||
import Image from "next/image";
|
||||
|
||||
const Profile = () => {
|
||||
const user = useSelector((state) => state.user.user);
|
||||
const [user, setUser] = useLocalStorage('user', {});
|
||||
const { returnImageProxy } = useImageProxy();
|
||||
const menu = useRef(null);
|
||||
|
||||
@ -61,22 +62,20 @@ const Profile = () => {
|
||||
<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="relative flex w-full items-center justify-center">
|
||||
{user.avatar && (
|
||||
<Image
|
||||
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}
|
||||
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}</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>
|
||||
<div className="flex flex-col w-1/2 mx-auto my-4 justify-between items-center">
|
||||
<h2>Subscription</h2>
|
||||
|
@ -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;
|
@ -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;
|
@ -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,
|
||||
}
|
||||
});
|
@ -10,6 +10,7 @@ module.exports = {
|
||||
screens: {
|
||||
'max-mob': {'max': '475px'},
|
||||
'max-tab': {'max': '768px'},
|
||||
'max-lap': {'max': '1440px'},
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
|
Loading…
x
Reference in New Issue
Block a user