Skip to content

Commit 431247d

Browse files
authored
Merge pull request #81 from SynclyProject/feat/80
Feat/80 스켈레톤 UI
2 parents bebd132 + 4e5cb84 commit 431247d

10 files changed

Lines changed: 343 additions & 0 deletions

File tree

Syncly/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { WorkSpaceProvider } from "./context/workSpaceContext";
2323
import OAuthSuccessPage from "./shared/api/common/OAuthSuccessPage";
2424
import { LiveKitProvider } from "./context/LiveKitContext";
2525

26+
2627
const queryClient = new QueryClient();
2728

2829
const AuthRoute = ({ children }: PropsWithChildren) => {

Syncly/src/pages/My/MyFilesPage.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,28 @@ import { useState } from "react";
66
import useDebounce from "../../hooks/useDebounce";
77
import TrashFileList from "../../components/Files/TrashFileList";
88
import { FileProvider } from "../../context/FileContext";
9+
import { useEffect } from "react";
10+
import MyFileSkeleton from "../../shared/ui/Skeleton/MyFileSkeleton";
11+
12+
913

1014
const MyFilesPage = () => {
1115
const [showInput, setShowInput] = useState(false);
1216
const [sort, setSort] = useState(false);
1317
const [trash, setTrash] = useState(false);
1418
const [mq, setMq] = useState("");
1519
const useDebouncedValue = useDebounce(mq, 500);
20+
const [isLoading, setIsLoading] = useState(true);
21+
22+
useEffect(() => {
23+
const timer = setTimeout(() => setIsLoading(false), 1000);
24+
return () => clearTimeout(timer);
25+
}, []);
26+
27+
if (isLoading) {
28+
return <MyFileSkeleton />;
29+
}
30+
1631
return (
1732
<FileProvider>
1833
<div className="w-full mx-[74px] flex flex-col items-center gap-5">

Syncly/src/pages/My/MyURLsPage.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,23 @@ import Navigate from "../../components/Navigate";
22
import URLsList from "../../components/URLs/URLsList";
33
import Button from "../../shared/ui/Button";
44
import { useState } from "react";
5+
import MyUrlSkeleton from "../../shared/ui/Skeleton/MyUrlSkeleton";
6+
import { useEffect } from "react";
57

68
const MyURLsPage = () => {
79
const [showInput, setShowInput] = useState(false);
10+
const [isLoading, setIsLoading] = useState(true);
11+
12+
useEffect(() => {
13+
const timer = setTimeout(() => setIsLoading(false), 1000);
14+
return () => clearTimeout(timer);
15+
}, []);
16+
17+
18+
if (isLoading) {
19+
return <MyUrlSkeleton />;
20+
}
21+
822
return (
923
<div className="w-full mx-[74px] flex flex-col items-center gap-5">
1024
<div className="w-full flex justify-between mt-5">

Syncly/src/pages/Team/TeamFilesPage.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ import useDebounce from "../../hooks/useDebounce";
66
import TrashFileList from "../../components/Files/TrashFileList";
77
import TeamNavigate from "../../components/TeamNavigate";
88
import { FileProvider } from "../../context/FileContext";
9+
import { useEffect } from "react";
10+
import TeamFileSkeleton from "../../shared/ui/Skeleton/TeamFileSkeleton";
911

1012
const TeamFilesPage = () => {
1113
const [showInput, setShowInput] = useState(false);
1214
const [sort, setSort] = useState(false);
1315
const [trash, setTrash] = useState(false);
1416
const [mq, setMq] = useState("");
1517
const useDebouncedValue = useDebounce(mq, 500);
18+
const [isLoading, setIsLoading] = useState(true);
19+
20+
useEffect(() => {
21+
const timer = setTimeout(() => setIsLoading(false), 1000);
22+
return () => clearTimeout(timer);
23+
}, []);
24+
25+
if (isLoading) {
26+
return <TeamFileSkeleton />;
27+
}
28+
29+
1630
return (
1731
<FileProvider>
1832
<div className="w-full mx-[74px] flex flex-col items-center gap-5">

Syncly/src/pages/Team/TeamURLsPage.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,22 @@ import TeamNavigate from "../../components/TeamNavigate";
22
import URLsListWithWebSocket from "../../components/URLs/URLsListWithWebSocket";
33
import Button from "../../shared/ui/Button";
44
import { useState } from "react";
5+
import { useEffect } from "react";
6+
import TeamUrlSkeleton from "../../shared/ui/Skeleton/TeamUrlSkeleton";
57

68
const TeamURLsPage = () => {
79
const [showInput, setShowInput] = useState(false);
10+
const [isLoading, setIsLoading] = useState(true);
11+
12+
useEffect(() => {
13+
const timer = setTimeout(() => setIsLoading(false), 1000);
14+
return () => clearTimeout(timer);
15+
}, []);
16+
17+
if (isLoading) {
18+
return <TeamUrlSkeleton />;
19+
}
20+
821
return (
922
<div className="w-full mx-[74px] flex flex-col items-center gap-5">
1023
<div className="w-full flex justify-between mt-5">
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// src/shared/ui/Skeleton/MyFilesSkeleton.tsx
2+
import Skeleton from "./Skeleton";
3+
4+
5+
export default function MyFilesSkeleton() {
6+
return (
7+
<div className="w-full mx-[74px] flex flex-col items-center gap-5">
8+
{/* 상단 탭 + 버튼 */}
9+
<div className="w-full flex justify-between mt-5">
10+
{/* url, file 선택 버튼 */}
11+
<div className="flex items-center gap-0 mb-5">
12+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
13+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
14+
</div>
15+
</div>
16+
17+
<div className="w-full flex flex-col gap-5">
18+
{/* 맨 위 + 버튼 */}
19+
<div className="w-full flex justify-end"><Skeleton width="w-12" height="h-10" rounded="rounded-lg" /></div>
20+
21+
22+
</div>
23+
24+
{/* Search / Filter / Delete */}
25+
<div className="w-full flex items-center gap-3">
26+
<Skeleton width="w-59" height="h-10" rounded="rounded-md" /> {/* Search files... */}
27+
<Skeleton width="w-23" height="h-10" rounded="rounded-md" /> {/* Filter */}
28+
<Skeleton width="w-11.5" height="h-10" rounded="rounded-md" /> {/* Delete */}
29+
</div>
30+
31+
32+
{/* 파일 영역 */}
33+
<div className="w-full bg-white rounded-lg border border-neutral-200 p-4">
34+
<div className="flex items-center px-3 h-8 gap-3">
35+
{/* Type */}
36+
<div className="w-14">
37+
<Skeleton width="w-10" height="h-4" rounded="rounded-sm" />
38+
</div>
39+
40+
{/* Title (가장 넓음) */}
41+
<div className="flex-1 min-w-0">
42+
<Skeleton width="w-40" height="h-4" rounded="rounded-sm" />
43+
</div>
44+
45+
{/* Date (우측 정렬) */}
46+
<div className="w-24 ml-auto text-right">
47+
<Skeleton width="w-16" height="h-4" rounded="rounded-sm" />
48+
</div>
49+
50+
{/* User */}
51+
<div className="w-24 text-center">
52+
<Skeleton width="w-14" height="h-4" rounded="rounded-sm" />
53+
</div>
54+
55+
56+
</div>
57+
<div className="border-t border-neutral-200" />
58+
59+
{/* 파일 부분 */}
60+
<div className="p-4">
61+
<Skeleton
62+
width="w-full"
63+
height="h-[260px]" // 필요에 따라 220~320px로 조절
64+
rounded="rounded-md"
65+
/>
66+
</div>
67+
68+
69+
</div>
70+
71+
72+
73+
</div>
74+
);
75+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import Skeleton from "./Skeleton";
2+
3+
export default function MyUrlSkeleton() {
4+
return (
5+
<div className="w-full mx-[74px] flex flex-col items-center gap-5">
6+
{/* 상단 탭 + 버튼 */}
7+
<div className="w-full flex justify-between mt-5">
8+
{/* url, file 선택 버튼 */}
9+
<div className="flex items-center gap-0">
10+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
11+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
12+
13+
</div>
14+
{/* 맨 위 + 버튼 */}
15+
<Skeleton width="w-12" height="h-10" rounded="rounded-lg" />
16+
</div>
17+
18+
{/* URL 그룹 카드 3개 정도 */}
19+
<div className="flex flex-col gap-5 w-full">
20+
{Array.from({ length: 3 }).map((_, i) => (
21+
<div
22+
key={i}
23+
className="w-full bg-white rounded-lg shadow-md border border-neutral-200 p-6"
24+
>
25+
{/* 카드 헤더 */}
26+
<div className="flex justify-between items-center mb-8">
27+
<Skeleton width="w-64" height="h-7" /> {/* 제목 */}
28+
<div className="flex gap-4">
29+
<Skeleton width="w-10" height="h-10" rounded="rounded-md" /> {/* + */}
30+
<Skeleton width="w-24" height="h-10" rounded="rounded-md" /> {/* Save Tabs */}
31+
<Skeleton width="w-24" height="h-10" rounded="rounded-md" /> {/* Open Links */}
32+
</div>
33+
</div>
34+
35+
{/* Source 텍스트 */}
36+
<Skeleton width="w-32" height="h-5" className="mb-6" />
37+
38+
{/* URL 리스트 (2~3개) */}
39+
<div className="flex flex-col gap-5">
40+
{Array.from({ length: 2 }).map((_, j) => (
41+
<div
42+
key={j}
43+
className="flex justify-between items-center border-t border-neutral-200 pt-3"
44+
>
45+
<div className="flex items-center gap-3 w-full">
46+
<Skeleton width="w-5" height="h-5" rounded="rounded-full" />
47+
<Skeleton width="w-3/4" height="h-5" />
48+
</div>
49+
<Skeleton width="w-5" height="h-5" rounded="rounded-md" />
50+
</div>
51+
))}
52+
</div>
53+
</div>
54+
))}
55+
</div>
56+
</div>
57+
);
58+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
interface SkeletonProps {
3+
width?: string;
4+
height?: string;
5+
rounded?: string;
6+
className?: string;
7+
}
8+
9+
export default function Skeleton({
10+
width = "w-full",
11+
height = "h-4",
12+
rounded = "rounded-md",
13+
className = "",
14+
}: SkeletonProps) {
15+
return (
16+
<div
17+
className={`bg-#DEE4ED dark:bg-[#abb9ce] ${width} ${height} ${rounded} animate-pulse ${className}`}
18+
/>
19+
);
20+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Skeleton from "./Skeleton";
2+
3+
4+
export default function MyFilesSkeleton() {
5+
return (
6+
<div className="w-full mx-[74px] flex flex-col items-center gap-5">
7+
{/* 상단 탭 + 버튼 */}
8+
<div className="w-full flex justify-between mt-5">
9+
{/* url, file 선택 버튼 */}
10+
<div className="flex items-center gap-0 mb-5">
11+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
12+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
13+
<Skeleton width="w-44" height="h-8" rounded="rounded-md" />
14+
</div>
15+
</div>
16+
17+
<div className="w-full flex flex-col gap-5">
18+
{/* 맨 위 + 버튼 */}
19+
<div className="w-full flex justify-end"><Skeleton width="w-12" height="h-10" rounded="rounded-lg" /></div>
20+
21+
22+
</div>
23+
24+
{/* Search / Filter / Delete */}
25+
<div className="w-full flex items-center gap-3">
26+
<Skeleton width="w-59" height="h-10" rounded="rounded-md" /> {/* Search files... */}
27+
<Skeleton width="w-23" height="h-10" rounded="rounded-md" /> {/* Filter */}
28+
<Skeleton width="w-11.5" height="h-10" rounded="rounded-md" /> {/* Delete */}
29+
</div>
30+
31+
32+
{/* 파일 영역 */}
33+
<div className="w-full bg-white rounded-lg border border-neutral-200 p-4">
34+
<div className="flex items-center px-3 h-8 gap-3">
35+
{/* Type */}
36+
<div className="w-14">
37+
<Skeleton width="w-10" height="h-4" rounded="rounded-sm" />
38+
</div>
39+
40+
{/* Title (가장 넓음) */}
41+
<div className="flex-1 min-w-0">
42+
<Skeleton width="w-40" height="h-4" rounded="rounded-sm" />
43+
</div>
44+
45+
{/* Date (우측 정렬) */}
46+
<div className="w-24 ml-auto text-right">
47+
<Skeleton width="w-16" height="h-4" rounded="rounded-sm" />
48+
</div>
49+
50+
{/* User */}
51+
<div className="w-24 text-center">
52+
<Skeleton width="w-14" height="h-4" rounded="rounded-sm" />
53+
</div>
54+
55+
56+
</div>
57+
<div className="border-t border-neutral-200" />
58+
59+
{/* 파일 부분 */}
60+
<div className="p-4">
61+
<Skeleton
62+
width="w-full"
63+
height="h-[260px]" // 필요에 따라 220~320px로 조절
64+
rounded="rounded-md"
65+
/>
66+
</div>
67+
68+
69+
</div>
70+
71+
72+
73+
</div>
74+
);
75+
}

0 commit comments

Comments
 (0)