mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
UI / responsiveness improvements
This commit is contained in:
parent
a5f25d7c4f
commit
b6797311d5
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
26
src/hooks/useWindowWidth.js
Normal file
26
src/hooks/useWindowWidth.js
Normal 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;
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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 */
|
||||||
|
}
|
||||||
|
@ -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: [],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user