Auth flow functional
This commit is contained in:
80
astro/src/astroTypes.ts
Normal file
80
astro/src/astroTypes.ts
Normal file
@ -0,0 +1,80 @@
|
||||
export interface User {
|
||||
name: string;
|
||||
id: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface Product extends Node {
|
||||
id: string;
|
||||
name: string;
|
||||
weight?: number;
|
||||
picture: Media;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
// export interface Location = {
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
// }
|
||||
|
||||
export interface Maker extends Node {
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
location: [number, number];
|
||||
stock: Product[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export interface Retailer extends Node {
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
location: [number, number];
|
||||
stock: Product[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const;
|
||||
export type DispatchStatus = typeof DISPATCH_STATUS[number];
|
||||
|
||||
export interface Dispatch {
|
||||
id: string;
|
||||
dispatchesCode?: string; //Human readable id
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
maker: Maker;
|
||||
retailer: Retailer;
|
||||
products: Product[];
|
||||
|
||||
courier?: User;
|
||||
|
||||
timeSensitive: boolean;
|
||||
status: DispatchStatus;
|
||||
|
||||
departureDate: string;
|
||||
arrivalDate: string;
|
||||
weightAllowance: number;
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import { MapContainer, TileLayer, Marker, CircleMarker, Popup, Polyline, LayerGr
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L, { LatLngBounds } from 'leaflet';
|
||||
import Contacts from './Contacts';
|
||||
import type { User, Node, Retailer, Maker, Product, Dispatch, DispatchStatus } from '../astroTypes';
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { useGetMakers, useGetDispatches, useGetRetailers, useGetUser, useGetMyself } from "../utils/hooks"
|
||||
|
||||
import { Button, buttonVariants } from './ui/Button';
|
||||
|
||||
@ -17,156 +18,8 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
|
||||
//Todo: Move types to own file
|
||||
interface User {
|
||||
name: string;
|
||||
id: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
filesize?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
interface Product extends Node {
|
||||
id: string;
|
||||
name: string;
|
||||
weight?: number;
|
||||
picture: Media;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
// interface Location = {
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
// }
|
||||
|
||||
interface Maker extends Node {
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
location: [number, number];
|
||||
stock: Product[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
interface Retailer extends Node {
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
location: [number, number];
|
||||
stock: Product[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const;
|
||||
type DispatchStatus = typeof DISPATCH_STATUS[number];
|
||||
|
||||
interface Dispatch {
|
||||
id: string;
|
||||
dispatchesCode?: string; //Human readable id
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
maker: Maker;
|
||||
retailer: Retailer;
|
||||
products: Product[];
|
||||
|
||||
courier?: User;
|
||||
|
||||
timeSensitive: boolean;
|
||||
status: DispatchStatus;
|
||||
|
||||
departureDate: string;
|
||||
arrivalDate: string;
|
||||
weightAllowance: number;
|
||||
}
|
||||
|
||||
|
||||
//Todo: update fetch url endpoints
|
||||
//Todo: Move queryclient and hooks to own file
|
||||
//Todo: Move axios stuff
|
||||
const API_URL = "http://localhost:3001"
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
const getMakers = async () => {
|
||||
const url = `${API_URL}/api/makers`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const makers: Maker[] = response.data.docs;
|
||||
console.log(`Fetch result from ${url}`, makers)
|
||||
return makers;
|
||||
|
||||
}
|
||||
|
||||
const useGetMakers = () => {
|
||||
return useQuery<Maker[]>({
|
||||
queryFn: () => getMakers(),
|
||||
queryKey: ['makers'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
|
||||
const getRetailers = async () => {
|
||||
const url = `${API_URL}/api/retailers`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const retailers: Retailer[] = response.data.docs;
|
||||
console.log(`Fetch result from ${url}`, retailers)
|
||||
return retailers;
|
||||
|
||||
}
|
||||
|
||||
const useGetRetailers = () => {
|
||||
return useQuery<Retailer[]>({
|
||||
queryFn: () => getRetailers(),
|
||||
queryKey: ['retailers'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
|
||||
const getDispatches = async () => {
|
||||
const url = `${API_URL}/api/dispatches`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const dispatches: Dispatch[] = response.data.docs;
|
||||
|
||||
console.log(`Fetch result from ${url}`, dispatches)
|
||||
return dispatches;
|
||||
|
||||
}
|
||||
|
||||
const useGetDispatches = () => {
|
||||
return useQuery<Dispatch[]>({
|
||||
queryFn: () => getDispatches(),
|
||||
queryKey: ['dispatches'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
import { LoginForm } from './LoginForm';
|
||||
import { hasAuthCookie } from '@/utils/authUtils';
|
||||
|
||||
//Payload longitude and latitude are mislabeled in payload (lol)
|
||||
const locationSwitcharoo = (location: number[]) => {
|
||||
@ -206,10 +59,14 @@ interface NodeSelection {
|
||||
|
||||
export const KiosMap = () => {
|
||||
|
||||
const [authToken, setAuthToken] = useState('')
|
||||
|
||||
const { data: makers, isLoading: isLoadingMakers } = useGetMakers();
|
||||
const { data: retailers, isLoading: isLoadingRetailers } = useGetRetailers();
|
||||
const { data: dispatches, isLoading: isLoadingDispatches } = useGetDispatches();
|
||||
|
||||
const { data: myself, isLoading: isLoadingMyself } = useGetMyself(authToken);
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<NodeSelection>({ id: "", type: "none" })
|
||||
|
||||
let selectedMaker: Maker | undefined = undefined;
|
||||
@ -267,31 +124,64 @@ export const KiosMap = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full flex justify-center align-middle'>
|
||||
{
|
||||
selectedNode.type !== 'none' && (
|
||||
<div className='absolute bg-white border-gray-950 border-2 z-[998] left-10 top-1/5 mt-24 p-4'>
|
||||
<div className='flex gap-8 flex-col'>
|
||||
{selectedMaker !== undefined && (
|
||||
<div className='flex gap-4 flex-col'>
|
||||
<Contacts
|
||||
name={selectedMaker.name}
|
||||
email={selectedMaker.email}
|
||||
phoneNumber={selectedMaker.phoneNumber}
|
||||
role={'maker'}
|
||||
/>
|
||||
{(selectedMaker.stock !== undefined && selectedMaker.stock.length > 0) &&
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className={buttonVariants({ variant: "kios" })}
|
||||
>
|
||||
See catalogue
|
||||
</DialogTrigger>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Dialog>
|
||||
<div className="flex justify-between items-end px-4 gap-4">
|
||||
<img
|
||||
width={120}
|
||||
src="/kios-logo.png"
|
||||
alt="" />
|
||||
|
||||
{(myself && myself.name) &&
|
||||
<p>Logged in as: {myself.name}</p>
|
||||
}
|
||||
{
|
||||
(!hasAuthCookie() && !authToken) &&
|
||||
<DialogTrigger
|
||||
className={`px-14 w-6 ${buttonVariants({ variant: "kios" })}`}
|
||||
>
|
||||
Login
|
||||
</DialogTrigger>
|
||||
}
|
||||
</div>
|
||||
|
||||
<DialogContent className='lg:max-w-screen-lg overflow-y-scroll max-h-screen'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl underline underline-offset-2">{selectedMaker.name}'s stock</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl text-center">Login</DialogTitle>
|
||||
<LoginForm setAuthToken={setAuthToken} authToken={authToken}/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className='w-full flex justify-center align-middle'>
|
||||
<div className=''>
|
||||
<div>
|
||||
<img src="/route-guide.png" width={300} className="absolute right-6 top-3/4 z-[998] border border-black" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
selectedNode.type !== 'none' && (
|
||||
<div className='absolute bg-white border-gray-950 border-2 z-[998] left-10 top-1/5 mt-24 p-4'>
|
||||
<div className='flex gap-8 flex-col'>
|
||||
{selectedMaker !== undefined && (
|
||||
<div className='flex gap-4 flex-col'>
|
||||
<Contacts
|
||||
name={selectedMaker.name}
|
||||
email={selectedMaker.email}
|
||||
phoneNumber={selectedMaker.phoneNumber}
|
||||
role={'maker'}
|
||||
/>
|
||||
{(selectedMaker.stock !== undefined && selectedMaker.stock.length > 0) &&
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className={buttonVariants({ variant: "kios" })}
|
||||
>
|
||||
See catalogue
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className='lg:max-w-screen-lg overflow-y-scroll max-h-screen'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-4xl underline underline-offset-2">{selectedMaker.name}'s stock</DialogTitle>
|
||||
<ul className='flex flex-col gap-4'>
|
||||
{selectedMaker.stock.map((product, i) => {
|
||||
return (
|
||||
@ -322,175 +212,175 @@ export const KiosMap = () => {
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedRetailer !== undefined && (
|
||||
<>
|
||||
<Contacts
|
||||
name={selectedRetailer.name}
|
||||
email={selectedRetailer.email}
|
||||
phoneNumber={selectedRetailer.phoneNumber}
|
||||
role={'retailer'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedDispatch !== undefined && (
|
||||
<div className='flex flex-col gap-8'>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold underline-offset-2 underline py-2'>
|
||||
Product{selectedDispatch.products.length > 1 && 's'}
|
||||
</h2>
|
||||
{selectedDispatch.products.map((product, i) => {
|
||||
return (
|
||||
<div className='flex flex-row items-center gap-4'>
|
||||
<img
|
||||
src={product.picture.url}
|
||||
alt={product.picture.alt}
|
||||
className='border-2 border-black'
|
||||
width={60}
|
||||
/>
|
||||
<h3 className='font-bold text-xl'>{product.name}</h3>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedRetailer !== undefined && (
|
||||
<>
|
||||
<Contacts
|
||||
name={selectedRetailer.name}
|
||||
email={selectedRetailer.email}
|
||||
phoneNumber={selectedRetailer.phoneNumber}
|
||||
role={'retailer'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Contacts
|
||||
name={selectedDispatch.maker.name}
|
||||
email={selectedDispatch.maker.email}
|
||||
phoneNumber={selectedDispatch.maker.phoneNumber}
|
||||
role={'maker'}
|
||||
/>
|
||||
{selectedDispatch !== undefined && (
|
||||
<div className='flex flex-col gap-8'>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold underline-offset-2 underline py-2'>
|
||||
Product{selectedDispatch.products.length > 1 && 's'}
|
||||
</h2>
|
||||
{selectedDispatch.products.map((product, i) => {
|
||||
return (
|
||||
<div className='flex flex-row items-center gap-4'>
|
||||
<img
|
||||
src={product.picture.url}
|
||||
alt={product.picture.alt}
|
||||
className='border-2 border-black'
|
||||
width={60}
|
||||
/>
|
||||
<h3 className='font-bold text-xl'>{product.name}</h3>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Contacts
|
||||
name={selectedDispatch.retailer.name}
|
||||
email={selectedDispatch.retailer.email}
|
||||
phoneNumber={selectedDispatch.retailer.phoneNumber}
|
||||
role={'retailer'}
|
||||
/>
|
||||
|
||||
{selectedDispatch.courier !== undefined ? (
|
||||
|
||||
<Contacts
|
||||
name={selectedDispatch.courier.name}
|
||||
email={selectedDispatch.courier.email}
|
||||
phoneNumber={selectedDispatch.courier.phoneNumber}
|
||||
role={'courier'}
|
||||
name={selectedDispatch.maker.name}
|
||||
email={selectedDispatch.maker.email}
|
||||
phoneNumber={selectedDispatch.maker.phoneNumber}
|
||||
role={'maker'}
|
||||
/>
|
||||
) :
|
||||
<div>
|
||||
<h2 className='text-xl font-bold underline-offset-2 underline py-2'>No courier!</h2>
|
||||
<Button
|
||||
variant={"kios"}
|
||||
onClick={() => handleAcceptRoute()}
|
||||
>
|
||||
Accept route as courier
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Contacts
|
||||
name={selectedDispatch.retailer.name}
|
||||
email={selectedDispatch.retailer.email}
|
||||
phoneNumber={selectedDispatch.retailer.phoneNumber}
|
||||
role={'retailer'}
|
||||
/>
|
||||
|
||||
{selectedDispatch.courier !== undefined ? (
|
||||
|
||||
<Contacts
|
||||
name={selectedDispatch.courier.name}
|
||||
email={selectedDispatch.courier.email}
|
||||
phoneNumber={selectedDispatch.courier.phoneNumber}
|
||||
role={'courier'}
|
||||
/>
|
||||
) :
|
||||
<div>
|
||||
<h2 className='text-xl font-bold underline-offset-2 underline py-2'>No courier!</h2>
|
||||
<Button
|
||||
variant={"kios"}
|
||||
onClick={() => handleAcceptRoute()}
|
||||
>
|
||||
Accept route as courier
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<MapContainer
|
||||
id="map"
|
||||
center={[-6.1815, 106.8228]}
|
||||
zoom={3}
|
||||
maxBounds={bounds} // Restrict panning beyond these bounds
|
||||
maxBoundsViscosity={0.9} // How strongly to snap the map's bounds to the restricted area
|
||||
style={{ height: '800px', width: '100%' }}
|
||||
>
|
||||
<TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
maxZoom={19}
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode("", "none")
|
||||
}}
|
||||
attribution='© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
/>
|
||||
|
||||
{(makers && !isLoadingMakers) &&
|
||||
<LayerGroup>
|
||||
{makers.map((maker: any, index: number) => (
|
||||
<Marker
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode(maker.id, "maker")
|
||||
}}
|
||||
key={maker.id}
|
||||
position={[locationSwitcharoo(maker.location)[0], locationSwitcharoo(maker.location)[1]]}
|
||||
icon={selectedNode.id === maker.id ? selectedDotIcon : blackDotIcon}
|
||||
>
|
||||
{/* <Popup>{maker.name}</Popup> */}
|
||||
</Marker>
|
||||
))}
|
||||
</LayerGroup>
|
||||
)
|
||||
}
|
||||
|
||||
{(retailers && !isLoadingRetailers) &&
|
||||
<LayerGroup>
|
||||
{retailers.map((retailer: any, index: number) => (
|
||||
<Marker
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode(retailer.id, "retailer")
|
||||
}}
|
||||
key={retailer.id}
|
||||
position={[locationSwitcharoo(retailer.location)[0], locationSwitcharoo(retailer.location)[1]]}
|
||||
icon={selectedNode.id === retailer.id ? selectedDotIcon : blackDotIcon}
|
||||
>
|
||||
{/* <Popup>{retailer.name}</Popup> */}
|
||||
</Marker>
|
||||
))}
|
||||
</LayerGroup>
|
||||
}
|
||||
<MapContainer
|
||||
id="map"
|
||||
center={[-6.1815, 106.8228]}
|
||||
zoom={3}
|
||||
maxBounds={bounds} // Restrict panning beyond these bounds
|
||||
maxBoundsViscosity={0.9} // How strongly to snap the map's bounds to the restricted area
|
||||
style={{ height: '800px', width: '100%' }}
|
||||
>
|
||||
<TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
maxZoom={19}
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode("", "none")
|
||||
}}
|
||||
attribution='© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
/>
|
||||
|
||||
{(dispatches && !isLoadingDispatches) &&
|
||||
<LayerGroup>
|
||||
{dispatches.map((dispatch: any, index: number) => {
|
||||
{(makers && !isLoadingMakers) &&
|
||||
<LayerGroup>
|
||||
{makers.map((maker: any, index: number) => (
|
||||
<Marker
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode(maker.id, "maker")
|
||||
}}
|
||||
key={maker.id}
|
||||
position={[locationSwitcharoo(maker.location)[0], locationSwitcharoo(maker.location)[1]]}
|
||||
icon={selectedNode.id === maker.id ? selectedDotIcon : blackDotIcon}
|
||||
>
|
||||
{/* <Popup>{maker.name}</Popup> */}
|
||||
</Marker>
|
||||
))}
|
||||
</LayerGroup>
|
||||
}
|
||||
|
||||
if (dispatch.maker && dispatch.retailer) {
|
||||
{(retailers && !isLoadingRetailers) &&
|
||||
<LayerGroup>
|
||||
{retailers.map((retailer: any, index: number) => (
|
||||
<Marker
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode(retailer.id, "retailer")
|
||||
}}
|
||||
key={retailer.id}
|
||||
position={[locationSwitcharoo(retailer.location)[0], locationSwitcharoo(retailer.location)[1]]}
|
||||
icon={selectedNode.id === retailer.id ? selectedDotIcon : blackDotIcon}
|
||||
>
|
||||
{/* <Popup>{retailer.name}</Popup> */}
|
||||
</Marker>
|
||||
))}
|
||||
</LayerGroup>
|
||||
}
|
||||
|
||||
const start = locationSwitcharoo(dispatch.maker.location);
|
||||
const end = locationSwitcharoo(dispatch.retailer.location);
|
||||
{(dispatches && !isLoadingDispatches) &&
|
||||
<LayerGroup>
|
||||
{dispatches.map((dispatch: any, index: number) => {
|
||||
|
||||
if (dispatch.maker && dispatch.retailer) {
|
||||
|
||||
const start = locationSwitcharoo(dispatch.maker.location);
|
||||
const end = locationSwitcharoo(dispatch.retailer.location);
|
||||
|
||||
|
||||
let productsString = '';
|
||||
dispatch.products.forEach((product: any, i: number) => {
|
||||
productsString += product.productTitle + (i + 1 < dispatch.products.length ? ', ' : '');
|
||||
});
|
||||
let productsString = '';
|
||||
dispatch.products.forEach((product: any, i: number) => {
|
||||
productsString += product.productTitle + (i + 1 < dispatch.products.length ? ', ' : '');
|
||||
});
|
||||
|
||||
//status type should already be inferred when list of dispatches is created, weird that is is required
|
||||
const status: DispatchStatus = dispatch.status;
|
||||
//status type should already be inferred when list of dispatches is created, weird that is is required
|
||||
const status: DispatchStatus = dispatch.status;
|
||||
|
||||
const dashArray: string = dashArrays[status]
|
||||
const dashColor: string = dashColors[status]
|
||||
const dashOpacity: number = dashOpacities[status]
|
||||
const dashArray: string = dashArrays[status]
|
||||
const dashColor: string = dashColors[status]
|
||||
const dashOpacity: number = dashOpacities[status]
|
||||
|
||||
return (
|
||||
<Polyline
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode(dispatch.id, "dispatch")
|
||||
}}
|
||||
key={dispatch.id}
|
||||
positions={[[start[0], start[1]], [end[0], end[1]]]}
|
||||
pathOptions={{ color: selectedNode.id === dispatch.id ? dashColorSelected : dashColor }}
|
||||
opacity={dashOpacity}
|
||||
dashArray={dashArray} />
|
||||
);
|
||||
}
|
||||
})}
|
||||
</LayerGroup>
|
||||
}
|
||||
</MapContainer >
|
||||
return (
|
||||
<Polyline
|
||||
eventHandlers={{
|
||||
click: () => handleSelectNode(dispatch.id, "dispatch")
|
||||
}}
|
||||
key={dispatch.id}
|
||||
positions={[[start[0], start[1]], [end[0], end[1]]]}
|
||||
pathOptions={{ color: selectedNode.id === dispatch.id ? dashColorSelected : dashColor }}
|
||||
opacity={dashOpacity}
|
||||
dashArray={dashArray} />
|
||||
);
|
||||
}
|
||||
})}
|
||||
</LayerGroup>
|
||||
}
|
||||
</MapContainer >
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
143
astro/src/components/LoginForm.tsx
Normal file
143
astro/src/components/LoginForm.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/Button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import axios from "axios";
|
||||
import { setAuthCookie } from "@/utils/authUtils"
|
||||
|
||||
const API_URL = "http://localhost:3001";
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Credentials": "true"
|
||||
}
|
||||
|
||||
const loginFetch = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/users/login`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
};
|
||||
|
||||
interface LoginFormProps {
|
||||
setAuthToken: (token: string) => void;
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
export function LoginForm(props: LoginFormProps) {
|
||||
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/api/users/login`, {
|
||||
email: email,
|
||||
password: password
|
||||
}, {
|
||||
withCredentials: true, // include cookies in the request
|
||||
headers: headers
|
||||
});
|
||||
const data = response.data;
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw Error("Login failed")
|
||||
}
|
||||
|
||||
setAuthCookie(data.token, 30);
|
||||
props.setAuthToken(data.token)
|
||||
return
|
||||
|
||||
} catch (error) {
|
||||
window.alert(error)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Email must be valid"
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
login(values.email, values.password)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
{props.authToken ? <p>Login successful</p>
|
||||
:
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button variant={"kios"} type="submit">Login</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -3,9 +3,9 @@ import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
@ -22,8 +22,6 @@ export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
phoneNumber?: number;
|
||||
adminOfMakers?: string[] | Maker[];
|
||||
adminOfRetailers?: string[] | Retailer[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@ -35,28 +33,6 @@ export interface User {
|
||||
lockUntil?: string;
|
||||
password?: string;
|
||||
}
|
||||
export interface Maker {
|
||||
id: string;
|
||||
name: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
location: [number, number];
|
||||
stock?: string[] | Product[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
picture: string | Media;
|
||||
weight?: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt?: string;
|
||||
@ -69,20 +45,6 @@ export interface Media {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
export interface Retailer {
|
||||
id: string;
|
||||
name: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
location: [number, number];
|
||||
stock?: string[] | Product[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Courier {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
@ -99,3 +61,41 @@ export interface Dispatch {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
picture: string | Media;
|
||||
weight?: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Maker {
|
||||
id: string;
|
||||
name: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
location: [number, number];
|
||||
admins?: string[] | User[];
|
||||
stock?: string[] | Product[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Retailer {
|
||||
id: string;
|
||||
name: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
location: [number, number];
|
||||
admins?: string[] | User[];
|
||||
stock?: string[] | Product[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
12
astro/src/utils/authUtils.ts
Normal file
12
astro/src/utils/authUtils.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const hasAuthCookie = () => {
|
||||
const matches = document.cookie.match(/^(.*;)?\s*payload-token\s*=\s*[^;]+(.*)?$/) || []
|
||||
const hasMatch = matches.length > 0
|
||||
return hasMatch;
|
||||
}
|
||||
|
||||
export const setAuthCookie = (value: string, expirationDays: number) => {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (expirationDays * 24 * 60 * 60 * 1000));
|
||||
const expires = "expires=" + date.toUTCString();
|
||||
document.cookie = "payload-token=" + value + ";" + expires + ";path=/";
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
import type { User, Node, Retailer, Maker, Product, Dispatch } from '../astroTypes';
|
||||
import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { hasAuthCookie } from './authUtils';
|
||||
|
||||
const API_URL = "http://localhost:3001"
|
||||
|
||||
const nonAuthHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
const authHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
const getMakers = async () => {
|
||||
const url = `${API_URL}/api/makers`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const makers: Maker[] = response.data.docs;
|
||||
console.log(`Fetch result from ${url}`, makers)
|
||||
return makers;
|
||||
|
||||
}
|
||||
|
||||
export const useGetMakers = () => {
|
||||
return useQuery<Maker[]>({
|
||||
queryFn: () => getMakers(),
|
||||
queryKey: ['makers'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
|
||||
const getRetailers = async () => {
|
||||
const url = `${API_URL}/api/retailers`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const retailers: Retailer[] = response.data.docs;
|
||||
console.log(`Fetch result from ${url}`, retailers)
|
||||
return retailers;
|
||||
|
||||
}
|
||||
|
||||
export const useGetRetailers = () => {
|
||||
return useQuery<Retailer[]>({
|
||||
queryFn: () => getRetailers(),
|
||||
queryKey: ['retailers'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
|
||||
const getUser = async (userId: string) => {
|
||||
const url = `${API_URL}/api/users/${userId}`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const user: User = response.data.docs;
|
||||
console.log(`Fetch result from ${url}`, user)
|
||||
return user;
|
||||
|
||||
}
|
||||
|
||||
export const useGetUser = (userId: string) => {
|
||||
return useQuery<User>({
|
||||
queryFn: () => getUser(userId),
|
||||
queryKey: ['user'],
|
||||
enabled: true//If login cookie
|
||||
})
|
||||
}
|
||||
|
||||
const getMyself = async (authToken: string) => {
|
||||
const url = `${API_URL}/api/users/me`
|
||||
console.log("Fetching url:", url)
|
||||
|
||||
const response = await axios.get(`${API_URL}/api/users/me`, {
|
||||
withCredentials: true,
|
||||
headers: authHeaders
|
||||
});
|
||||
|
||||
const user: User = response.data.docs;
|
||||
console.log(`Fetch result from ${url}`, user)
|
||||
return user;
|
||||
|
||||
}
|
||||
|
||||
export const useGetMyself = (authToken: string) => {
|
||||
return useQuery<User>({
|
||||
queryFn: () => getMyself(authToken),
|
||||
queryKey: ['myself'],
|
||||
enabled: authToken !== ''
|
||||
})
|
||||
}
|
||||
|
||||
const getDispatches = async () => {
|
||||
const url = `${API_URL}/api/dispatches`
|
||||
console.log("Fetching url:", url)
|
||||
const response = await axios.get(url);
|
||||
|
||||
const dispatches: Dispatch[] = response.data.docs;
|
||||
|
||||
console.log(`Fetch result from ${url}`, dispatches)
|
||||
return dispatches;
|
||||
|
||||
}
|
||||
|
||||
export const useGetDispatches = () => {
|
||||
return useQuery<Dispatch[]>({
|
||||
queryFn: () => getDispatches(),
|
||||
queryKey: ['dispatches'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user