mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
add cool animations to store :)
This commit is contained in:
+3
-1
@@ -53,9 +53,10 @@
|
|||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"dompurify": "^3.0.8",
|
"dompurify": "^3.0.8",
|
||||||
"framer-motion": "^10.18.0",
|
"framer-motion": "^11.0.25",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"million": "latest",
|
"million": "latest",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"swiper": "^11.1.0",
|
"swiper": "^11.1.0",
|
||||||
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "../../utils/cn";
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const MouseEnterContext = createContext<
|
||||||
|
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export const CardContainer = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isMouseEntered, setIsMouseEntered] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const { left, top, width, height } =
|
||||||
|
containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - left - width / 2) / 25;
|
||||||
|
const y = (e.clientY - top - height / 2) / 25;
|
||||||
|
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = (_: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
setIsMouseEntered(true);
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (_: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
setIsMouseEntered(false);
|
||||||
|
containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center",
|
||||||
|
containerClassName
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
perspective: "1000px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center relative transition-all duration-200 ease-linear",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transformStyle: "preserve-3d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MouseEnterContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardBody = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-96 w-96 [transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CardItem = ({
|
||||||
|
as: Tag = "div",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
translateX = 0,
|
||||||
|
translateY = 0,
|
||||||
|
translateZ = 0,
|
||||||
|
rotateX = 0,
|
||||||
|
rotateY = 0,
|
||||||
|
rotateZ = 0,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
as?: React.ElementType;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
translateX?: number | string;
|
||||||
|
translateY?: number | string;
|
||||||
|
translateZ?: number | string;
|
||||||
|
rotateX?: number | string;
|
||||||
|
rotateY?: number | string;
|
||||||
|
rotateZ?: number | string;
|
||||||
|
[key: string]: any;
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [isMouseEntered] = useMouseEnter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleAnimations();
|
||||||
|
}, [isMouseEntered]);
|
||||||
|
|
||||||
|
const handleAnimations = () => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
if (isMouseEntered) {
|
||||||
|
ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`;
|
||||||
|
} else {
|
||||||
|
ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-fit transition duration-200 ease-linear", className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a hook to use the context
|
||||||
|
export const useMouseEnter = () => {
|
||||||
|
const context = useContext(MouseEnterContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useMouseEnter must be used within a MouseEnterProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,60 +1,74 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { Scrollbar, Autoplay } from 'swiper/modules';
|
import { Autoplay } from 'swiper/modules';
|
||||||
import Header from '../components/store/header';
|
import Header from '../components/store/header';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/pagination';
|
import 'swiper/css/pagination';
|
||||||
import 'swiper/css/scrollbar';
|
import 'swiper/css/scrollbar';
|
||||||
import 'swiper/css/autoplay';
|
import 'swiper/css/autoplay';
|
||||||
|
import { CardBody, CardContainer, CardItem } from '../components/store/card';
|
||||||
|
import { spring } from 'motion';
|
||||||
|
|
||||||
const Store = () => {
|
const Store = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const swiperCover = useRef<any | null>(null);
|
const swiperCover = useRef<any | null>(null);
|
||||||
|
const [filteredThemes, setFilteredThemes] = useState<typeof gridThemes>([]);
|
||||||
|
|
||||||
|
|
||||||
const coverThemes = [
|
const coverThemes = [
|
||||||
{
|
{ coverImage: 'https://source.unsplash.com/random', name: 'Ocean View', description: 'Feel the ocean breeze with this theme.' },
|
||||||
coverImage: 'https://via.placeholder.com/300x200',
|
{ coverImage: 'https://source.unsplash.com/random?2', name: 'Mountain Majesty', description: 'Elevate your desktop to new heights.' },
|
||||||
name: 'Theme Name',
|
{ coverImage: 'https://source.unsplash.com/random?3', name: 'Urban Explorer', description: 'Bring the city life to your screen.' }
|
||||||
description: 'Theme description goes here.'
|
];
|
||||||
},
|
|
||||||
{
|
// Additional mocked themes for the grid with varied names
|
||||||
coverImage: 'https://via.placeholder.com/300x200',
|
const gridThemes = [
|
||||||
name: 'Theme Name',
|
{ image: 'https://source.unsplash.com/random', name: 'Serene Landscapes', description: 'Calm landscapes to soothe your soul.' },
|
||||||
description: 'Theme description goes here.'
|
{ image: 'https://source.unsplash.com/random?4', name: 'Cosmic Energy', description: 'Explore the outer space.' },
|
||||||
},
|
{ image: 'https://source.unsplash.com/random?5', name: 'Abstract Art', description: 'Artistic and abstract designs.' },
|
||||||
{
|
{ image: 'https://source.unsplash.com/random?6', name: 'Nature’s Wonders', description: 'The beauty of nature captured in one theme.' },
|
||||||
coverImage: 'https://via.placeholder.com/300x200',
|
{ image: 'https://source.unsplash.com/random?7', name: 'Techie Vibes', description: 'For the tech enthusiasts.' },
|
||||||
name: 'Theme Name',
|
{ image: 'https://source.unsplash.com/random?8', name: 'Cafe Culture', description: 'Experience the cafe culture on your screen.' },
|
||||||
description: 'Theme description goes here.'
|
];
|
||||||
}
|
|
||||||
]
|
useEffect(() => {
|
||||||
|
setFilteredThemes(gridThemes.filter(theme =>
|
||||||
|
theme.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
));
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-y-scroll bg-zinc-100 dark:bg-zinc-900">
|
<div className="w-screen h-screen overflow-y-scroll bg-zinc-100 dark:bg-zinc-900">
|
||||||
|
|
||||||
<Header searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
<Header searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
||||||
|
|
||||||
<div className="px-32 py-12">
|
<div className="px-24 py-12">
|
||||||
<div className="relative w-full bg-green-100 aspect-[8/3] rounded-xl overflow-clip">
|
<div className={`relative w-full rounded-xl overflow-clip transition-opacity ${searchTerm == '' ? 'opacity-100' : 'opacity-0'}`}>
|
||||||
|
<motion.div className='overflow-clip' animate={{
|
||||||
|
height: searchTerm == '' ? 'auto' : '0px'
|
||||||
|
}} transition={{
|
||||||
|
type: 'spring',
|
||||||
|
bounce: 0,
|
||||||
|
duration: 1,
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 30
|
||||||
|
}}>
|
||||||
<Swiper
|
<Swiper
|
||||||
ref={swiperCover}
|
ref={swiperCover}
|
||||||
spaceBetween={0}
|
spaceBetween={20}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
modules={[Scrollbar, Autoplay]}
|
className='w-full aspect-[8/3]'
|
||||||
navigation
|
modules={[Autoplay]}
|
||||||
autoplay={{
|
autoplay={{
|
||||||
delay: 5000,
|
delay: 5000,
|
||||||
disableOnInteraction: false,
|
disableOnInteraction: false,
|
||||||
pauseOnMouseEnter: true
|
pauseOnMouseEnter: true
|
||||||
}}
|
}}
|
||||||
pagination={{ clickable: true }}
|
|
||||||
scrollbar={{ draggable: true }}
|
|
||||||
onSlideChange={() => console.log('slide change')}
|
|
||||||
onSwiper={(swiper) => console.log(swiper)}
|
|
||||||
>
|
>
|
||||||
{ coverThemes.map((theme, index) => (
|
{ coverThemes.map((theme, index) => (
|
||||||
<SwiperSlide key={index}>
|
<SwiperSlide className='rounded-xl overflow-clip' key={index}>
|
||||||
<img
|
<img
|
||||||
src={theme.coverImage}
|
src={theme.coverImage}
|
||||||
alt="Theme Preview"
|
alt="Theme Preview"
|
||||||
@@ -63,6 +77,7 @@ const Store = () => {
|
|||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
)) }
|
)) }
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* pagination */}
|
{/* pagination */}
|
||||||
<div className='absolute z-10 flex gap-2 bottom-2 right-2'>
|
<div className='absolute z-10 flex gap-2 bottom-2 right-2'>
|
||||||
@@ -81,30 +96,38 @@ const Store = () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8 px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-8 py-4 mx-auto md:grid-cols-2 lg:grid-cols-3">
|
||||||
{
|
{filteredThemes.map((theme, index) => (
|
||||||
Array.from({ length: 30 }).map((_, index) => (
|
<CardContainer key={index} className='w-full cursor-pointer'>
|
||||||
<div key={index} className="overflow-hidden bg-white rounded-lg shadow-md dark:bg-zinc-800">
|
<CardBody className="bg-gray-50 w-full transition-all duration-300 relative group/card dark:hover:shadow-2xl dark:hover:shadow-emerald-500/[0.1] dark:bg-black dark:border-white/[0.2] border-black/[0.1] h-auto rounded-xl p-6 border">
|
||||||
<img
|
<CardItem
|
||||||
src="https://via.placeholder.com/300x200"
|
translateZ={30}
|
||||||
alt="Theme Preview"
|
className="mb-1 text-xl font-bold text-neutral-600 dark:text-white">
|
||||||
className="object-cover w-full h-48"
|
{theme.name}
|
||||||
/>
|
</CardItem>
|
||||||
<div className="p-4">
|
<CardItem
|
||||||
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
as="p"
|
||||||
Theme Name
|
translateZ={25}
|
||||||
</h3>
|
className="max-w-sm mb-4 text-sm text-neutral-500 dark:text-neutral-300">
|
||||||
<p className="mt-2 text-gray-600 dark:text-gray-400">
|
{theme.description}
|
||||||
Theme description goes here.
|
</CardItem>
|
||||||
</p>
|
<CardItem
|
||||||
<button className="px-4 py-2 mt-4 text-white bg-blue-500 rounded-full hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
className='w-full'
|
||||||
|
translateZ={15}>
|
||||||
|
<img src={theme.image} alt="Theme Preview" className="object-cover w-full h-48 rounded-md" />
|
||||||
|
</CardItem>
|
||||||
|
<CardItem>
|
||||||
|
<button className="px-4 py-2 mt-4 transition rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-800 dark:hover:bg-zinc-700 hover:bg-zinc-200 focus:outline-none focus:ring-2 focus:ring-zinc-800 focus:ring-offset-2">
|
||||||
Install
|
Install
|
||||||
</button>
|
</button>
|
||||||
|
</CardItem>
|
||||||
|
</CardBody>
|
||||||
|
</CardContainer>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user