Compare commits

...

2 Commits

Author SHA1 Message Date
toqvist 00cf0d9905 Auth works but not with cookie
continuous-integration/drone/push Build is passing Details
2024-04-06 17:43:57 +02:00
toqvist 3b5ac81cc3 Auth flow functional 2024-04-06 17:28:36 +02:00
16 changed files with 655 additions and 406 deletions

View File

@ -1,9 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M50.0529 99.6475C22.7705 99.7533 -0.0352444 77.3352 0.422986 49.2422C0.669726 35.6715 5.63976 24.0395 15.3331 14.5224C24.9559 5.04054 36.6937 0.281993 50.1586 0.317242C63.835 0.317242 75.6433 5.28728 85.3014 15.0159C94.9947 24.7797 99.7533 36.7289 99.718 50.4759C99.6828 59.2527 97.3916 67.5009 92.915 75.1146C88.4385 82.7282 82.3757 88.7557 74.7268 93.1618C67.1132 97.4974 58.865 99.6475 50.0529 99.6475Z" fill="#CFADC5"/>
<path d="M42.4392 67.2541C41.9105 67.2541 41.417 67.2541 40.8883 67.2541C40.6768 67.2541 40.5358 67.1837 40.3948 67.0074C37.7864 63.6236 35.2133 60.2749 32.6049 56.8911C31.7942 55.8336 31.4769 55.8336 30.8424 56.9968C30.7367 57.1731 30.7014 57.3846 30.7367 57.5608C30.8424 60.2397 30.7719 62.8833 30.7719 65.5622C30.7719 65.9852 30.7367 66.4082 30.7367 66.8312C30.7367 67.1484 30.631 67.2541 30.3137 67.2541C28.4808 67.2541 26.6479 67.2541 24.815 67.2541C24.4977 67.2541 24.392 67.1837 24.3567 66.8312C24.2862 66.1967 24.2862 65.5975 24.2862 64.963C24.2862 54.5647 24.2862 44.1664 24.2862 33.7681C24.2862 29.8202 24.2862 25.9077 24.2862 21.9598C24.2862 21.8541 24.2862 21.7483 24.2862 21.6073C24.2862 20.6556 24.674 20.3031 25.6257 20.3736C26.6126 20.4794 27.6348 20.4441 28.6218 20.4441C28.9743 20.4441 29.362 20.3736 29.7145 20.3736C30.4547 20.3736 30.8072 20.6556 30.8072 21.3958C30.8072 23.4755 30.8072 25.5552 30.8072 27.5996C30.8072 33.2041 30.8072 38.8086 30.8072 44.3779C30.8072 45.4353 30.8424 46.528 30.7719 47.5855C30.7719 47.8675 30.7015 48.2552 31.0187 48.3962C31.2654 48.5019 31.4769 48.1847 31.6884 47.9732C33.2393 46.387 34.755 44.8009 36.306 43.2147C37.1519 42.3335 38.0331 41.4522 38.8791 40.571C39.2668 40.1481 39.7251 39.9366 40.289 39.9718C41.9105 40.0423 43.5672 40.0071 45.1886 40.0071C45.6821 40.0071 46.2108 39.9718 46.7043 39.9718C46.81 39.9718 46.9158 39.9718 46.9863 39.9718C47.2682 40.0071 47.6207 39.9718 47.7265 40.289C47.797 40.5005 47.4797 40.6415 47.3035 40.8178C45.4001 42.6507 43.4967 44.5189 41.5932 46.3518C40.148 47.7617 38.6676 49.1364 37.2224 50.5111C36.5879 51.1103 36.5175 51.5686 37.0814 52.2735C40.289 56.3271 43.4967 60.3807 46.7043 64.4343C47.3035 65.1745 47.3035 65.1745 48.1142 64.6105C48.1847 64.5753 48.22 64.54 48.2905 64.4695C48.6077 64.2228 48.8544 64.0465 49.3127 64.1875C50.0529 64.399 50.8283 63.9408 51.0751 63.1301C51.3218 62.3899 51.8505 61.9316 52.4145 61.5086C52.8375 61.1914 53.2605 60.9094 53.613 60.5217C53.895 60.2749 54.1065 59.993 54.2474 59.6405C54.4237 59.2527 54.6704 58.9355 55.0229 58.724C55.1992 58.6535 55.3049 58.5125 55.4106 58.3363C55.6574 57.9485 55.9394 57.6313 56.2566 57.3493C56.7148 56.9616 57.1026 56.5386 57.2788 55.9394C57.3493 55.7279 57.4903 55.5869 57.6313 55.4106C58.0543 54.9172 58.5125 54.4589 58.865 53.9302C59.0412 53.7187 59.2175 53.472 59.4642 53.331C60.1339 52.9433 60.4864 52.344 60.7332 51.6391C61.1562 50.4759 61.3676 49.2774 61.6496 48.079C61.9669 46.8805 62.7071 46.0698 63.976 45.7526C64.3638 45.6468 64.7868 45.6821 65.1392 45.8583C66.5139 46.528 67.9239 47.1625 69.1576 48.079C70.4265 49.0307 71.0962 50.4054 71.2725 51.9915C71.343 52.6613 71.1667 53.331 70.6028 53.8245C70.2855 54.0712 70.1093 54.4237 70.0035 54.8114C69.933 55.1992 69.792 55.5516 69.51 55.8336C68.7698 56.6443 68.5936 57.6666 68.5231 58.6888C68.4526 59.8872 68.2764 61.0504 67.9591 62.2136C67.7124 63.0596 67.6066 63.9408 67.9944 64.7868C68.1001 65.0335 68.2411 65.245 68.4173 65.4565C69.1928 66.3024 70.1445 66.2672 70.8495 65.386C71.202 64.9277 71.484 64.399 72.0832 64.2228C72.2242 64.1875 72.3299 63.9408 72.4004 63.7998C72.8234 63.1301 73.2464 62.4251 73.6694 61.7554C73.9866 61.2619 74.2686 60.7684 74.7268 60.3807C75.0793 60.0635 75.2556 59.6052 75.4671 59.1822C75.6433 58.8298 75.7843 58.5125 75.9605 58.16C76.2425 57.5256 76.8065 57.2788 77.441 57.1731C77.7229 57.1378 77.8639 57.3141 78.0049 57.5256C78.7452 58.9355 79.2034 60.3807 79.4854 61.9316C79.6969 63.1301 79.6969 64.3638 79.6616 65.5975C79.6616 65.95 79.5559 66.2672 79.3444 66.5492C78.7452 67.3246 78.1459 68.1354 77.5115 68.9461C77.4762 69.0166 77.441 69.0518 77.3705 69.0871C76.6655 69.3691 76.1368 69.933 75.5728 70.3913C75.1498 70.7085 74.7268 70.9905 74.1629 71.0257C73.7399 71.061 73.3521 71.202 72.9644 71.4135C72.4004 71.6955 71.766 71.7307 71.1667 71.7307C69.4396 71.6602 67.7476 71.343 66.1262 70.6733C64.822 70.1445 63.7998 69.2633 62.9538 68.1354C61.7906 66.6197 61.7906 66.6197 61.6849 64.5753C61.6496 64.117 61.5791 63.694 61.3676 63.2711C61.0504 62.7071 60.4864 62.5661 59.9577 62.9538C59.6405 63.1653 59.429 63.4473 59.2527 63.7998C59.006 64.3285 58.6535 64.7515 58.2305 65.1392C56.7853 66.4434 55.6574 67.9591 54.3884 69.4043C53.5072 70.356 52.5203 71.202 51.4981 72.0127C51.0398 72.4004 50.4054 72.4709 49.8414 72.5414C48.9954 72.6472 48.1847 72.9292 47.515 73.4579C45.8583 74.7268 44.1311 74.5858 42.404 73.6341C41.417 73.1054 40.571 72.4709 39.8661 71.5545C39.2668 70.779 39.1258 69.8978 39.3726 68.9461C39.5136 68.3821 39.8661 68.0296 40.43 67.8886C41.135 67.7124 41.8047 67.5009 42.5097 67.2894C42.4392 67.2894 42.4392 67.2894 42.4392 67.2541Z" fill="#84285E"/>
<path d="M73.7046 36.5175C73.7046 37.5397 73.6694 38.4561 73.4579 39.3726C73.3874 39.7603 73.2111 40.1128 72.9644 40.43C72.2947 41.3818 71.3782 42.122 70.497 42.8974C70.4618 42.9327 70.3913 42.9679 70.356 43.0032C68.8051 43.4262 67.2541 43.8139 65.7737 42.7564C64.4695 42.545 64.0113 41.417 63.4826 40.43C62.9186 39.4431 62.7776 38.3504 62.7776 37.2577C62.7776 36.0945 63.0948 35.0018 63.4121 33.9091C63.694 32.9574 64.4343 32.3229 64.963 31.5122C64.963 31.5122 64.963 31.4769 64.9982 31.4769C65.527 31.3359 65.9147 30.8777 66.5139 30.8425C67.0427 30.8072 67.5714 30.7367 68.1001 30.6662C70.1093 30.3842 71.5545 31.2654 72.6119 32.9221C73.0349 33.5566 73.3169 34.2263 73.5989 34.9313C73.8104 35.4953 73.6694 36.0592 73.7046 36.5175Z" fill="#84285E"/>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
astro/public/kios-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

80
astro/src/astroTypes.ts Normal file
View 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;
}

View File

@ -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,66 @@ 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>
: <p>Logged in</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 +214,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='&copy; <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='&copy; <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>
);
};

View 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>
)
}

View File

@ -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"

View File

@ -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;
}

View 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=/";
}

View File

@ -0,0 +1,116 @@
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 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 authHeaders = {
"Content-Type": "application/json",
"Authorization": `JWT ${authToken}`,
}
const response = await axios.get(`${API_URL}/api/users/me`, {
withCredentials: true,
headers: authHeaders
});
const user: User = response.data
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
})
}

View File

@ -894,17 +894,17 @@
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.2.4.tgz#1b380fad8eaccc6bec4ab1c265d70413e66e8034"
integrity sha512-ClaUWpt8oTzjcF0MM1P81AeWyzc1sNSJlAjMG80CbwqbFqXSNz+NpQVUC0icobt3sZn43Sn27M4pHD/Jmp3zHw==
"@tanstack/query-core@5.28.13":
version "5.28.13"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.28.13.tgz#15c187c23b87a393e91d0fd2ea6dfc22b8a85b75"
integrity sha512-C3+CCOcza+mrZ7LglQbjeYEOTEC3LV0VN0eYaIN6GvqAZ8Foegdgch7n6QYPtT4FuLae5ALy+m+ZMEKpD6tMCQ==
"@tanstack/query-core@5.29.0":
version "5.29.0"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.29.0.tgz#d0b3d12c07d5a47f42ab0c1ed4f317106f3d4b20"
integrity sha512-WgPTRs58hm9CMzEr5jpISe8HXa3qKQ8CxewdYZeVnA54JrPY9B1CZiwsCoLpLkf0dGRZq+LcX5OiJb0bEsOFww==
"@tanstack/react-query@^5.28.14":
version "5.28.14"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.28.14.tgz#9585b6300eb8f167ed374e2748043dc8d6476709"
integrity sha512-cZqt03Igb3I9tM72qNX5TAAmeYl75Z+k4Mv92VkXIXc2hCrv0fIywd7GN3JV1BBJl4mr7Cc+OOKKOPy8sNVOkA==
version "5.29.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.29.0.tgz#42b3a2de4ed1d63666f0af04392a34b5e70d49c0"
integrity sha512-yxlhHB73jaBla6h5B6zPaGmQjokkzAhMHN4veotkPNiQ3Ac/mCxgABRZPsJJrgCTvhpcncBZcDBFxaR2B37vug==
dependencies:
"@tanstack/query-core" "5.28.13"
"@tanstack/query-core" "5.29.0"
"@testing-library/dom@^9.0.0":
version "9.3.4"
@ -1018,9 +1018,9 @@
"@types/unist" "^2"
"@types/node@*":
version "20.12.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.4.tgz#af5921bd75ccdf3a3d8b3fa75bf3d3359268cd11"
integrity sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==
version "20.12.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3"
integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==
dependencies:
undici-types "~5.26.4"
@ -1867,9 +1867,9 @@ eastasianwidth@^0.2.0:
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
electron-to-chromium@^1.4.668:
version "1.4.728"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.728.tgz#ac54d9d1b38752b920ec737a48c83dec2bf45ea1"
integrity sha512-Ud1v7hJJYIqehlUJGqR6PF1Ek8l80zWwxA6nGxigBsGJ9f9M2fciHyrIiNMerSHSH3p+0/Ia7jIlnDkt41h5cw==
version "1.4.729"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz#8477d21e2a50993781950885b2731d92ad532c00"
integrity sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==
emoji-regex@^10.2.1, emoji-regex@^10.3.0:
version "10.3.0"

View File

@ -30,12 +30,18 @@ const Makers: CollectionConfig = {
label: 'Location',
required: true
},
{
name: 'admins',
type: 'relationship',
relationTo: 'users',
hasMany: true,
},
{
name: 'stock',
type: 'relationship',
relationTo: 'products',
hasMany: true,
},
}
],
};

View File

@ -31,6 +31,12 @@ const Retailers: CollectionConfig = {
label: 'Location',
required: true
},
{
name: 'admins',
type: 'relationship',
relationTo: 'users',
hasMany: true,
},
{
name: 'stock',
type: 'relationship',

View File

@ -2,12 +2,22 @@ import { CollectionConfig } from 'payload/types';
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
auth: {
tokenExpiration: 7200, // How many seconds to keep the user logged in
verify: false, // Require email verification before being allowed to authenticate
maxLoginAttempts: 5, // Automatically lock a user out after X amount of failed logins
lockTime: 600 * 1000, // Time period to allow the max login attempts
cookies: {
secure: true,
sameSite: "lax",
domain: process.env.ASTRO_HOST
},
},
fields: [
// Email added by default
@ -20,20 +30,7 @@ const Users: CollectionConfig = {
name: 'phoneNumber',
type: 'number',
required: false
},
{
name: 'adminOfMakers',
type: 'relationship',
relationTo: 'makers',
hasMany: true,
},
{
name: 'adminOfRetailers',
type: 'relationship',
relationTo: 'retailers',
hasMany: true,
},
}
],
};

View File

@ -21,6 +21,7 @@ payload.init({
const corsOptions = {
origin: 'http://localhost:3000',
credentials: true
};
app.use(cors(corsOptions));