Working on airport drawer
This commit is contained in:
@@ -45,11 +45,13 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
|||||||
|
|
||||||
// Send confirmation email
|
// Send confirmation email
|
||||||
if let Some(email) = email {
|
if let Some(email) = email {
|
||||||
tokio::spawn(async move {
|
if !email.is_empty() {
|
||||||
if let Err(err) = send_confirm_email(&email, &ip_address).await {
|
tokio::spawn(async move {
|
||||||
log::error!("Failed to send confirmation email: {}", err);
|
if let Err(err) = send_confirm_email(&email, &ip_address).await {
|
||||||
};
|
log::error!("Failed to send confirmation email: {}", err);
|
||||||
});
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponse::Created().json(user_response)
|
HttpResponse::Created().json(user_response)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
force=0
|
force=0
|
||||||
push=0
|
push=0
|
||||||
|
push_all=0
|
||||||
|
|
||||||
API_VERSION=$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$(pwd)"/api/Cargo.toml)
|
API_VERSION=$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$(pwd)"/api/Cargo.toml)
|
||||||
UI_VERSION=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$(pwd)"/ui/package.json)
|
UI_VERSION=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$(pwd)"/ui/package.json)
|
||||||
@@ -17,6 +18,13 @@ for arg in "$@"; do
|
|||||||
push=1
|
push=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-a|--push-all)
|
||||||
|
push_all=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -69,3 +77,14 @@ if echo "$changed_files" | grep -q "^api/"; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Push all tags
|
||||||
|
if [ $push_all -eq 1 ]; then
|
||||||
|
if [ $force -eq 1 ]; then
|
||||||
|
echo "Force-pushing ALL tags to remote"
|
||||||
|
git push -f origin --tags
|
||||||
|
else
|
||||||
|
echo "Pushing ALL tags to remote"
|
||||||
|
git push origin --tags
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box, Button,
|
||||||
Divider,
|
Divider,
|
||||||
Drawer,
|
Drawer,
|
||||||
Group,
|
Group, Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsList,
|
TabsList,
|
||||||
Text,
|
Text,
|
||||||
@@ -15,12 +15,13 @@ import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
|||||||
import { getMarkerColor, Metar } from '@lib/metar.types.ts';
|
import { getMarkerColor, Metar } from '@lib/metar.types.ts';
|
||||||
import { CSSProperties, forwardRef, ReactNode, useEffect, useState } from 'react';
|
import { CSSProperties, forwardRef, ReactNode, useEffect, useState } from 'react';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
import { IconViewfinder } from '@tabler/icons-react';
|
import { IconStar, IconStarFilled, IconViewfinder } from '@tabler/icons-react';
|
||||||
import { RunwayTable } from '@components/AirportDrawer/RunwayTable.tsx';
|
import { RunwayTable } from '@components/AirportDrawer/RunwayTable.tsx';
|
||||||
import { CommunicationTable } from '@components/AirportDrawer/CommunicationTable.tsx';
|
import { CommunicationTable } from '@components/AirportDrawer/CommunicationTable.tsx';
|
||||||
import { useMap } from 'react-leaflet';
|
import { useMap } from 'react-leaflet';
|
||||||
import type { Map as LeafletMap } from 'leaflet';
|
import type { Map as LeafletMap } from 'leaflet';
|
||||||
import { getMetars } from '@lib/metar.ts';
|
import { getMetars } from '@lib/metar.ts';
|
||||||
|
import { useUserContext } from '@components/context/UserContext.tsx';
|
||||||
|
|
||||||
export function AirportDrawer({
|
export function AirportDrawer({
|
||||||
airport,
|
airport,
|
||||||
@@ -29,6 +30,9 @@ export function AirportDrawer({
|
|||||||
airport: Airport | null;
|
airport: Airport | null;
|
||||||
setAirport: (airport: Airport | null) => void;
|
setAirport: (airport: Airport | null) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { user, favorites, toggleFavorite } = useUserContext();
|
||||||
|
const isAdmin = user?.role === 'ADMIN';
|
||||||
|
const isFavorite = airport ? favorites.includes(airport.icao) : false;
|
||||||
const [metar, setMetar] = useState<Metar | undefined>(undefined);
|
const [metar, setMetar] = useState<Metar | undefined>(undefined);
|
||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
@@ -73,6 +77,15 @@ export function AirportDrawer({
|
|||||||
>
|
>
|
||||||
<Drawer.Content>
|
<Drawer.Content>
|
||||||
<Drawer.Header>
|
<Drawer.Header>
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={() => toggleFavorite(airport.icao)}
|
||||||
|
aria-label={isFavorite ? 'Unfavorite airport' : 'Favorite airport'}
|
||||||
|
style={{ padding: 4 }}
|
||||||
|
>
|
||||||
|
{isFavorite
|
||||||
|
? <IconStarFilled size={24} color="#faca15" />
|
||||||
|
: <IconStar size={24} />}
|
||||||
|
</UnstyledButton>
|
||||||
<Drawer.Title>
|
<Drawer.Title>
|
||||||
<Text size={'xl'}>{airport.name}</Text>
|
<Text size={'xl'}>{airport.name}</Text>
|
||||||
</Drawer.Title>
|
</Drawer.Title>
|
||||||
@@ -104,6 +117,7 @@ export function AirportDrawer({
|
|||||||
<TabsList grow>
|
<TabsList grow>
|
||||||
<Tabs.Tab value={'info'}>Info</Tabs.Tab>
|
<Tabs.Tab value={'info'}>Info</Tabs.Tab>
|
||||||
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
|
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
|
||||||
|
{ user && <Tabs.Tab value={'manage'}>Manage</Tabs.Tab> }
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<Tabs.Panel value={'info'}>
|
<Tabs.Panel value={'info'}>
|
||||||
<AirportInfo map={map} airport={airport} />
|
<AirportInfo map={map} airport={airport} />
|
||||||
@@ -111,6 +125,23 @@ export function AirportDrawer({
|
|||||||
<Tabs.Panel value={'weather'}>
|
<Tabs.Panel value={'weather'}>
|
||||||
<WeatherInfo metar={airport.latest_metar} />
|
<WeatherInfo metar={airport.latest_metar} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
{user && (
|
||||||
|
<Tabs.Panel value={'manage'}>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Stack mt="md">
|
||||||
|
<Button onClick={() => {}}>Update METAR</Button>
|
||||||
|
<Button onClick={() => {}}>Edit Airport</Button>
|
||||||
|
<Button color="red" onClick={() => {}}>
|
||||||
|
Delete Airport
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack mt="md">
|
||||||
|
<Button onClick={() => {}}>Request Edit</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Tabs.Panel>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
</Drawer.Body>
|
</Drawer.Body>
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ interface UserContextType {
|
|||||||
user?: User;
|
user?: User;
|
||||||
setUser: (user: User | undefined) => void;
|
setUser: (user: User | undefined) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
favorites: string[];
|
||||||
|
toggleFavorite: (icao: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserContext = createContext<UserContextType>({
|
export const UserContext = createContext<UserContextType>({
|
||||||
user: undefined,
|
user: undefined,
|
||||||
setUser: () => {},
|
setUser: () => {},
|
||||||
loading: true
|
loading: true,
|
||||||
|
favorites: [],
|
||||||
|
toggleFavorite: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useUserContext(): UserContextType {
|
export function useUserContext(): UserContextType {
|
||||||
|
|||||||
@@ -9,8 +9,21 @@ const sessionExpirationName = 'session_expiration';
|
|||||||
|
|
||||||
export function UserProvider({ children }: { children: ReactNode }) {
|
export function UserProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | undefined>(undefined);
|
const [user, setUser] = useState<User | undefined>(undefined);
|
||||||
|
const [favorites, setFavorites] = useState<string[]>(() => {
|
||||||
|
return JSON.parse(localStorage.getItem('favorites') || '[]')
|
||||||
|
})
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const toggleFavorite = (icao: string) => {
|
||||||
|
setFavorites((prev) => {
|
||||||
|
const next = prev.includes(icao)
|
||||||
|
? prev.filter((i) => i !== icao)
|
||||||
|
: [...prev, icao]
|
||||||
|
localStorage.setItem('favorites', JSON.stringify(next))
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sessionExpiration = Cookies.get(sessionExpirationName);
|
const sessionExpiration = Cookies.get(sessionExpirationName);
|
||||||
|
|
||||||
@@ -36,7 +49,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{ user, setUser, loading }}>
|
<UserContext.Provider value={{ user, setUser, loading, favorites, toggleFavorite }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Center style={{ height: '100vh' }}>
|
<Center style={{ height: '100vh' }}>
|
||||||
<Loader size='xl' />
|
<Loader size='xl' />
|
||||||
|
|||||||
Reference in New Issue
Block a user