= {
@@ -13,22 +14,24 @@ export function LanguageSwitcher() {
SUPPORTED_LANGUAGES.find((language) => i18n.language.startsWith(language)) ?? 'zh-Hant';
return (
-
+
+
);
}
diff --git a/client/src/components/layout/PrimaryLayout.tsx b/client/src/components/layout/PrimaryLayout.tsx
index 0b899ab..b8d6999 100644
--- a/client/src/components/layout/PrimaryLayout.tsx
+++ b/client/src/components/layout/PrimaryLayout.tsx
@@ -1,5 +1,20 @@
import { useEffect, useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
+import {
+ AppBar,
+ Box,
+ Divider,
+ Drawer,
+ IconButton,
+ Link,
+ Stack,
+ Toolbar,
+ Typography,
+ useMediaQuery
+} from '@mui/material';
+import MenuIcon from '@mui/icons-material/Menu';
+import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
+import { useTheme } from '@mui/material/styles';
import { LanguageSwitcher } from './LanguageSwitcher';
interface PrimaryLayoutProps {
@@ -8,61 +23,99 @@ interface PrimaryLayoutProps {
onSidebarToggle?: (isOpen: boolean) => void;
}
+const SIDEBAR_WIDTH = 320;
+
export function PrimaryLayout({ sidebar, content, onSidebarToggle }: PrimaryLayoutProps) {
const { t } = useTranslation();
- const [isSidebarOpen, setIsSidebarOpen] = useState(true);
+ const theme = useTheme();
+ const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
+ const [isSidebarOpen, setIsSidebarOpen] = useState(isDesktop);
+
+ useEffect(() => {
+ setIsSidebarOpen(isDesktop);
+ }, [isDesktop]);
useEffect(() => {
onSidebarToggle?.(isSidebarOpen);
}, [isSidebarOpen, onSidebarToggle]);
+ const toggleSidebar = () => setIsSidebarOpen((prev) => !prev);
+
+ const toolbarOffset = { xs: '56px', sm: '64px' } as const;
+
return (
-
-
-
-
-
-
-
{t('common.appName')}
-
{t('common.tagline')}
-
-
-
-
-
-
-
-
-
-
+
+ {sidebar}
+
+
+
+ {isDesktop && (
+
+ )}
+
+ {content}
+
+
);
}
diff --git a/client/src/components/layout/VisitSidebar.tsx b/client/src/components/layout/VisitSidebar.tsx
index b95df1c..78d6854 100644
--- a/client/src/components/layout/VisitSidebar.tsx
+++ b/client/src/components/layout/VisitSidebar.tsx
@@ -1,4 +1,6 @@
import { useTranslation } from 'react-i18next';
+import { Button, Card, CardContent, Stack, Typography } from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
import { VisitList } from '../../features/visits/components/VisitList';
import { VisitSummary } from '../../features/visits/components/VisitSummary';
import { useVisitModalStore } from '../../state/useVisitModalStore';
@@ -9,26 +11,33 @@ export function VisitSidebar() {
const openForEdit = useVisitModalStore((state) => state.openForEdit);
return (
-
+
+
+
+
+
+
);
}
diff --git a/client/src/components/overlay/VisitModal.tsx b/client/src/components/overlay/VisitModal.tsx
index 0e17f51..80935b5 100644
--- a/client/src/components/overlay/VisitModal.tsx
+++ b/client/src/components/overlay/VisitModal.tsx
@@ -1,4 +1,12 @@
import { useTranslation } from 'react-i18next';
+import {
+ Dialog,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Typography
+} from '@mui/material';
+import CloseIcon from '@mui/icons-material/Close';
import { useVisitModalStore } from '../../state/useVisitModalStore';
import { VisitForm } from '../../features/visits/components/VisitForm';
@@ -6,29 +14,24 @@ export function VisitModal() {
const { isOpen, close, editingVisit, initialLocation } = useVisitModalStore();
const { t } = useTranslation();
- if (!isOpen) return null;
-
return (
-
-
-
-
-
- {editingVisit ? t('sidebar.drawerTitleEdit') : t('sidebar.drawerTitleCreate')}
-
-
{t('sidebar.subtitle')}
-
-
-
+
-
+
+
);
}
diff --git a/client/src/features/map/TravelMap.tsx b/client/src/features/map/TravelMap.tsx
index 0a6d26d..12f4e27 100644
--- a/client/src/features/map/TravelMap.tsx
+++ b/client/src/features/map/TravelMap.tsx
@@ -8,6 +8,8 @@ import { useVisitsQuery } from '../visits/hooks/useVisitQueries';
import { useTranslation } from 'react-i18next';
import { useResizeMap } from '../visits/hooks/useResizeMap';
import type { Map as LeafletMap } from 'leaflet';
+import { Box, Fab, Tooltip } from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
const INITIAL_POSITION: [number, number] = [20, 0];
const INITIAL_ZOOM = 2;
@@ -43,7 +45,7 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
useResizeMap(mapRef, [sidebarOpen]);
return (
-
+
{
mapRef.current = mapInstance;
@@ -74,14 +76,15 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
))}
-
-
+
+ (onTriggerCreate ? onTriggerCreate() : openForCreate())}
+ >
+
+
+
+
);
}
diff --git a/client/src/features/visits/components/VisitForm.tsx b/client/src/features/visits/components/VisitForm.tsx
index be38f1f..d27e629 100644
--- a/client/src/features/visits/components/VisitForm.tsx
+++ b/client/src/features/visits/components/VisitForm.tsx
@@ -2,6 +2,15 @@ import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
+import {
+ Alert,
+ Box,
+ Button,
+ Grid,
+ Stack,
+ TextField,
+ Typography
+} from '@mui/material';
import type { Visit, VisitCreateInput } from '../types';
import { useCreateVisitMutation, useUpdateVisitMutation } from '../hooks/useVisitQueries';
import { useTranslation } from 'react-i18next';
@@ -171,114 +180,107 @@ export function VisitForm({ editingVisit, onCompleted, onCancel, initialLocation
}
return (
-
+
+
+
+
+
+
);
}
diff --git a/client/src/features/visits/components/VisitList.tsx b/client/src/features/visits/components/VisitList.tsx
index 56e0eb9..abf9d6b 100644
--- a/client/src/features/visits/components/VisitList.tsx
+++ b/client/src/features/visits/components/VisitList.tsx
@@ -1,4 +1,7 @@
import { useTranslation } from 'react-i18next';
+import { Button, Card, CardActions, CardContent, CircularProgress, Stack, Typography } from '@mui/material';
+import EditIcon from '@mui/icons-material/Edit';
+import DeleteIcon from '@mui/icons-material/Delete';
import { useDeleteVisitMutation, useVisitsQuery } from '../hooks/useVisitQueries';
import type { Visit } from '../types';
@@ -12,48 +15,58 @@ export function VisitList({ onEdit }: VisitListProps) {
const { t } = useTranslation();
if (isLoading) {
- return {t('list.loading')}
;
+ return (
+
+
+
+ {t('list.loading')}
+
+
+ );
}
if (!visits.length) {
- return {t('list.empty')}
;
+ return (
+
+ {t('list.empty')}
+
+ );
}
return (
-
+
{visits.map((visit) => (
- -
-
-
-
- {visit.location.country}
- {visit.location.city ? ` · ${visit.location.city}` : null}
-
-
- {visit.date.start}
- {visit.date.end ? ` — ${visit.date.end}` : null}
-
- {visit.notes ?
{visit.notes}
: null}
-
-
-
-
-
-
-
+
+
+
+ {visit.location.country}
+ {visit.location.city ? ` · ${visit.location.city}` : ''}
+
+
+ {visit.date.start}
+ {visit.date.end ? ` — ${visit.date.end}` : ''}
+
+ {visit.notes ? (
+
+ {visit.notes}
+
+ ) : null}
+
+
+ } size="small" onClick={() => onEdit(visit)}>
+ {t('list.edit')}
+
+ }
+ size="small"
+ color="error"
+ onClick={() => deleteMutation.mutate(visit.id)}
+ >
+ {t('list.delete')}
+
+
+
))}
-
+
);
}
diff --git a/client/src/features/visits/components/VisitSummary.tsx b/client/src/features/visits/components/VisitSummary.tsx
index 99779f7..f542982 100644
--- a/client/src/features/visits/components/VisitSummary.tsx
+++ b/client/src/features/visits/components/VisitSummary.tsx
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
+import { Card, CardContent, Grid, Paper, Typography } from '@mui/material';
import { useVisitsQuery } from '../hooks/useVisitQueries';
export function VisitSummary() {
@@ -15,19 +16,32 @@ export function VisitSummary() {
};
}, [visits]);
+ const summaryItems = [
+ { label: t('summary.totalVisits'), value: stats.totalVisits },
+ { label: t('summary.countries'), value: stats.uniqueCountries }
+ ];
+
return (
-
-
{t('summary.title')}
-
-
-
- {t('summary.totalVisits')}
- - {stats.totalVisits}
-
-
-
- {t('summary.countries')}
- - {stats.uniqueCountries}
-
-
-
+
+
+
+ {t('summary.title')}
+
+
+ {summaryItems.map((item) => (
+
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+
+ ))}
+
+
+
);
}
diff --git a/client/src/styles/theme.ts b/client/src/styles/theme.ts
new file mode 100644
index 0000000..5cfab24
--- /dev/null
+++ b/client/src/styles/theme.ts
@@ -0,0 +1,18 @@
+import { createTheme } from '@mui/material/styles';
+
+export const appTheme = createTheme({
+ palette: {
+ primary: {
+ main: '#0b7285'
+ },
+ secondary: {
+ main: '#51cf66'
+ },
+ background: {
+ default: '#f1f5f9'
+ }
+ },
+ shape: {
+ borderRadius: 12
+ }
+});