Skip to content

Commit

Permalink
feature: carousel v2 component (#402)
Browse files Browse the repository at this point in the history
* feat: add carousel v2 component

* chore: update tailwind shadow and style

* feat: add carousel v2 to home page

* feat: carousel v2 transition animation
  • Loading branch information
JohnsonMao authored Aug 4, 2024
1 parent f9e4097 commit 1d26dcc
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 22 deletions.
47 changes: 47 additions & 0 deletions components/shared/Carousel/v2/Carousel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react";
import Carousel from "./";

const Card = (props: any) => (
<div className="text-white mx-4 px-12 py-8 border border-white rounded-md text-8xl font-mono">
{props.name}
</div>
);

const meta: Meta<typeof Carousel> = {
title: "Data Display/CarouselV2",
component: Carousel,
tags: ["autodocs"],
decorators: [
(Story, ctx) => {
return (
<div className="flex justify-center">
<Story {...ctx} />
</div>
);
},
],
args: {
uniqueKey: "name",
items: [{ name: "TEST 1" }, { name: "TEST 2" }, { name: "TEST 3" }],
Component: Card,
},
argTypes: {
Component: {
options: ["Card"],
defaultValue: Card,
mapping: {
Card,
},
},
},
};

export default meta;

type Story = StoryObj<typeof Carousel>;

export const Playground: Story = {
render: (args) => <Carousel {...args} />,
};

Playground.args = {};
93 changes: 93 additions & 0 deletions components/shared/Carousel/v2/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { CSSProperties, FC, useEffect, useRef, useState } from "react";
import Icon from "../../Icon/v2/Icon";

interface CarouselProps<
Item extends Record<string, unknown>,
Key extends keyof Item = keyof Item,
> {
items: (Item & Record<Key, string>)[];
uniqueKey: Key;
Component: FC<Item>;
}

export default function Carousel<Item extends Record<string, unknown>>({
items,
uniqueKey,
Component,
}: Readonly<CarouselProps<Item>>) {
const [showIndex, setShowIndex] = useState(0);
const [maxWidth, setMaxWidth] = useState(0);
const carouselRef = useRef<HTMLDivElement>(null);

const handleChangePage = (action: "prev" | "next") => () => {
const maxIndex = items.length - 1;

switch (action) {
case "prev":
setShowIndex((preIndex) => {
const newIndex = preIndex - 1;
return newIndex > -1 ? newIndex : maxIndex;
});
break;
case "next":
setShowIndex((preIndex) => {
const newIndex = preIndex + 1;
return newIndex <= maxIndex ? newIndex : 0;
});
break;
default:
}
};

const buttonClassName =
"p-2.5 shrink-0 bg-white/4 shadow-default rounded-2xl";
const buttonIconClassName = "stroke-white w-6 h-6 pointer-events-none";

useEffect(() => {
setMaxWidth(carouselRef.current?.clientWidth || 0);
}, []);

return (
<div className="flex items-center">
<button
type="button"
className={buttonClassName}
onClick={handleChangePage("prev")}
>
<Icon name="navArrowLeft" className={buttonIconClassName} />
</button>
<div ref={carouselRef} className="overflow-hidden w-full">
<ul
className="flex transition-transform translate-x-[var(--translate-x)]"
style={
{
"--translate-x": `${showIndex * maxWidth * -1}px`,
} as CSSProperties
}
>
{Array.isArray(items) &&
items.map((item) => (
<li
key={item[uniqueKey]}
className="shrink-0 w-[var(--max-width)]"
style={
{
"--max-width": `${maxWidth}px`,
} as CSSProperties
}
>
<Component {...item} />
</li>
))}
</ul>
</div>
<button
type="button"
className={buttonClassName}
onClick={handleChangePage("next")}
>
<Icon name="navArrowRight" className={buttonIconClassName} />
</button>
</div>
);
}
5 changes: 5 additions & 0 deletions components/shared/Carousel/v2/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Carousel from "./Carousel";

export * from "./Carousel";

export default Carousel;
2 changes: 1 addition & 1 deletion components/shared/Chat/v2/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function Chat({
lobbyMessages,
friendList,
roomMessages,
maxHeight = "calc(100vh - 10rem)",
maxHeight = "calc(100vh - 168px)",
}: Readonly<ChatProps>) {
const [messages, setMessages] = useState(lobbyMessages);
const [target, setTarget] = useState<[ChatTab["id"], string | null]>([
Expand Down
4 changes: 2 additions & 2 deletions components/shared/Icon/v2/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import leadingIcon from "./svgs/leading-icon.svg";
import linkedin from "./svgs/linkedin.svg";
import logOut from "./svgs/log-out.svg";
import longArrowUpLeft from "./svgs/long-arrow-up-left.svg";
import navAarrowLeft from "./svgs/nav-arrow-left.svg";
import navArrowLeft from "./svgs/nav-arrow-left.svg";
import navArrowRight from "./svgs/nav-arrow-right.svg";
import nonpublic from "./svgs/nonpublic.svg";
import notificationDefault from "./svgs/notification-default.svg";
Expand Down Expand Up @@ -73,7 +73,7 @@ const icons = {
linkedin,
logOut,
longArrowUpLeft,
navAarrowLeft,
navArrowLeft,
navArrowRight,
nonpublic,
notificationDefault,
Expand Down
117 changes: 101 additions & 16 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,104 @@
import { GetStaticProps } from "next";
import Button from "@/components/shared/Button";
import CreateRoomModal from "@/components/lobby/CreateRoomModal";
import Image from "next/image";
import Link from "next/link";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useTranslation } from "next-i18next";

import Carousel, { mockCarouselItems } from "@/components/shared/Carousel";
import Button from "@/components/shared/Button";
import CreateRoomModal from "@/components/lobby/CreateRoomModal";
import { mockCarouselItems } from "@/components/shared/Carousel";
import CarouselV2 from "@/components/shared/Carousel/v2";
import FastJoinButton from "@/components/lobby/FastJoinButton";
import SearchBar from "@/components/shared/SearchBar";

function CarouselCard({
imgUrl,
imgAlt,
}: Readonly<(typeof mockCarouselItems)[number]>) {
return (
<div className="flex text-white px-12 gap-4">
<div className="relative flex-[60%]">
<Image src={imgUrl} alt={imgAlt} draggable={false} priority fill />
</div>
<div className="flex-[40%] p-4 rounded-lg bg-primary-50/8">
<div className="text-xs text-primary-300">Massive Monster</div>
<div className="text-xl text-primary-100">AZUL ({imgAlt})</div>
<div className="flex mb-2 text-xs text-primary-300">
<div className="flex-1">4.6 * * * * * (14)</div>
<time className="flex-1">2023.08.25</time>
</div>
<div className="mb-3">
<ul className="flex gap-3">
<li className="relative w-16 h-10">
<Image
src="https://images.unsplash.com/photo-1533237264985-ee62f6d342bb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80"
alt={imgAlt}
draggable={false}
priority
fill
/>
</li>
<li className="relative w-16 h-10">
<Image
src="https://images.unsplash.com/photo-1613160717888-faa82cdb8a94?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
alt={imgAlt}
draggable={false}
priority
fill
/>
</li>
<li className="relative w-16 h-10">
<Image
src="https://images.unsplash.com/photo-1601987177651-8edfe6c20009?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
alt={imgAlt}
draggable={false}
priority
fill
/>
</li>
<li className="relative w-16 h-10">
<Image
src="https://images.unsplash.com/photo-1511512578047-dfb367046420?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2071&q=80"
alt={imgAlt}
draggable={false}
priority
fill
/>
</li>
</ul>
</div>
<div className="mb-3 text-xs text-primary-50">
《AZUL》是強手棋類休閒遊戲,在遊戲中,你可以任意選擇比賽地圖、參賽角色和組隊方式,使用卡片等方式賺取金錢,最終取得比賽勝利。
</div>
<div>
<ul className="flex flex-wrap gap-2 text-xs">
<li className="bg-primary-800 text-primary-300 rounded-full px-2 py-0.5">
回合制
</li>
<li className="bg-primary-800 text-primary-300 rounded-full px-2 py-0.5">
第三人稱
</li>
<li className="bg-primary-800 text-primary-300 rounded-full px-2 py-0.5">
策略型
</li>
<li className="bg-primary-800 text-primary-300 rounded-full px-2 py-0.5">
玩家對戰
</li>
<li className="bg-primary-800 text-primary-300 rounded-full px-2 py-0.5">
輕鬆休閒
</li>
</ul>
</div>
</div>
</div>
);
}

export default function Home() {
const { t } = useTranslation("rooms");
return (
<div>
<div className="flex justify-center">
<div className="max-w-[1036px] mx-auto px-6">
<div className="flex justify-center mb-6">
<SearchBar
leftSlot={
<button type="button" className="pl-5 pr-2.5 px-4 text-primary-300">
Expand All @@ -22,20 +107,20 @@ export default function Home() {
}
/>
</div>
<div className="px-[18px] mt-[12px] mb-[22px] w-[calc(100vw-100px)]">
<Carousel
itemWidth={332}
itemHeight={158}
<div>
<CarouselV2
items={mockCarouselItems}
itemsToSlide={2}
autoplay
uniqueKey="imgAlt"
Component={CarouselCard}
/>
</div>
<CreateRoomModal />
<Button component={Link} href="/rooms">
{t("rooms_list")}
</Button>
<FastJoinButton />
<div className="my-6 flex gap-3">
<CreateRoomModal />
<Button component={Link} href="/rooms">
{t("rooms_list")}
</Button>
<FastJoinButton />
</div>
</div>
);
}
Expand Down
12 changes: 9 additions & 3 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ module.exports = {
"./containers/**/*.{js,ts,jsx,tsx}",
],
theme: {
fontFamily: {
body: ['"Noto Sans TC"', "Roboto"],
},
extend: {
fontFamily: {
body: ['"Noto Sans TC"', "Roboto"],
},
width: {
18: "4.5rem" /** 72px */,
},
Expand Down Expand Up @@ -139,6 +139,12 @@ module.exports = {
0 -1px 1px rgba(255, 255, 255, 0.1)
`,
},
".shadow-default": {
"box-shadow": `
inset 2px 2px 4px rgba(135, 135, 135, 0.1),
1px 2px 4px rgba(0, 0, 0, 0.1)
`,
},
});
}),
plugin(({ addUtilities }) => {
Expand Down

0 comments on commit 1d26dcc

Please sign in to comment.