ok
This commit is contained in:
575
src/components/CoverflowCarousel.module.css
Normal file
575
src/components/CoverflowCarousel.module.css
Normal file
@@ -0,0 +1,575 @@
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
margin: 40px 0 60px 0;
|
||||
perspective: 1000px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.carouselWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform-style: preserve-3d;
|
||||
perspective: 1500px;
|
||||
}
|
||||
|
||||
/* Entering edge helpers to ensure ease-out even with <5 products */
|
||||
/* entering helpers removed (reverted) */
|
||||
|
||||
/* Edge fade-out on shift for better intuition */
|
||||
.carouselWrapper.shiftingLeft .cardContainer.positionNeg2 {
|
||||
opacity: 0;
|
||||
transform: translateX(-420px) rotateY(62deg) scale(0.66);
|
||||
}
|
||||
|
||||
.carouselWrapper.shiftingRight .cardContainer.position2 {
|
||||
opacity: 0;
|
||||
transform: translateX(420px) rotateY(-62deg) scale(0.66);
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
height: 370px;
|
||||
cursor: pointer;
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: center center;
|
||||
transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transform: translateX(0) rotateY(0) scale(1);
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
aspect-ratio: 320/370;
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
transform-style: preserve-3d;
|
||||
backface-visibility: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 2; /* ensure content stacks above shadow */
|
||||
}
|
||||
|
||||
/* Ground shadow to enhance 3D standing effect */
|
||||
.cardShadow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 6px;
|
||||
width: 92%;
|
||||
height: 32px;
|
||||
transform: translateX(-50%) scale(1);
|
||||
background: radial-gradient(ellipse at center, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.32) 45%, rgba(0,0,0,0) 78%);
|
||||
filter: blur(8px);
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cardContainer.position0 .cardShadow {
|
||||
opacity: 0.8;
|
||||
transform: translateX(-50%) scale(1.15);
|
||||
}
|
||||
|
||||
.cardContainer.positionNeg1 .cardShadow,
|
||||
.cardContainer.position1 .cardShadow {
|
||||
opacity: 0.55;
|
||||
transform: translateX(-50%) scale(1.02);
|
||||
}
|
||||
|
||||
.cardContainer.positionNeg2 .cardShadow,
|
||||
.cardContainer.position2 .cardShadow {
|
||||
opacity: 0.42;
|
||||
transform: translateX(-50%) scale(0.96);
|
||||
}
|
||||
|
||||
/* Initial state: subtle pop-in */
|
||||
.cardContainer.initial {
|
||||
transform: translateX(0) translateY(20px) rotateY(0) scale(0.92);
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* New load spread: compact fan-out from center */
|
||||
.cardContainer.spread.positionNeg2 {
|
||||
transform: translateX(-320px) translateZ(-120px) rotateY(10deg) scale(0.9);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cardContainer.spread.positionNeg1 {
|
||||
transform: translateX(-160px) translateZ(-60px) rotateY(6deg) scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cardContainer.spread.position0 {
|
||||
transform: translateX(0) translateZ(0) rotateY(0) scale(1.06);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cardContainer.spread.position1 {
|
||||
transform: translateX(160px) translateZ(-60px) rotateY(-6deg) scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.cardContainer.spread.position2 {
|
||||
transform: translateX(320px) translateZ(-120px) rotateY(-10deg) scale(0.9);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Position classes for coverflow effect (closer to center) */
|
||||
.cardContainer.positionNeg2 {
|
||||
transform: translateX(-380px) rotateY(55deg) scale(0.7);
|
||||
opacity: 1;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.cardContainer.positionNeg1 {
|
||||
transform: translateX(-190px) rotateY(35deg) scale(0.8);
|
||||
opacity: 1;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cardContainer.position0 {
|
||||
transform: translateX(0) rotateY(0deg) scale(1);
|
||||
opacity: 1;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.cardContainer.position1 {
|
||||
transform: translateX(190px) rotateY(-35deg) scale(0.8);
|
||||
opacity: 1;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cardContainer.position2 {
|
||||
transform: translateX(380px) rotateY(-55deg) scale(0.7);
|
||||
opacity: 1;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Edge-enter overrides (placed after position rules to win cascade) */
|
||||
.cardContainer.enterFromRight.position1,
|
||||
.cardContainer.enterFromRight.position2 {
|
||||
opacity: 0;
|
||||
transform: translateX(520px) rotateY(-62deg) scale(0.66);
|
||||
}
|
||||
|
||||
.cardContainer.enterFromLeft.positionNeg1,
|
||||
.cardContainer.enterFromLeft.positionNeg2 {
|
||||
opacity: 0;
|
||||
transform: translateX(-520px) rotateY(62deg) scale(0.66);
|
||||
}
|
||||
|
||||
.cardImage {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
transition: transform 0.4s ease;
|
||||
position: relative;
|
||||
background-color: #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 4/3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholderImage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f1f5f9;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 4/3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholderImage span {
|
||||
font-size: 2rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
padding: 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.3;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.85rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cardFooter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding-top: 1rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.priceContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.freePrice {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.paidPrice {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.enrollButton {
|
||||
background: linear-gradient(135deg, #6a59ff 0%, #8261ee 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(106, 89, 255, 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.enrollButton:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(106, 89, 255, 0.3);
|
||||
}
|
||||
|
||||
.enrollButton:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.noCourses {
|
||||
text-align: center;
|
||||
grid-column: 1 / -1;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.loadingCourses {
|
||||
text-align: center;
|
||||
grid-column: 1 / -1;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #e2e8f0;
|
||||
border-top: 3px solid #6a59ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Navigation buttons */
|
||||
.navButton {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: #475569;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.navButton:hover:not(:disabled) {
|
||||
border-color: #6a59ff;
|
||||
color: #6a59ff;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.navButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.navButton:first-of-type {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.navButton:last-of-type {
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
/* Dots indicator */
|
||||
.dotsContainer {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #cbd5e1;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background-color: #6a59ff;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.dot:hover {
|
||||
background-color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 1200px) {
|
||||
.container {
|
||||
height: 430px;
|
||||
margin: 35px 0 55px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 300px;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.navButton:first-of-type {
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
.navButton:last-of-type {
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
.dotsContainer {
|
||||
bottom: -35px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.container {
|
||||
height: 410px;
|
||||
margin: 30px 0 50px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 280px;
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.navButton:first-of-type {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.navButton:last-of-type {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.dotsContainer {
|
||||
bottom: -30px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
height: 380px;
|
||||
margin: 25px 0 45px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 250px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.navButton:first-of-type {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.navButton:last-of-type {
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.navButton svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dotsContainer {
|
||||
bottom: -25px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.container {
|
||||
height: 350px;
|
||||
margin: 20px 0 40px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 220px;
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
.cardWrapper {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.navButton:first-of-type {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.navButton:last-of-type {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.navButton svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.dotsContainer {
|
||||
bottom: -20px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.container {
|
||||
height: 320px;
|
||||
margin: 15px 0 35px 0;
|
||||
}
|
||||
|
||||
.cardContainer {
|
||||
width: 200px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.navButton svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.dotsContainer {
|
||||
bottom: -18px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user