UI / responsiveness improvements

This commit is contained in:
austinkelsay 2024-03-18 19:32:43 -05:00
parent a5f25d7c4f
commit b6797311d5
10 changed files with 176 additions and 61 deletions

View File

@ -4,16 +4,18 @@ import Image from 'next/image';
const HeroBanner = () => { const HeroBanner = () => {
const options = ['Bitcoin', 'Lightning', 'Nostr']; const options = ['Bitcoin', 'Lightning', 'Nostr'];
const [currentOption, setCurrentOption] = useState(0); const [currentOption, setCurrentOption] = useState(0);
const [fade, setFade] = useState(true); const [isFlipping, setIsFlipping] = useState(false);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setFade(false); setIsFlipping(true);
setTimeout(() => { setTimeout(() => {
setCurrentOption((prevOption) => (prevOption + 1) % options.length); setCurrentOption((prevOption) => (prevOption + 1) % options.length);
setFade(true); setTimeout(() => {
}, 700); // Half the interval time setIsFlipping(false);
}, 1500); // Change text every 2 seconds }, 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 () => clearInterval(interval);
}, []); }, []);
@ -27,16 +29,16 @@ const HeroBanner = () => {
height={1080} height={1080}
quality={100} quality={100}
/> />
<div className="absolute text-center text-white text-2xl"> <div className="absolute text-center text-white text-xl">
<p className='text-4xl max-tab:text-2xl max-mob:text-2xl'>Learn how to code</p> <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-2xl max-mob:text-2xl'> <p className='text-4xl pt-4 max-tab:text-xl max-mob:text-xl max-tab:pt-2 max-mob:pt-2'>
Build{' '} Build{' '}
<span className={`text-4xl max-tab:text-2xl max-mob:text-2xl pt-4 transition-opacity duration-500 ${fade ? 'opacity-100' : 'opacity-0'}`}> <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' : ''}`}>
{options[currentOption]} {options[currentOption]}
</span> </span>
{' '}apps {' '}apps
</p> </p>
<p className='text-4xl pt-4 max-tab:text-2xl max-mob:text-2xl'>Become a Bitcoin developer</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>
</div> </div>
</div> </div>
); );

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Button } from 'primereact/button';
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';
@ -51,18 +50,16 @@ export default function CoursesCarousel() {
}, []); // The empty array ensures this effect only runs once, similar to componentDidMount }, []); // The empty array ensures this effect only runs once, similar to componentDidMount
// Function to calculate image dimensions based on screenWidth
const calculateImageDimensions = () => { const calculateImageDimensions = () => {
if (screenWidth >= 1200) { if (screenWidth >= 1200) {
// Large screens // Large screens
return { width: 344, height: 194 }; return { width: 426, height: 240 };
} else if (screenWidth >= 768 && screenWidth < 1200) { } else if (screenWidth >= 768 && screenWidth < 1200) {
// Medium screens // Medium screens
return { width: 300, height: 169 }; return { width: 344, height: 194 };
} else { } else {
console.log('screenWidth:', screenWidth);
// Small screens // Small screens
return { width: screenWidth - 30, height: (screenWidth - 30) * (9 / 16) }; return { width: screenWidth - 50, height: (screenWidth - 50) * (9 / 16) };
} }
}; };
@ -77,26 +74,34 @@ export default function CoursesCarousel() {
const courseTemplate = (course) => { const courseTemplate = (course) => {
const { width, height } = calculateImageDimensions(); const { width, height } = calculateImageDimensions();
console.log('width:', width);
console.log('height:', height);
return ( return (
<div onClick={() => router.push(`/details/${course.id}`)} className="flex flex-col items-center w-full mx-auto px-4 cursor-pointer mt-8"> <div style={{width: width < 768 ? "auto" : width}} onClick={() => router.push(`/details/${course.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg">
<div className="w-86 h-60 bg-gray-200 overflow-hidden rounded-md shadow-lg max-tab:w-[100vw] max-mob:w-[100vw] max-tab:h-auto max-mob:h-auto"> <div style={{maxWidth: width}} className="max-tab:h-auto max-mob:h-auto">
<Image <Image
alt="resource thumbnail" alt="resource thumbnail"
src={returnImageProxy(course.image)} src={returnImageProxy(course.image)}
quality={100} quality={100}
width={width} width={width}
height={height} height={height}
className="w-full h-full object-cover object-center rounded-md"
/> />
</div> <div className='flex flex-col justify-start'>
<div className='flex flex-col justify-start max-w-[426px] max-tab:w-[100vw] max-mob:w-[100vw]'> <h4 className="mb-1 font-bold text-2xl font-blinker">{course.title}</h4>
<h4 className="mb-1 font-bold text-xl">{course.title}</h4> <p style={{
<p className='truncate'>{course.summary}</p> display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'prewrap',
font: '400 1rem/1.5 Blinker, sans-serif'
}}>
{course.summary}
</p>
<p className="text-sm mt-1 text-gray-400">Published: {formatTimestampToHowLongAgo(course.published_at)}</p> <p className="text-sm mt-1 text-gray-400">Published: {formatTimestampToHowLongAgo(course.published_at)}</p>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -68,11 +68,11 @@ const Navbar = () => {
const start = ( const start = (
<div className='flex items-center'> <div className='flex items-center'>
<div className='hidden max-tab:block max-mob:block max-tab:px-6 max-mob:px-6'> {/* <div className='hidden max-tab:block max-mob:block max-tab:px-6 max-mob:px-6'>
<i className="pi pi-bars text-xl pt-1" <i className="pi pi-bars text-xl pt-1"
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> */}
<div onClick={() => router.push('/')} className="flex flex-row items-center justify-center cursor-pointer"> <div onClick={() => router.push('/')} className="flex flex-row items-center justify-center cursor-pointer">
<Image <Image
alt="logo" alt="logo"
@ -92,7 +92,7 @@ const Navbar = () => {
<Menubar <Menubar
start={start} start={start}
end={UserAvatar} end={UserAvatar}
className='px-[2%] bg-gray-800 border-t-0 border-l-0 border-r-0 rounded-none fixed z-10 w-[100vw]' className='px-[2%] py-[2%] bg-gray-800 border-t-0 border-l-0 border-r-0 rounded-none fixed z-10 w-[100vw] max-tab:px-[5%] max-mob:px-[5%]'
style={{ height: navbarHeight }} style={{ height: navbarHeight }}
/> />
</div> </div>

View File

@ -6,6 +6,7 @@ import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu'; import { Menu } from 'primereact/menu';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { setUser } from '@/redux/reducers/userReducer'; import { setUser } from '@/redux/reducers/userReducer';
import useWindowWidth from '@/hooks/useWindowWidth';
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';
@ -15,6 +16,7 @@ const UserAvatar = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const user = useSelector((state) => state.user.user); const user = useSelector((state) => state.user.user);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth();
const menu = useRef(null); const menu = useRef(null);
@ -71,6 +73,7 @@ const UserAvatar = () => {
className="text-[#f8f8ff]" className="text-[#f8f8ff]"
rounded rounded
onClick={() => router.push('/login')} onClick={() => router.push('/login')}
size={windowWidth < 768 ? 'small' : 'normal'}
/> />
); );
} }

View File

@ -28,10 +28,40 @@ const responsiveOptions = [
export default function WorkshopsCarousel() { export default function WorkshopsCarousel() {
const workshops = useSelector((state) => state.events.resources); const workshops = useSelector((state) => state.events.resources);
const [processedWorkshops, setProcessedWorkshops] = useState([]); const [processedWorkshops, setProcessedWorkshops] = useState([]);
const [screenWidth, setScreenWidth] = useState(null);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const router = useRouter(); const router = useRouter();
useEffect(() => {
// Update the state to the current window width
setScreenWidth(window.innerWidth);
const handleResize = () => {
// Update the state to the new window width when it changes
setScreenWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Remove the event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []); // The empty array ensures this effect only runs once, similar to componentDidMount
const calculateImageDimensions = () => {
if (screenWidth >= 1200) {
// Large screens
return { width: 426, height: 240 };
} else if (screenWidth >= 768 && screenWidth < 1200) {
// Medium screens
return { width: 344, height: 194 };
} else {
// Small screens
return { width: screenWidth - 50, height: (screenWidth - 50) * (9 / 16) };
}
};
useEffect(() => { useEffect(() => {
const processWorkshops = workshops.map(workshop => { const processWorkshops = workshops.map(workshop => {
const { id, content, title, summary, image, published_at } = parseEvent(workshop); const { id, content, title, summary, image, published_at } = parseEvent(workshop);
@ -41,21 +71,33 @@ export default function WorkshopsCarousel() {
}, [workshops]); }, [workshops]);
const workshopTemplate = (workshop) => { const workshopTemplate = (workshop) => {
const { width, height } = calculateImageDimensions();
return ( return (
<div onClick={() => router.push(`/details/${workshop.id}`)} className="flex flex-col items-center w-full mx-auto px-4 cursor-pointer mt-8"> <div style={{width: width < 768 ? "auto" : width}} onClick={() => router.push(`/details/${workshop.id}`)} className="flex flex-col items-center mx-auto px-4 cursor-pointer mt-8 rounded-md shadow-lg">
<div className="w-86 h-60 bg-gray-200 overflow-hidden rounded-md shadow-lg max-tab:w-[264px] max-mob:w-[244px]"> <div style={{maxWidth: width}} className="max-tab:h-auto max-mob:h-auto">
<Image <Image
alt="resource thumbnail" alt="resource thumbnail"
src={returnImageProxy(workshop.image)} src={returnImageProxy(workshop.image)}
width={344} quality={100}
height={194} width={width}
className="w-full h-full object-cover object-center" height={height}
className="w-full h-full object-cover object-center rounded-md"
/> />
<div className='flex flex-col justify-start'>
<h4 className="mb-1 font-bold text-2xl font-blinker">{workshop.title}</h4>
<p style={{
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'prewrap',
font: '400 1rem/1.5 Blinker, sans-serif'
}}>
{workshop.summary}
</p>
<p className="text-sm mt-1 text-gray-400 font-blinker">Published: {formatTimestampToHowLongAgo(workshop.published_at)}</p>
</div> </div>
<div className='flex flex-col justify-start max-w-[426px] max-tab:w-[264px] max-mob:w-[244px]'>
<h4 className="mb-1 font-bold text-xl">{workshop.title}</h4>
<p className='truncate'>{workshop.summary}</p>
<p className="text-sm mt-1 text-gray-400">Published: {formatTimestampToHowLongAgo(workshop.published_at)}</p>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,26 @@
import { useState, useEffect } from 'react';
const useWindowWidth = () => {
const [windowWidth, setWindowWidth] = useState(undefined);
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width to state
setWindowWidth(window.innerWidth);
}
// Add event listener
window.addEventListener('resize', handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array ensures that effect is only run on mount and unmount
return windowWidth;
};
export default useWindowWidth;

View File

@ -18,12 +18,13 @@ export default function MyApp({
<Layout> <Layout>
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
<Navbar /> <Navbar />
<div className='flex'> {/* <div className='flex'> */}
<Sidebar /> {/* <Sidebar /> */}
<div className='max-w-[100vw] pl-[15vw]'> {/* <div className='max-w-[100vw] pl-[15vw]'> */}
<div className='max-w-[100vw]'>
<Component {...pageProps} /> <Component {...pageProps} />
</div> </div>
</div> {/* </div> */}
</div> </div>
</Layout> </Layout>
</ToastProvider> </ToastProvider>

View File

@ -1,13 +1,21 @@
import { Html, Head, Main, NextScript } from 'next/document' import Document, { Html, Head, Main, NextScript } from 'next/document';
export default function Document() { class MyDocument extends Document {
render() {
return ( return (
<Html lang="en"> <Html>
<Head /> <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" />
</Head>
<body> <body>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
) );
} }
}
export default MyDocument;

View File

@ -9,6 +9,11 @@
@tailwind utilities; @tailwind utilities;
} }
body {
font-family: 'Blinker', sans-serif;
}
.p-tabmenu .p-tabmenu-nav { .p-tabmenu .p-tabmenu-nav {
background-color: transparent !important; background-color: transparent !important;
border: none !important; border: none !important;
@ -26,3 +31,23 @@
.p-button .pi.pi-bolt { .p-button .pi.pi-bolt {
color: yellow; color: yellow;
} }
/* hero banner animation */
@keyframes flip {
0%, 100% {
transform: rotateX(0);
opacity: 1;
}
45%, 60% { /* Adjusted for quicker opacity transition */
transform: rotateX(180deg);
opacity: 0;
}
}
.flip-enter-active, .flip-exit-active {
animation-name: flip;
animation-duration: 800ms; /* Keep as is for smooth transition */
animation-timing-function: ease-in-out;
transform-origin: center center;
animation-fill-mode: forwards; /* Ensures the end state of the animation is retained */
}

View File

@ -15,6 +15,9 @@ module.exports = {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
fontFamily: {
'blinker': ['Blinker', 'sans-serif'],
},
}, },
}, },
plugins: [], plugins: [],