Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Dialog } from "@/shared/ui";
import { Button, Dialog, SkeletonPanel } from "@/shared/ui";
import { isAttended, type WeeklyAttendanceResponse } from "@/entities/attendance";
import * as S from "./AttendanceDialog.style";

Expand Down Expand Up @@ -48,29 +48,20 @@ export const AttendanceDialog = ({

return (
<Dialog title="출석" width={26} height={33} isOpen={isOpen} onClose={onClose}>
<S.LoadState
role={hasLoadError ? "alert" : "status"}
aria-live={hasLoadError ? "assertive" : "polite"}
>
<S.GiftIcon aria-hidden />
<S.Headline>
{isLoading
? "출석 현황을 불러오는 중이에요."
: hasLoadError
? "출석 현황을 불러오지 못했어요."
: "표시할 출석 현황이 없어요."}
</S.Headline>
<S.Description>
{isLoading
? "잠시만 기다려 주세요."
: loadErrorMessage || "잠시 후 다시 확인해 주세요."}
</S.Description>
{!isLoading && (
{isLoading ? (
<SkeletonPanel ariaLabel="출석 현황을 불러오는 중" />
) : (
<S.LoadState role={hasLoadError ? "alert" : "status"} aria-live="assertive">
<S.GiftIcon aria-hidden />
<S.Headline>
{hasLoadError ? "출석 현황을 불러오지 못했어요." : "표시할 출석 현황이 없어요."}
</S.Headline>
<S.Description>{loadErrorMessage || "잠시 후 다시 확인해 주세요."}</S.Description>
<Button variant="primary" size="md" onClick={onRetry}>
다시 시도
</Button>
)}
</S.LoadState>
</S.LoadState>
)}
</Dialog>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styled, { css, keyframes } from "styled-components";
import styled, { css } from "styled-components";
import { font } from "@clash/design-tokens/font";
import { InlineNotice } from "@clash/ui";
import FirstFrameIcon from "../assets/first-frame.svg";
Expand All @@ -10,25 +10,8 @@ import { DefaultProfileIcon } from "@/shared/ui";
export type RankingPageEnum = "section" | "chapter";
export type RankingPositionEnum = "top" | "bottom";

const skeletonWave = keyframes`
0% {
background-position: 200% 0;
}

100% {
background-position: -200% 0;
}
`;

const skeletonBase = css`
background: linear-gradient(
90deg,
${({ theme }) => theme.fill.normal} 0%,
${({ theme }) => theme.fill.alternative} 50%,
${({ theme }) => theme.fill.normal} 100%
);
background-size: 200% 100%;
animation: ${skeletonWave} 1.6s ease-in-out infinite;
background-color: ${({ theme }) => theme.fill.neutral};
`;

export const RankingContainer = styled.div<{ $page: RankingPageEnum }>`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as S from "./MissionContainer.style";
import { useEffect, useRef, useState } from "react";
import { SidePanel } from "@/shared/ui";
import { SidePanel, SkeletonPanel } from "@/shared/ui";
import { getErrorMessage } from "@/shared/lib";
import { MarkdownCodeContent } from "@/shared/ui/markdown-code-content/MarkdownCodeContent";
import type { Mission } from "@/features/chapter/model/chapter.types";
Expand Down Expand Up @@ -344,9 +344,7 @@ export const MissionContainer = ({
}
};

const descriptionText = isLoading
? "챕터 정보를 불러오는 중입니다."
: description?.trim() || "이 챕터에 대한 설명이 아직 준비되지 않았습니다.";
const descriptionText = description?.trim() || "이 챕터에 대한 설명이 아직 준비되지 않았습니다.";
const isCompletedStage =
isFinishedSection ||
(currentStage.totalMissions > 0 && currentStage.currentProgress >= currentStage.totalMissions);
Expand Down Expand Up @@ -392,9 +390,13 @@ export const MissionContainer = ({
) : (
<>
<S.OverviewBody>
<S.SectionCard>
<S.DescriptionText>{descriptionText}</S.DescriptionText>
</S.SectionCard>
{isLoading ? (
<SkeletonPanel ariaLabel="챕터 정보를 불러오는 중" />
) : (
<S.SectionCard>
<S.DescriptionText>{descriptionText}</S.DescriptionText>
</S.SectionCard>
)}
</S.OverviewBody>

<S.FooterActions>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { useState } from "react";
import * as S from "./Battle.style";
import { Button, DefaultProfileIcon, Dialog, Select, SlideSelector } from "@/shared/ui";
import {
Button,
DefaultProfileIcon,
Dialog,
Select,
SkeletonPanel,
SkeletonRows,
SlideSelector,
} from "@/shared/ui";
import type { AnalyzeCategory } from "@/entities/competition";
import { MATCH_VALUE } from "@/entities/competition";
import { useBattle } from "@/features/competition/model/useBattle";
Expand Down Expand Up @@ -69,11 +77,7 @@ export const Battle = () => {
)}

{battle.queries.info.isPending ? (
<S.DetailWrapper>
<S.DefaultBattleBox role="status" aria-live="polite">
<S.DefaultBattleText>배틀 정보를 불러오는 중이에요.</S.DefaultBattleText>
</S.DefaultBattleBox>
</S.DetailWrapper>
<SkeletonPanel ariaLabel="배틀 정보를 불러오는 중" />
) : battle.queries.info.isError && !battle.battleData ? (
<S.DetailWrapper>
<S.DefaultBattleBox role="alert">
Expand Down Expand Up @@ -139,13 +143,7 @@ export const Battle = () => {

{battle.isBattleSelected ? (
battle.queries.detail.isPending || battle.queries.detail.isPlaceholderData ? (
<S.DetailWrapper>
<S.DefaultBattleBox role="status" aria-live="polite">
<S.DefaultBattleText>
배틀 상세 정보를 불러오는 중이에요.
</S.DefaultBattleText>
</S.DefaultBattleBox>
</S.DetailWrapper>
<SkeletonPanel ariaLabel="배틀 상세 정보를 불러오는 중" />
) : battle.queries.detail.isError && !battle.battleDetailData ? (
<S.DetailWrapper>
<S.DefaultBattleBox role="alert">
Expand Down Expand Up @@ -248,9 +246,7 @@ export const Battle = () => {

{battle.queries.analyze.isPending ||
battle.queries.analyze.isPlaceholderData ? (
<S.AnalyzeState role="status" aria-live="polite">
세부 분석을 불러오는 중이에요.
</S.AnalyzeState>
<SkeletonPanel ariaLabel="배틀 세부 분석을 불러오는 중" compact />
) : battle.queries.analyze.isError &&
battle.queries.analyze.data === undefined ? (
<S.AnalyzeState role="alert">
Expand Down Expand Up @@ -379,9 +375,7 @@ export const Battle = () => {
<S.ModalBody>
<S.UserChoiceContainer aria-busy={battle.queries.rivals.isFetching || undefined}>
{battle.queries.rivals.isPending ? (
<S.EmptyStateBox role="status" aria-live="polite">
<S.EmptyTitle>신청 가능한 라이벌을 불러오는 중이에요.</S.EmptyTitle>
</S.EmptyStateBox>
<SkeletonRows ariaLabel="신청 가능한 라이벌을 불러오는 중" rows={4} compact />
) : battle.queries.rivals.isError && !battle.battleList ? (
<S.EmptyStateBox role="alert">
<S.EmptyTitle>신청 가능한 라이벌을 불러오지 못했어요.</S.EmptyTitle>
Expand Down Expand Up @@ -475,9 +469,7 @@ export const Battle = () => {
aria-busy={battle.queries.applications.isFetching || undefined}
>
{battle.queries.applications.isPending ? (
<S.EmptyStateBox role="status" aria-live="polite">
<S.EmptyTitle>배틀 신청 목록을 불러오는 중이에요.</S.EmptyTitle>
</S.EmptyStateBox>
<SkeletonRows ariaLabel="배틀 신청 목록을 불러오는 중" rows={4} compact />
) : battle.queries.applications.isError && !battle.battleApplyList ? (
<S.EmptyStateBox role="alert">
<S.EmptyTitle>배틀 신청 목록을 불러오지 못했어요.</S.EmptyTitle>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, SlideSelector } from "@/shared/ui";
import { Button, SkeletonCards, SlideSelector } from "@/shared/ui";
import { GROUP_CATEGORY_LABELS } from "@/entities/group";
import type { GroupFormPanelProps } from "../../../model/useGroup";
import { useGroupFormPanel } from "../../../model/useGroupFormPanel";
Expand Down Expand Up @@ -78,9 +78,11 @@ export const GroupFormPanel = ({

<S.GroupsWrapper>
{isLoading ? (
<S.EmptyState>
<S.EmptyTitle>로딩 중...</S.EmptyTitle>
</S.EmptyState>
<SkeletonCards
ariaLabel="참여할 수 있는 그룹을 불러오는 중"
cards={4}
columns={1}
/>
) : groups.length === 0 ? (
<S.EmptyState>
<S.EmptyTextBox>
Expand Down
19 changes: 9 additions & 10 deletions apps/mac/src/renderer/src/features/home/ui/ranking/Ranking.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as S from "./Ranking.style";
import { rankingRewardTooltipContent } from "./Ranking.constants";
import { useRanking } from "@/features/home/model/useRanking";
import { Button, Select, QuestionTooltip } from "@/shared/ui";
import { Button, Select, QuestionTooltip, SkeletonRows } from "@/shared/ui";
import type { CategoryType, PeriodType, RankingItem } from "@/entities/ranking";
import { UserRanking } from "./user-ranking/UserRanking";

Expand Down Expand Up @@ -58,15 +58,14 @@ export const Ranking = () => {
<S.UserWrapper>
<S.UserContainer ref={wrapperRef} aria-busy={domain.isFetching}>
{domain.isLoading || domain.isPlaceholderData ? (
<S.DetailWrapper>
<S.DefaultBattleBox role="status" aria-live="polite">
<S.DefaultBattleText>
{domain.isPlaceholderData
? "선택한 조건의 랭킹을 불러오는 중이에요."
: "랭킹을 불러오는 중이에요."}
</S.DefaultBattleText>
</S.DefaultBattleBox>
</S.DetailWrapper>
<SkeletonRows
ariaLabel={
domain.isPlaceholderData ? "선택한 조건의 랭킹을 불러오는 중" : "랭킹을 불러오는 중"
}
rows={6}
surface
compact
/>
) : domain.isError && !hasRankings ? (
<S.DetailWrapper>
<S.DefaultBattleBox role="alert">
Expand Down
11 changes: 8 additions & 3 deletions apps/mac/src/renderer/src/features/major-choice/ui/test/Test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as S from "./Test.style";
import type { TestProps } from "@/features/major-choice/model/useMajorChoice";
import { Button } from "@/shared/ui/button";
import { SkeletonRows } from "@/shared/ui/skeleton";
import { AGREEMENT_LABELS, LevelSlider } from "@/shared/ui/level-slider";
import type { LevelSliderValue } from "@/shared/ui/level-slider/types";

Expand Down Expand Up @@ -61,9 +62,13 @@ export const Test = ({
</S.StickyHeader>
<S.QuestionWrapper>
{isQuestionsLoading ? (
<S.StateBox role="status" aria-live="polite">
<S.StateTitle>검사 문항을 불러오는 중이에요.</S.StateTitle>
</S.StateBox>
<SkeletonRows
ariaLabel="검사 문항을 불러오는 중"
rows={4}
showAvatar={false}
showTrailing={false}
surface
/>
) : questionsError && questionData.length === 0 ? (
<S.StateBox role="alert">
<S.StateTitle>검사 문항을 불러오지 못했어요.</S.StateTitle>
Expand Down
6 changes: 2 additions & 4 deletions apps/mac/src/renderer/src/features/notice/ui/TopbarNotice.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
import type { KeyboardEvent } from "react";
import { formatNoticeDate, useTopbarNotice } from "@/features/notice/model/useTopbarNotice";
import { Button, Popover } from "@/shared/ui";
import { Button, Popover, SkeletonRows } from "@/shared/ui";
import * as S from "./TopbarNotice.style";
import { getErrorMessage } from "@/shared/lib";

Expand Down Expand Up @@ -157,9 +157,7 @@ export const TopbarNotice = () => {
</S.BackgroundErrorNotice>
)}
{isLoading ? (
<S.NoneNotice role="status" aria-live="polite">
알림을 불러오는 중입니다.
</S.NoneNotice>
<SkeletonRows ariaLabel="알림을 불러오는 중" rows={4} showTrailing compact />
) : isError && !hasQueryData ? (
<S.NoticeState role="alert">
<span>{getErrorMessage(error, "알림을 불러오지 못했어요.")}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GitHubInfo } from "@/features/profile/ui/profile-tabs/github-info/GitHu
import { GitHubStreak } from "@/features/profile/ui/profile-tabs/github-streak/GitHubStreak";
import { useProfileGitHubDetailQuery } from "@/entities/profile";
import { useProfileGitHubStreak } from "@/features/profile/model/useProfileTabs";
import { SkeletonPanel } from "@/shared/ui";
import * as S from "./GitHubActivityPanel.style";

const formatKoreanDate = (ymd: string) => {
Expand Down Expand Up @@ -73,13 +74,17 @@ export const GitHubActivityPanel = () => {
</S.StreakSection>

<S.InfoSection>
<GitHubInfo
{...infoProps}
showSummary={hasSelection}
hasDetail={Boolean(selectedDate && displayDetail)}
emptyTitle={emptyTitle}
emptyDescription={emptyDescription}
/>
{selectedDate && !displayDetail && (detailQuery.isFetching || detailQuery.isLoading) ? (
<SkeletonPanel ariaLabel="선택한 날짜의 GitHub 활동을 불러오는 중" />
) : (
<GitHubInfo
{...infoProps}
showSummary={hasSelection}
hasDetail={Boolean(selectedDate && displayDetail)}
emptyTitle={emptyTitle}
emptyDescription={emptyDescription}
/>
)}
</S.InfoSection>
</S.Panel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { sortEquippedItemsFirst } from "@/features/profile/lib/sortEquippedItemsFirst";
import { useGetMyProfile, userQueryKeys } from "@/entities/user";
import { getErrorMessage } from "@/shared/lib";
import { Button } from "@/shared/ui";
import { Button, SkeletonCards } from "@/shared/ui";

const FILTER_OPTIONS = [
{ key: OwnedItemCategory.ALL, label: "전체" },
Expand Down Expand Up @@ -126,10 +126,7 @@ export const ItemPanel = () => {
</Button>
</S.StateBox>
) : isLoading ? (
<S.StateBox role="status" aria-live="polite" aria-busy="true">
<S.StateTitle>아이템을 불러오는 중...</S.StateTitle>
<S.StateDescription>잠시만 기다려 주세요.</S.StateDescription>
</S.StateBox>
<SkeletonCards ariaLabel="보유한 아이템을 불러오는 중" cards={8} columns={4} />
) : items.length === 0 ? (
<S.StateBox>
<S.StateTitle>보유한 아이템이 아직 없어요.</S.StateTitle>
Expand Down
4 changes: 2 additions & 2 deletions apps/mac/src/renderer/src/features/record/ui/task/Task.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as S from "./Task.style";
import { formatTime, getErrorMessage } from "@/shared/lib";
import { useTaskList } from "../../model/useTaskList";
import { Button, ConfirmDialog, Popover, Tooltip } from "@/shared/ui";
import { Button, ConfirmDialog, Popover, SkeletonRows, Tooltip } from "@/shared/ui";

interface TaskProps {
selectedDate?: string;
Expand Down Expand Up @@ -74,7 +74,7 @@ export const Task = ({ selectedDate }: TaskProps) => {
<S.TaskContainer>
<S.TaskBox aria-busy={subjectsQuery.isFetching || undefined}>
{subjectsQuery.isPending ? (
<S.ListState kind="loading">과목을 불러오는 중이에요.</S.ListState>
<SkeletonRows ariaLabel="과목을 불러오는 중" rows={5} showAvatar={false} compact />
) : hasInitialLoadError ? (
<S.ListState kind="error">
<span>{getErrorMessage(subjectsQuery.error, "과목을 불러오지 못했어요.")}</span>
Expand Down
3 changes: 2 additions & 1 deletion apps/mac/src/renderer/src/features/record/ui/timer/Timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as S from "./Timer.style";
import { formatTime } from "@/shared/lib";
import { useRecordStore } from "../../model/recordStore";
import { useLiveRecordStudyTime } from "../../model/useLiveRecordStudyTime";
import { Skeleton } from "@/shared/ui";

interface TimerProps {
date: string;
Expand Down Expand Up @@ -103,7 +104,7 @@ export const Timer = ({
aria-label={isLoading ? "총 학습 시간을 불러오는 중" : `총 학습 시간 ${displayTime}`}
$loading={isLoading}
>
{displayTime}
{isLoading ? <Skeleton width="8ch" height="2.75rem" radius="0.5rem" /> : displayTime}
</S.Time>
{stopButtonPosition === "RIGHT" && stopButton}
</S.TimeBox>
Expand Down
4 changes: 2 additions & 2 deletions apps/mac/src/renderer/src/features/record/ui/todo/Todo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Popover, Select, Tooltip } from "@/shared/ui";
import { Button, Popover, Select, SkeletonRows, Tooltip } from "@/shared/ui";
import * as S from "./Todo.style";
import { useTodoList } from "../../model/useTodoList";
import { getErrorMessage } from "@/shared/lib";
Expand Down Expand Up @@ -84,7 +84,7 @@ export const Todo = ({ selectedDate }: TodoProps) => {
</S.SourceNotice>
)}
{tasksQuery.isPending ? (
<S.ListState kind="loading">할 일을 불러오는 중이에요.</S.ListState>
<SkeletonRows ariaLabel="할 일을 불러오는 중" rows={5} showAvatar={false} compact />
) : hasInitialTasksError ? (
<S.ListState kind="error">
<span>{getErrorMessage(tasksQuery.error, "할 일을 불러오지 못했어요.")}</span>
Expand Down
Loading
Loading