Skip to content

Commit 66bc35f

Browse files
committed
refactor(table)
1 parent c96daef commit 66bc35f

File tree

12 files changed

+239
-71
lines changed

12 files changed

+239
-71
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Metadata } from "next";
2+
3+
export const metadata: Metadata = {
4+
title: "ZComponent | Carousel",
5+
description: "Carousel component with React with Tailwind CSS",
6+
};
7+
8+
export default function RootLayout({
9+
children,
10+
}: Readonly<{
11+
children: React.ReactNode;
12+
}>) {
13+
return <main>{children}</main>;
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Carousel from "@/components/component/carousel";
2+
import Title from "@/components/ui/Title";
3+
import Wrapper from "@/components/ui/Wrapper";
4+
5+
export default function Page() {
6+
return (
7+
<div>
8+
<Title title="Canban" subtitle="canban" />
9+
<Wrapper>
10+
<Carousel />
11+
</Wrapper>
12+
</div>
13+
);
14+
}

app/(root)/component/table/page.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import Table from "@/components/component/table";
22
import Title from "@/components/ui/Title";
3-
import Wrapper from "@/components/ui/Wrapper";
43

54
export default function Page() {
65
return (
76
<div>
87
<Title title="Table" subtitle="table" />
9-
<Wrapper>
10-
<Table />
11-
</Wrapper>
8+
<Table />
129
</div>
1310
);
1411
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
import { useScrollableSlider } from "@/hooks";
3+
import { useState } from "react";
4+
5+
const Carousel = () => {
6+
const [currentSlide, setCurrentSlide] = useState(0);
7+
const {
8+
sliderEl,
9+
sliderPrevBtn,
10+
sliderNextBtn,
11+
scrollToTheRight,
12+
scrollToTheLeft,
13+
} = useScrollableSlider();
14+
15+
const LENGTH = 10;
16+
17+
return (
18+
<div className="relative w-full">
19+
<div className="overflow-hidden">
20+
<div
21+
className="flex w-full gap-5 overflow-x-scroll no-scrollbar"
22+
ref={sliderEl}
23+
>
24+
{Array.from({ length: LENGTH }).map((_, index) => (
25+
<div
26+
key={index}
27+
className="w-96 h-20 shrink-0 bg-gray-300 flex items-center justify-center"
28+
>
29+
<p className="font-bold text-dark">{index}</p>
30+
</div>
31+
))}
32+
</div>
33+
</div>
34+
<button ref={sliderPrevBtn} onClick={() => scrollToTheLeft()}>
35+
Prev
36+
</button>
37+
<button ref={sliderNextBtn} onClick={() => scrollToTheRight()}>
38+
Next
39+
</button>
40+
<div>
41+
{Array.from({ length: LENGTH }).map((_, index) => (
42+
<span
43+
key={index}
44+
onClick={() => setCurrentSlide(index)}
45+
className={currentSlide === index ? "w-10" : "w-5"}
46+
/>
47+
))}
48+
</div>
49+
</div>
50+
);
51+
};
52+
53+
export default Carousel;

components/component/table/index.tsx

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ export default function Index() {
1010
const [select, setSelect] = useState<string[]>([]);
1111

1212
const columns = [
13+
{
14+
title: "Title",
15+
item: "title",
16+
width: 50,
17+
sortable: true,
18+
},
19+
{
20+
title: "Title",
21+
item: "title",
22+
width: 50,
23+
sortable: true,
24+
},
1325
{
1426
title: "Title",
1527
item: "title",
@@ -25,29 +37,41 @@ export default function Index() {
2537
{
2638
title: "Image",
2739
item: "image",
28-
width: 50,
40+
width: 80,
2941
sortable: true,
3042
render: (row: any) => {
3143
return (
32-
<Image
33-
src={logo}
34-
alt={row.title}
35-
width={50}
36-
height={50}
37-
/>
44+
<div className="flex justify-center w-20">
45+
<Image
46+
className="w-40"
47+
src={logo}
48+
alt={row.title}
49+
width={50}
50+
height={50}
51+
/>
52+
</div>
53+
);
54+
},
55+
},
56+
{
57+
title: "Title",
58+
item: "title",
59+
width: 80,
60+
sortable: true,
61+
render: () => {
62+
return (
63+
<div className="rounded-full p-0.5 px-2 bg-red-500/50">title</div>
3864
);
3965
},
4066
},
4167
];
4268

4369
return (
44-
<div>
45-
<Table
46-
select={select}
47-
setSelect={setSelect}
48-
columns={columns}
49-
data={RANDOM_ITEMS}
50-
/>
51-
</div>
70+
<Table
71+
select={select}
72+
setSelect={setSelect}
73+
columns={columns}
74+
data={RANDOM_ITEMS}
75+
/>
5276
);
5377
}

components/ui/component/Table.tsx

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { TbTableMinus } from "react-icons/tb";
2+
import Checkbox from "../form/Checkbox";
3+
import { shortText } from "@/lib/utils";
24

35
export default function Table({ select, setSelect, columns, data }: ITable) {
46
const handleCheckboxClick = (e: React.ChangeEvent<HTMLInputElement>) => {
57
const isChecked = e.target.checked;
6-
const id = e.target.id;
8+
const id = e.target.name;
79

810
if (id === "select-all") {
911
if (isChecked) {
10-
const allIds = Array.from(document.querySelectorAll(".checkbox"))
11-
.filter((checkbox) => checkbox !== e.target)
12-
.map((checkbox) => checkbox.id);
12+
const allIds = data.map((_, index) => String(index));
1313
setSelect(allIds);
1414
} else {
1515
setSelect([]);
@@ -26,20 +26,17 @@ export default function Table({ select, setSelect, columns, data }: ITable) {
2626
};
2727

2828
return (
29-
<div className="border border-dark-800 rounded-t-lg overflow-x-auto">
29+
<div className="border border-dark-800 rounded-t-lg overflow-x-auto custom-scrollbar w-[98vh]">
3030
<table className="table w-full">
3131
<thead>
32-
<tr className="[&>th]:text-left bg-dark-350 rounded-t-lg bg-light *:font-semibold [&>th]:py-5">
32+
<tr className="[&>th]:text-left bg-dark-200 rounded-t-lg bg-light *:font-semibold [&>th]:py-5">
3333
<th className="flex justify-center">
34-
<label>
35-
<input
36-
type="checkbox"
37-
className="checkbox"
38-
onChange={handleCheckboxClick}
39-
id="select-all"
40-
style={{ width: 18, height: 18 }}
41-
/>
42-
</label>
34+
<Checkbox
35+
name="select-all"
36+
checked={select.length === data?.length}
37+
onChange={handleCheckboxClick}
38+
className="checkbox"
39+
/>
4340
</th>
4441
{columns.map((column) => (
4542
<th
@@ -52,40 +49,31 @@ export default function Table({ select, setSelect, columns, data }: ITable) {
5249
))}
5350
</tr>
5451
</thead>
55-
<tbody className="[&>tr]:border-t [&>tr]:border-dark-800 [&>tr>th]:py-4 [&>tr>td]:p-2">
56-
{data?.length === 0 &&
57-
columns.map((column) => (
58-
<th
59-
style={{ width: column.width }}
60-
key={column.item}
61-
className={`${column.sortable ? "max-w-20 truncate pr-10" : ""
62-
}`}
63-
></th>
64-
))}
52+
<tbody className="[&>tr]:border-t [&>tr]:border-dark-800 [&>tr>td]:py-4 bg-dark-300">
6553
{data?.map((item, id) => (
6654
<tr
6755
key={id}
68-
className="hover:bg-dark-300 [&>td:last-child]:!rounded-b-lg"
56+
className="hover:bg-dark-200 [&>td:last-child]:!rounded-b-lg"
6957
>
70-
<th className="min-w-20 md:min-w-10 w-10">
71-
<label>
72-
<input
73-
type="checkbox"
74-
className="checkbox"
75-
id={String(id)}
76-
checked={select.includes(String(id))}
77-
onChange={handleCheckboxClick}
78-
style={{ width: 18, height: 18 }}
79-
/>
80-
</label>
81-
</th>
58+
<td className="w-1 *:flex *:items-center *:!justify-center px-4">
59+
<Checkbox
60+
name={String(id)}
61+
className="checkbox"
62+
checked={select.includes(String(id))}
63+
onChange={handleCheckboxClick}
64+
/>
65+
</td>
8266
{columns.map((column) => (
8367
<td
8468
key={column.item}
85-
className={`${column.sortable ? "max-w-20 truncate pr-10" : ""
86-
} text-[13px] sm:text-sm`}
69+
style={{ width: column.width }}
70+
className={` ${
71+
column.sortable ? "w-20 truncate !pr-10" : ""
72+
} text-[13px] sm:text-sm`}
8773
>
88-
{column.render ? column.render(item) : item[column.item]}
74+
{column.render
75+
? column.render(item)
76+
: shortText(item[column.item], 30)}
8977
</td>
9078
))}
9179
</tr>
@@ -96,7 +84,7 @@ export default function Table({ select, setSelect, columns, data }: ITable) {
9684
<div className="flex flex-col gap-4 items-center justify-center *:text-dark-900 py-16">
9785
<TbTableMinus className="text-5xl " />
9886
<p className="text-center font-semibold text-xl">
99-
{"Hmm, looks like there's no data to show."}
87+
{"Hmm, looks like there's no data to show."}
10088
</p>
10189
</div>
10290
)}

components/ui/form/Checkbox.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default function Checkbox({
1010
name,
1111
checked,
1212
isDisabled,
13+
id,
1314
labelSide = "left",
1415
variant = "solid",
1516
size = "md",
@@ -23,11 +24,12 @@ export default function Checkbox({
2324

2425
return (
2526
<div
26-
className={cn(`${styles.checkbox}`, {
27+
className={cn(` ${styles.checkbox}`, {
2728
"cursor-not-allowed": isDisabled,
2829
})}
2930
onClick={handleAction}
30-
role="button"
31+
role="checkbox"
32+
id={id}
3133
>
3234
{label && labelSide === "left" && <label>{label}</label>}
3335

hooks/index.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useEffect } from "react";
3+
import { useEffect, useRef } from "react";
44

55
export function useClickOutside(
66
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
@@ -66,3 +66,70 @@ export function useScrollX() {
6666
}
6767
}, []);
6868
}
69+
70+
export function useScrollableSlider() {
71+
const sliderEl = useRef<HTMLDivElement>(null!);
72+
const sliderPrevBtn = useRef<HTMLButtonElement>(null!);
73+
const sliderNextBtn = useRef<HTMLButtonElement>(null!);
74+
function scrollToTheRight() {
75+
let offsetWidth = sliderEl.current.offsetWidth;
76+
sliderEl.current.scrollLeft += offsetWidth / 2;
77+
sliderPrevBtn.current.classList.remove("opacity-0", "invisible");
78+
}
79+
function scrollToTheLeft() {
80+
let offsetWidth = sliderEl.current.offsetWidth;
81+
sliderEl.current.scrollLeft -= offsetWidth / 2;
82+
sliderNextBtn.current.classList.remove("opacity-0", "invisible");
83+
}
84+
useEffect(() => {
85+
const filterBarEl = sliderEl.current;
86+
const prevBtn = sliderPrevBtn.current;
87+
const nextBtn = sliderNextBtn.current;
88+
initNextBtnVisibility();
89+
// @ts-ignore
90+
function initNextBtnVisibility() {
91+
let offsetWidth = filterBarEl.offsetWidth;
92+
let scrollWidth = filterBarEl.scrollWidth;
93+
// show next btn when scrollWidth is gather than offsetWidth
94+
if (scrollWidth > offsetWidth) {
95+
nextBtn?.classList.remove("opacity-0", "invisible");
96+
} else {
97+
nextBtn?.classList.add("opacity-0", "invisible");
98+
}
99+
}
100+
function visibleNextAndPrevBtnOnScroll() {
101+
let newScrollLeft = filterBarEl.scrollLeft,
102+
offsetWidth = filterBarEl.offsetWidth,
103+
scrollWidth = filterBarEl.scrollWidth;
104+
// reach to the right end
105+
if (scrollWidth - newScrollLeft == offsetWidth) {
106+
nextBtn?.classList.add("opacity-0", "invisible");
107+
prevBtn?.classList.remove("opacity-0", "invisible");
108+
} else {
109+
nextBtn?.classList.remove("opacity-0", "invisible");
110+
}
111+
// reach to the left end
112+
if (newScrollLeft === 0) {
113+
prevBtn?.classList.add("opacity-0", "invisible");
114+
nextBtn?.classList.remove("opacity-0", "invisible");
115+
} else {
116+
prevBtn?.classList.remove("opacity-0", "invisible");
117+
}
118+
}
119+
window.addEventListener("resize", initNextBtnVisibility);
120+
filterBarEl.addEventListener("scroll", visibleNextAndPrevBtnOnScroll);
121+
// clear event
122+
return () => {
123+
window.removeEventListener("resize", initNextBtnVisibility);
124+
filterBarEl.removeEventListener("scroll", visibleNextAndPrevBtnOnScroll);
125+
};
126+
}, []);
127+
128+
return {
129+
sliderEl,
130+
sliderPrevBtn,
131+
sliderNextBtn,
132+
scrollToTheRight,
133+
scrollToTheLeft,
134+
};
135+
}

0 commit comments

Comments
 (0)