develop #23
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useAuth } from "../../shared/auth";
|
import { useAuth } from "../../shared/auth";
|
||||||
|
import { useTheme } from "../../shared/hooks";
|
||||||
import { useAisTargetPolling } from "../../features/aisPolling/useAisTargetPolling";
|
import { useAisTargetPolling } from "../../features/aisPolling/useAisTargetPolling";
|
||||||
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
||||||
import { LEGACY_ALARM_KINDS } from "../../features/legacyDashboard/model/types";
|
import { LEGACY_ALARM_KINDS } from "../../features/legacyDashboard/model/types";
|
||||||
@ -58,6 +59,7 @@ function useLegacyIndex(data: LegacyVesselDataset | null) {
|
|||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
const uid = user?.id ?? null;
|
const uid = user?.id ?? null;
|
||||||
|
|
||||||
// ── Data fetching ──
|
// ── Data fetching ──
|
||||||
@ -259,6 +261,8 @@ export function DashboardPage() {
|
|||||||
onLogoClick={onLogoClick}
|
onLogoClick={onLogoClick}
|
||||||
userName={user?.name}
|
userName={user?.name}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
|
theme={theme}
|
||||||
|
onToggleTheme={toggleTheme}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DashboardSidebar
|
<DashboardSidebar
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export { usePersistedState } from './usePersistedState';
|
export { usePersistedState } from './usePersistedState';
|
||||||
|
export { useTheme } from './useTheme';
|
||||||
|
|||||||
47
apps/web/src/shared/hooks/useTheme.ts
Normal file
47
apps/web/src/shared/hooks/useTheme.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'dark' | 'light';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'wing:theme';
|
||||||
|
const DEFAULT_THEME: Theme = 'dark';
|
||||||
|
|
||||||
|
function readTheme(): Theme {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw === 'light' || raw === 'dark') return raw;
|
||||||
|
} catch {
|
||||||
|
// storage unavailable
|
||||||
|
}
|
||||||
|
return DEFAULT_THEME;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme: Theme) {
|
||||||
|
document.documentElement.dataset.theme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(() => {
|
||||||
|
const t = readTheme();
|
||||||
|
applyTheme(t);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyTheme(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const setTheme = useCallback((t: Theme) => {
|
||||||
|
setThemeState(t);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, t);
|
||||||
|
} catch {
|
||||||
|
// quota exceeded or unavailable
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||||
|
}, [theme, setTheme]);
|
||||||
|
|
||||||
|
return { theme, setTheme, toggleTheme } as const;
|
||||||
|
}
|
||||||
@ -11,9 +11,11 @@ type Props = {
|
|||||||
onLogoClick?: () => void;
|
onLogoClick?: () => void;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
|
theme?: "dark" | "light";
|
||||||
|
onToggleTheme?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout }: Props) {
|
export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout, theme, onToggleTheme }: Props) {
|
||||||
const statusColor =
|
const statusColor =
|
||||||
pollingStatus === "ready" ? "#22C55E" : pollingStatus === "loading" ? "#F59E0B" : pollingStatus === "error" ? "#EF4444" : "var(--muted)";
|
pollingStatus === "ready" ? "#22C55E" : pollingStatus === "loading" ? "#F59E0B" : pollingStatus === "error" ? "#EF4444" : "var(--muted)";
|
||||||
return (
|
return (
|
||||||
@ -55,6 +57,15 @@ export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStat
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2.5 whitespace-nowrap text-[10px] font-semibold text-wing-accent">{clock}</div>
|
<div className="ml-2.5 whitespace-nowrap text-[10px] font-semibold text-wing-accent">{clock}</div>
|
||||||
|
{onToggleTheme && (
|
||||||
|
<button
|
||||||
|
className="ml-1 cursor-pointer rounded border border-wing-border bg-transparent px-1.5 py-0.5 text-[9px] text-wing-muted transition-all duration-150 hover:border-wing-accent hover:text-wing-text"
|
||||||
|
onClick={onToggleTheme}
|
||||||
|
title={theme === "dark" ? "라이트 모드로 전환" : "다크 모드로 전환"}
|
||||||
|
>
|
||||||
|
{theme === "dark" ? "Light" : "Dark"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{userName && (
|
{userName && (
|
||||||
<div className="ml-2.5 flex shrink-0 items-center gap-2">
|
<div className="ml-2.5 flex shrink-0 items-center gap-2">
|
||||||
<span className="whitespace-nowrap text-[10px] font-medium text-wing-text">{userName}</span>
|
<span className="whitespace-nowrap text-[10px] font-medium text-wing-text">{userName}</span>
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user