style(ui): review toggle + stats layout + exam spacing + sidebar fix — 7I.3
This commit is contained in:
@@ -5,6 +5,7 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AxiosError } from 'axios';
|
||||
import styled, { css } from 'styled-components';
|
||||
import AppShell from '@/components/layout/AppShell';
|
||||
import { Icon } from '@/components/ui/Icon';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { Button, KbdHint } from '@/components/ui/primitives';
|
||||
@@ -44,6 +45,7 @@ const KEYBOARD_HINTS = [
|
||||
{ keys: ['S'], label: '스킵' },
|
||||
{ keys: ['←'], label: '이전' },
|
||||
] as const;
|
||||
const FOCUS_MODE_STORAGE_KEY = 'reloop-review-focus-mode';
|
||||
|
||||
export default function ReviewPage() {
|
||||
const router = useRouter();
|
||||
@@ -61,6 +63,20 @@ export default function ReviewPage() {
|
||||
const [dashboardSummary, setDashboardSummary] = useState<DashboardSummary | null>(null);
|
||||
const [activityLogs, setActivityLogs] = useState<StudyLog[]>([]);
|
||||
const [detailCache, setDetailCache] = useState<Record<number, StudyLogDetail>>({});
|
||||
const [focusMode, setFocusMode] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const storedValue = window.localStorage.getItem(FOCUS_MODE_STORAGE_KEY);
|
||||
if (storedValue !== null) {
|
||||
setFocusMode(storedValue === 'true');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(FOCUS_MODE_STORAGE_KEY, String(focusMode));
|
||||
}, [focusMode]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!hasToken()) {
|
||||
@@ -331,17 +347,22 @@ export default function ReviewPage() {
|
||||
router.push('/dashboard');
|
||||
}, [router]);
|
||||
|
||||
const wrapLayout = useCallback(
|
||||
(content: React.ReactNode) => (focusMode ? content : <AppShell>{content}</AppShell>),
|
||||
[focusMode],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<FullscreenState>
|
||||
return wrapLayout(
|
||||
<FullscreenState $embedded={!focusMode}>
|
||||
<StateCopy>오늘 다시 잡아둘 문제를 불러오는 중...</StateCopy>
|
||||
</FullscreenState>
|
||||
</FullscreenState>,
|
||||
);
|
||||
}
|
||||
|
||||
if (error || queue === null) {
|
||||
return (
|
||||
<FullscreenState>
|
||||
return wrapLayout(
|
||||
<FullscreenState $embedded={!focusMode}>
|
||||
<StateCard>
|
||||
<StateTitle>지금은 꺼내올 복습이 없어.</StateTitle>
|
||||
<StateSub>
|
||||
@@ -358,14 +379,14 @@ export default function ReviewPage() {
|
||||
</Link>
|
||||
</StateActions>
|
||||
</StateCard>
|
||||
</FullscreenState>
|
||||
</FullscreenState>,
|
||||
);
|
||||
}
|
||||
|
||||
if (queue.length === 0) {
|
||||
return (
|
||||
<Page>
|
||||
<AmbientGlow />
|
||||
return wrapLayout(
|
||||
<Page $focusMode={focusMode}>
|
||||
<AmbientGlow $visible={focusMode} />
|
||||
<FocusHeader>
|
||||
<HeaderButton type="button" onClick={goBack} aria-label="이전으로">
|
||||
<Icon name="caret-left" size={18} weight="bold" />
|
||||
@@ -376,7 +397,14 @@ export default function ReviewPage() {
|
||||
</LogoBadge>
|
||||
<LogoText>ReLoop</LogoText>
|
||||
</BrandMark>
|
||||
<FocusBadge>집중 모드</FocusBadge>
|
||||
<FocusToggleButton
|
||||
type="button"
|
||||
$active={focusMode}
|
||||
onClick={() => setFocusMode((prev) => !prev)}
|
||||
>
|
||||
<Icon name="target" size={16} weight={focusMode ? 'fill' : 'regular'} />
|
||||
집중 모드: {focusMode ? 'ON' : 'OFF'}
|
||||
</FocusToggleButton>
|
||||
</FocusHeader>
|
||||
|
||||
<Content>
|
||||
@@ -418,13 +446,13 @@ export default function ReviewPage() {
|
||||
</CompletionActions>
|
||||
</CompletionCard>
|
||||
</Content>
|
||||
</Page>
|
||||
</Page>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<AmbientGlow />
|
||||
return wrapLayout(
|
||||
<Page $focusMode={focusMode}>
|
||||
<AmbientGlow $visible={focusMode} />
|
||||
|
||||
<FocusHeader>
|
||||
<HeaderButton type="button" onClick={goBack} aria-label="이전으로">
|
||||
@@ -438,7 +466,14 @@ export default function ReviewPage() {
|
||||
<LogoText>ReLoop</LogoText>
|
||||
</BrandMark>
|
||||
|
||||
<FocusBadge>집중 모드</FocusBadge>
|
||||
<FocusToggleButton
|
||||
type="button"
|
||||
$active={focusMode}
|
||||
onClick={() => setFocusMode((prev) => !prev)}
|
||||
>
|
||||
<Icon name="target" size={16} weight={focusMode ? 'fill' : 'regular'} />
|
||||
집중 모드: {focusMode ? 'ON' : 'OFF'}
|
||||
</FocusToggleButton>
|
||||
</FocusHeader>
|
||||
|
||||
<Content>
|
||||
@@ -660,7 +695,7 @@ export default function ReviewPage() {
|
||||
</ActionGrid>
|
||||
)}
|
||||
</Content>
|
||||
</Page>
|
||||
</Page>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -707,18 +742,21 @@ function normalizeOptionalText(value: string | null | undefined): string | null
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
const Page = styled.div`
|
||||
min-height: 100vh;
|
||||
const Page = styled.div<{ $focusMode: boolean }>`
|
||||
min-height: ${({ $focusMode }) => ($focusMode ? '100vh' : 'calc(100vh - 32px)')};
|
||||
background: ${theme.color.bgDeep};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: ${({ $focusMode }) => ($focusMode ? '0' : theme.radius.lg)};
|
||||
`;
|
||||
|
||||
const AmbientGlow = styled.div`
|
||||
const AmbientGlow = styled.div<{ $visible: boolean }>`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: ${({ $visible }) => ($visible ? 1 : 0)};
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
@@ -842,20 +880,25 @@ const LogoText = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
const FocusBadge = styled.span`
|
||||
const FocusToggleButton = styled.button<{ $active: boolean }>`
|
||||
justify-self: end;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 7px 14px;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: ${theme.radius.pill};
|
||||
border: 1px solid rgba(79, 70, 229, 0.28);
|
||||
background: rgba(79, 70, 229, 0.14);
|
||||
color: #a5b4fc;
|
||||
border: 1px solid
|
||||
${({ $active }) =>
|
||||
$active ? 'rgba(129, 140, 248, 0.44)' : theme.color.borderSoftAlpha};
|
||||
background: ${({ $active }) =>
|
||||
$active ? 'rgba(79, 70, 229, 0.18)' : 'rgba(255, 255, 255, 0.03)'};
|
||||
color: ${({ $active }) => ($active ? '#a5b4fc' : theme.color.textSub)};
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.mobile}) {
|
||||
padding: 6px 10px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
@@ -1320,8 +1363,8 @@ const ActionShortcut = styled.span`
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const FullscreenState = styled.div`
|
||||
min-height: 100vh;
|
||||
const FullscreenState = styled.div<{ $embedded: boolean }>`
|
||||
min-height: ${({ $embedded }) => ($embedded ? '60vh' : '100vh')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -988,7 +988,7 @@ function resultLabel(result: StudyLog['result']) {
|
||||
const PageWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.space.lg};
|
||||
gap: 24px;
|
||||
`;
|
||||
|
||||
const Header = styled.header`
|
||||
@@ -1064,15 +1064,15 @@ const RangeButton = styled.button<{ $active: boolean }>`
|
||||
|
||||
const Panels = styled.div`
|
||||
display: grid;
|
||||
gap: ${theme.space.lg};
|
||||
gap: 24px;
|
||||
`;
|
||||
|
||||
const BottomGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
|
||||
gap: ${theme.space.lg};
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.desktop}) {
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`;
|
||||
@@ -1082,21 +1082,16 @@ const PanelCard = styled.section`
|
||||
overflow: hidden;
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
border-radius: ${theme.radius.lg};
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(79, 70, 229, 0.12), transparent 34%),
|
||||
${theme.color.surfaceDeep};
|
||||
box-shadow: ${theme.shadow.cardElevated};
|
||||
padding: ${theme.space.lg};
|
||||
background: ${theme.color.surfaceDeep};
|
||||
padding: 20px;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
padding: ${theme.space.md};
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeatmapCard = styled(PanelCard)`
|
||||
width: min(100%, 720px);
|
||||
justify-self: center;
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const PanelHeader = styled.div`
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function ExamPage() {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [panelWidth, setPanelWidth] = useState(DEFAULT_PANEL_WIDTH);
|
||||
const [isDraggingPanel, setIsDraggingPanel] = useState(false);
|
||||
const [passageCollapsed, setPassageCollapsed] = useState(false);
|
||||
|
||||
const examStartedAtRef = useRef<number>(0);
|
||||
const problemEnteredAtRef = useRef<number>(0);
|
||||
@@ -123,6 +124,7 @@ export default function ExamPage() {
|
||||
setMenuOpen(false);
|
||||
setJumpOpen(false);
|
||||
setProblemModalOpen(false);
|
||||
setPassageCollapsed(false);
|
||||
} catch {
|
||||
setError('문제집을 불러오지 못했어. 네트워크 상태를 확인해줘.');
|
||||
} finally {
|
||||
@@ -427,7 +429,7 @@ export default function ExamPage() {
|
||||
const answeredCurrent = answers[currentProblem.id] !== undefined && answers[currentProblem.id] !== null;
|
||||
|
||||
const examContent = (
|
||||
<Page ref={pageRef}>
|
||||
<Page ref={pageRef} $focusMode={focusMode}>
|
||||
<TopBar>
|
||||
<BackButton type="button" onClick={() => setShowLeaveConfirm(true)}>
|
||||
<Icon name="arrow-left" size={18} />
|
||||
@@ -506,17 +508,31 @@ export default function ExamPage() {
|
||||
{passage && (
|
||||
<PassageCard>
|
||||
<PassageHeader>
|
||||
<span>
|
||||
공통 지문 [{passage.startNumber}~{passage.endNumber}]
|
||||
</span>
|
||||
{isMobile && (
|
||||
<PassageHint>
|
||||
<Icon name="arrows-clockwise" size={14} />
|
||||
탭해서 확대
|
||||
</PassageHint>
|
||||
)}
|
||||
<PassageHeaderRow>
|
||||
<span>
|
||||
공통 지문 [{passage.startNumber}~{passage.endNumber}]
|
||||
</span>
|
||||
{isMobile && (
|
||||
<PassageHint>
|
||||
<Icon name="arrows-clockwise" size={14} />
|
||||
탭해서 확대
|
||||
</PassageHint>
|
||||
)}
|
||||
</PassageHeaderRow>
|
||||
<PassageToggle
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setPassageCollapsed((prev) => !prev);
|
||||
}}
|
||||
>
|
||||
<PassageToggleChevron $collapsed={passageCollapsed}>
|
||||
<Icon name="caret-down" size={14} weight="bold" />
|
||||
</PassageToggleChevron>
|
||||
지문 {passageCollapsed ? '펼치기' : '접기'}
|
||||
</PassageToggle>
|
||||
</PassageHeader>
|
||||
<PassageBody>{passage.bodyText}</PassageBody>
|
||||
{!passageCollapsed && <PassageBody>{passage.bodyText}</PassageBody>}
|
||||
</PassageCard>
|
||||
)}
|
||||
|
||||
@@ -531,36 +547,38 @@ export default function ExamPage() {
|
||||
)}
|
||||
</ProblemMetaRow>
|
||||
<ProblemTitle>{currentProblem.title}</ProblemTitle>
|
||||
{currentProblemImageUrl ? (
|
||||
<>
|
||||
<ProblemImageButton
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openProblemModal();
|
||||
}}
|
||||
aria-label={`${problemSet.subjectName} ${currentProblem.number}번 이미지 크게 보기`}
|
||||
>
|
||||
<ProblemImage
|
||||
src={currentProblemImageUrl}
|
||||
alt={`${problemSet.subjectName} ${currentProblem.number}번`}
|
||||
/>
|
||||
{isMobile && (
|
||||
<ProblemImageHint>
|
||||
<Icon name="export" size={14} />
|
||||
탭해서 확대
|
||||
</ProblemImageHint>
|
||||
<ProblemContentScroll>
|
||||
{currentProblemImageUrl ? (
|
||||
<>
|
||||
<ProblemImageButton
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openProblemModal();
|
||||
}}
|
||||
aria-label={`${problemSet.subjectName} ${currentProblem.number}번 이미지 크게 보기`}
|
||||
>
|
||||
<ProblemImage
|
||||
src={currentProblemImageUrl}
|
||||
alt={`${problemSet.subjectName} ${currentProblem.number}번`}
|
||||
/>
|
||||
{isMobile && (
|
||||
<ProblemImageHint>
|
||||
<Icon name="export" size={14} />
|
||||
탭해서 확대
|
||||
</ProblemImageHint>
|
||||
)}
|
||||
</ProblemImageButton>
|
||||
{currentProblemBodyText && (
|
||||
<ProblemBody $muted>{currentProblemBodyText}</ProblemBody>
|
||||
)}
|
||||
</ProblemImageButton>
|
||||
{currentProblemBodyText && (
|
||||
<ProblemBody $muted>{currentProblemBodyText}</ProblemBody>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ProblemBody>
|
||||
{currentProblemBodyText ?? '본문 텍스트가 비어 있어.'}
|
||||
</ProblemBody>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ProblemBody>
|
||||
{currentProblemBodyText ?? '본문 텍스트가 비어 있어.'}
|
||||
</ProblemBody>
|
||||
)}
|
||||
</ProblemContentScroll>
|
||||
{currentProblem.topic && <TopicChip>{currentProblem.topic}</TopicChip>}
|
||||
</ProblemTextCard>
|
||||
</ProblemPanel>
|
||||
@@ -737,10 +755,21 @@ export default function ExamPage() {
|
||||
</ReadModalHeader>
|
||||
{passage && (
|
||||
<ReadSection>
|
||||
<ReadSectionTitle>
|
||||
공통 지문 [{passage.startNumber}~{passage.endNumber}]
|
||||
</ReadSectionTitle>
|
||||
<ReadText>{passage.bodyText}</ReadText>
|
||||
<ReadSectionHeader>
|
||||
<ReadSectionTitle>
|
||||
공통 지문 [{passage.startNumber}~{passage.endNumber}]
|
||||
</ReadSectionTitle>
|
||||
<PassageToggle
|
||||
type="button"
|
||||
onClick={() => setPassageCollapsed((prev) => !prev)}
|
||||
>
|
||||
<PassageToggleChevron $collapsed={passageCollapsed}>
|
||||
<Icon name="caret-down" size={14} weight="bold" />
|
||||
</PassageToggleChevron>
|
||||
지문 {passageCollapsed ? '펼치기' : '접기'}
|
||||
</PassageToggle>
|
||||
</ReadSectionHeader>
|
||||
{!passageCollapsed && <ReadText>{passage.bodyText}</ReadText>}
|
||||
</ReadSection>
|
||||
)}
|
||||
<ReadSection>
|
||||
@@ -800,13 +829,14 @@ function clampPanelWidth(width: number): number {
|
||||
return Math.min(PANEL_WIDTH_MAX, Math.max(PANEL_WIDTH_MIN, Math.round(width)));
|
||||
}
|
||||
|
||||
const Page = styled.div`
|
||||
const Page = styled.div<{ $focusMode: boolean }>`
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(79, 70, 229, 0.2), transparent 24%),
|
||||
linear-gradient(180deg, #0d1018 0%, #131724 100%);
|
||||
background: ${({ $focusMode }) =>
|
||||
$focusMode
|
||||
? 'radial-gradient(circle at top left, rgba(79, 70, 229, 0.12), transparent 24%), linear-gradient(180deg, #0d1018 0%, #131724 100%)'
|
||||
: theme.color.bgDeep};
|
||||
color: ${theme.color.textBright};
|
||||
border-radius: 24px;
|
||||
border-radius: ${({ $focusMode }) => ($focusMode ? '24px' : theme.radius.lg)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
@@ -981,21 +1011,20 @@ const Content = styled.div`
|
||||
const Workspace = styled.main`
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, auto) 4px minmax(0, 1fr);
|
||||
gap: 0;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Surface = css`
|
||||
background: ${theme.color.surfaceDeep};
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
border-radius: 24px;
|
||||
box-shadow: ${theme.shadow.cardElevated};
|
||||
border-radius: ${theme.radius.lg};
|
||||
`;
|
||||
|
||||
const ProblemPanel = styled.section<{ $width?: number; $clickable: boolean }>`
|
||||
@@ -1010,14 +1039,15 @@ const ProblemPanel = styled.section<{ $width?: number; $clickable: boolean }>`
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
width: 100%;
|
||||
padding: 18px 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ResizeHandle = styled.button<{ $dragging: boolean }>`
|
||||
align-self: stretch;
|
||||
width: 4px;
|
||||
margin: 18px 12px;
|
||||
margin: 0;
|
||||
justify-self: center;
|
||||
border-radius: ${theme.radius.pill};
|
||||
background: ${({ $dragging }) =>
|
||||
$dragging ? 'rgba(99, 102, 241, 0.55)' : 'rgba(99, 102, 241, 0.18)'};
|
||||
@@ -1030,21 +1060,32 @@ const ResizeHandle = styled.button<{ $dragging: boolean }>`
|
||||
`;
|
||||
|
||||
const PassageCard = styled.div`
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid ${theme.color.borderSoftAlpha};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border-radius: ${theme.radius.lg};
|
||||
border: 1px solid ${theme.color.borderSoftAlpha};
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
`;
|
||||
|
||||
const PassageHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
color: #c7d2fe;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const PassageHeaderRow = styled.div`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const PassageHint = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1053,11 +1094,33 @@ const PassageHint = styled.span`
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
const PassageToggle = styled.button`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
color: ${theme.color.textSub};
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const PassageToggleChevron = styled.span<{ $collapsed: boolean }>`
|
||||
display: inline-flex;
|
||||
transition: transform 0.16s ease;
|
||||
transform: rotate(${({ $collapsed }) => ($collapsed ? '-90deg' : '0deg')});
|
||||
`;
|
||||
|
||||
const PassageBody = styled.div`
|
||||
white-space: pre-wrap;
|
||||
line-height: 2;
|
||||
font-size: 17px;
|
||||
color: ${theme.color.textMain};
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
max-height: 60vh;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProblemTextCard = styled.div`
|
||||
@@ -1067,6 +1130,19 @@ const ProblemTextCard = styled.div`
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const ProblemContentScroll = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
max-height: 60vh;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProblemMetaRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1118,9 +1194,9 @@ const ProblemImage = styled.img`
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.26);
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
`;
|
||||
|
||||
@@ -1160,7 +1236,7 @@ const AnswerPanel = styled.section`
|
||||
padding: 24px;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
padding: 18px 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1223,7 +1299,7 @@ const AnswerStatus = styled.span<{ $answered: boolean }>`
|
||||
const ChoiceList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const ChoiceButton = styled.button<{ $selected: boolean }>`
|
||||
@@ -1234,7 +1310,7 @@ const ChoiceButton = styled.button<{ $selected: boolean }>`
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border-radius: ${theme.radius.lg};
|
||||
border: 1px solid
|
||||
${({ $selected }) =>
|
||||
$selected ? 'rgba(129, 140, 248, 0.44)' : theme.color.borderSoftAlpha};
|
||||
@@ -1481,12 +1557,20 @@ const ReadModalClose = styled.button`
|
||||
|
||||
const ReadSection = styled.section`
|
||||
${Surface};
|
||||
padding: 18px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const ReadSectionTitle = styled.h4`
|
||||
const ReadSectionHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const ReadSectionTitle = styled.h4`
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
`;
|
||||
@@ -1501,12 +1585,11 @@ const ReadText = styled.div<{ $muted?: boolean }>`
|
||||
const ReadImage = styled.img`
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 260px);
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
const StateScreen = styled.div<{ $embedded: boolean }>`
|
||||
|
||||
@@ -65,6 +65,7 @@ const Layout = styled.div`
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
background: ${theme.color.bgDeep};
|
||||
`;
|
||||
|
||||
const Main = styled.main<{ $withNav: boolean }>`
|
||||
@@ -75,6 +76,7 @@ const Main = styled.main<{ $withNav: boolean }>`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
|
||||
@media (max-width: ${theme.breakpoint.tablet}) {
|
||||
padding: 16px;
|
||||
|
||||
@@ -100,77 +100,79 @@ export default function SideNav({ user, reviewCount }: SideNavProps) {
|
||||
|
||||
return (
|
||||
<Aside>
|
||||
<TopSection>
|
||||
<Brand href="/dashboard">
|
||||
<LogoMark>
|
||||
<Icon name="infinity" weight="bold" size={18} color="#ffffff" />
|
||||
</LogoMark>
|
||||
<BrandText>ReLoop</BrandText>
|
||||
</Brand>
|
||||
<SidebarBody>
|
||||
<TopSection>
|
||||
<Brand href="/dashboard">
|
||||
<LogoMark>
|
||||
<Icon name="infinity" weight="bold" size={18} color="#ffffff" />
|
||||
</LogoMark>
|
||||
<BrandText>ReLoop</BrandText>
|
||||
</Brand>
|
||||
|
||||
<NavList>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = item.match(pathname);
|
||||
const shouldShowBadge =
|
||||
item.showBadge && typeof reviewCount === 'number' && reviewCount > 0;
|
||||
<NavList>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = item.match(pathname);
|
||||
const shouldShowBadge =
|
||||
item.showBadge && typeof reviewCount === 'number' && reviewCount > 0;
|
||||
|
||||
return (
|
||||
<NavItem key={item.href} href={item.href} $active={isActive}>
|
||||
<NavLabel>
|
||||
<NavIconWrap $active={isActive}>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
weight="regular"
|
||||
size={18}
|
||||
color={isActive ? theme.color.brandIndigo : 'currentColor'}
|
||||
/>
|
||||
</NavIconWrap>
|
||||
<span>{item.label}</span>
|
||||
</NavLabel>
|
||||
{shouldShowBadge ? <ReviewBadge>{reviewCount}</ReviewBadge> : null}
|
||||
</NavItem>
|
||||
);
|
||||
})}
|
||||
</NavList>
|
||||
</TopSection>
|
||||
return (
|
||||
<NavItem key={item.href} href={item.href} $active={isActive}>
|
||||
<NavLabel>
|
||||
<NavIconWrap $active={isActive}>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
weight="regular"
|
||||
size={18}
|
||||
color={isActive ? theme.color.brandIndigo : 'currentColor'}
|
||||
/>
|
||||
</NavIconWrap>
|
||||
<span>{item.label}</span>
|
||||
</NavLabel>
|
||||
{shouldShowBadge ? <ReviewBadge>{reviewCount}</ReviewBadge> : null}
|
||||
</NavItem>
|
||||
);
|
||||
})}
|
||||
</NavList>
|
||||
</TopSection>
|
||||
|
||||
<UserSection ref={menuRef}>
|
||||
<UserButton type="button" onClick={() => setMenuOpen((prev) => !prev)}>
|
||||
<Avatar>
|
||||
{avatarSrc ? (
|
||||
<AvatarImage src={avatarSrc} alt={`${user.nickname} avatar`} />
|
||||
) : (
|
||||
initial
|
||||
)}
|
||||
</Avatar>
|
||||
<UserInfo>
|
||||
<UserName>{user.nickname}</UserName>
|
||||
<PlanLabel>{SUBSCRIPTION_LABELS[user.subscriptionTier]}</PlanLabel>
|
||||
</UserInfo>
|
||||
<CaretWrap $open={menuOpen}>
|
||||
<Icon name="dots-three-vertical" size={14} weight="fill" />
|
||||
</CaretWrap>
|
||||
</UserButton>
|
||||
<UserSection ref={menuRef}>
|
||||
<UserButton type="button" onClick={() => setMenuOpen((prev) => !prev)}>
|
||||
<Avatar>
|
||||
{avatarSrc ? (
|
||||
<AvatarImage src={avatarSrc} alt={`${user.nickname} avatar`} />
|
||||
) : (
|
||||
initial
|
||||
)}
|
||||
</Avatar>
|
||||
<UserInfo>
|
||||
<UserName>{user.nickname}</UserName>
|
||||
<PlanLabel>{SUBSCRIPTION_LABELS[user.subscriptionTier]}</PlanLabel>
|
||||
</UserInfo>
|
||||
<CaretWrap $open={menuOpen}>
|
||||
<Icon name="dots-three-vertical" size={14} weight="fill" />
|
||||
</CaretWrap>
|
||||
</UserButton>
|
||||
|
||||
{menuOpen ? (
|
||||
<Dropdown>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<DropdownLink
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
<Icon name={item.icon} size={14} color={theme.color.textSub} />
|
||||
{item.label}
|
||||
</DropdownLink>
|
||||
))}
|
||||
<LogoutButton type="button" onClick={handleLogout}>
|
||||
<Icon name="sign-out" size={14} color="currentColor" />
|
||||
로그아웃
|
||||
</LogoutButton>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</UserSection>
|
||||
{menuOpen ? (
|
||||
<Dropdown>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<DropdownLink
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
>
|
||||
<Icon name={item.icon} size={14} color={theme.color.textSub} />
|
||||
{item.label}
|
||||
</DropdownLink>
|
||||
))}
|
||||
<LogoutButton type="button" onClick={handleLogout}>
|
||||
<Icon name="sign-out" size={14} color="currentColor" />
|
||||
로그아웃
|
||||
</LogoutButton>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</UserSection>
|
||||
</SidebarBody>
|
||||
</Aside>
|
||||
);
|
||||
}
|
||||
@@ -181,10 +183,8 @@ const Aside = styled.aside`
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
width: 256px;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid ${theme.color.borderSoftAlpha};
|
||||
background: rgba(15, 15, 20, 0.5);
|
||||
@@ -212,9 +212,19 @@ const Aside = styled.aside`
|
||||
}
|
||||
`;
|
||||
|
||||
const TopSection = styled.div`
|
||||
const SidebarBody = styled.div`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const TopSection = styled.div`
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
`;
|
||||
|
||||
@@ -318,7 +328,7 @@ const ReviewBadge = styled.span`
|
||||
|
||||
const UserSection = styled.div`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid ${theme.color.borderSoftAlpha};
|
||||
padding: 24px;
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user