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 = {
reactStrictMode: true,
images: {
domains: ['localhost'],
domains: ['localhost', 'secure.gravatar.com'],
},
}

View File

@ -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>
);
};

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 { 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} />
</>
);
}

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 { 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} />
</>

View File

@ -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} />
</>
);
}

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 { 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 };
};

View File

@ -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
};
};

View File

@ -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>
);
}

View File

@ -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 />

View File

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

View File

@ -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>

View File

@ -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>

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: {
'max-mob': {'max': '475px'},
'max-tab': {'max': '768px'},
'max-lap': {'max': '1440px'},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',