Compare commits
46 Commits
12788566f5
...
main
Author | SHA1 | Date | |
---|---|---|---|
2c30f4a345 | |||
918c5d0214 | |||
324ad812e2 | |||
1f8142a82f | |||
cf33c001db | |||
f30199e5f8 | |||
9be7ae81ae | |||
00cf0d9905 | |||
3b5ac81cc3 | |||
98e967f7c0 | |||
d0a9dd6a13 | |||
e0bb3a349d | |||
1447d8fc17 | |||
0645bbd877 | |||
ed420d5ff1 | |||
12c43e9a00 | |||
1a2d143c4f | |||
f059ca37da | |||
3b359dd3cc | |||
318db9322f | |||
9d3e600eae | |||
2184c11448 | |||
a57206f8b2 | |||
35afa4a755 | |||
a1f3187bc7 | |||
a5cac66c3f | |||
509b6b5e8b | |||
7f508fc04b | |||
974cee1cf4 | |||
6240a4104a | |||
f51f6daad3 | |||
c72685679c | |||
15700ae304 | |||
174a163cd3 | |||
2ebb81a981 | |||
225d45307a | |||
cb26b8edcd | |||
4a95f6e2d1 | |||
d8438e2d84 | |||
95243557c6 | |||
4be030ec28 | |||
816eac8470 | |||
37d0359dae | |||
75fe3cdcf5 | |||
dc4618383e | |||
2c79904102 |
51
.drone.yml
Normal file
51
.drone.yml
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: publish pipeline
|
||||
steps:
|
||||
- name: publish astro container
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username: 3wordchant
|
||||
password:
|
||||
from_secret: git_autonomic_zone_token_3wc
|
||||
repo: git.autonomic.zone/ruangrupa/lumbung-kios-astro
|
||||
auto_tag: true
|
||||
registry: git.autonomic.zone
|
||||
context: astro
|
||||
dockerfile: astro/Dockerfile
|
||||
target: prod
|
||||
- name: publish payload container
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username: 3wordchant
|
||||
password:
|
||||
from_secret: git_autonomic_zone_token_3wc
|
||||
repo: git.autonomic.zone/ruangrupa/lumbung-kios-payload
|
||||
auto_tag: true
|
||||
registry: git.autonomic.zone
|
||||
context: payload
|
||||
dockerfile: payload/Dockerfile
|
||||
target: prod
|
||||
- name: deploy stack
|
||||
image: git.coopcloud.tech/coop-cloud/stack-ssh-deploy:latest
|
||||
settings:
|
||||
stack: kios_lumbung_space
|
||||
host: lumbung.space
|
||||
deploy_key:
|
||||
from_secret: drone_ssh_lumbung.space
|
||||
environment:
|
||||
DOMAIN: kios.lumbung.space
|
||||
STACK_NAME: kios_lumbung_space
|
||||
SECRET_PAYLOAD_SECRET_VERSION: v1
|
||||
SECRET_TOKEN_VERSION: v1
|
||||
SECRET_MONGO_PASSWORD_VERSION: v1
|
||||
depends_on:
|
||||
- publish astro container
|
||||
- publish payload container
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
20
README.md
20
README.md
@ -2,6 +2,26 @@
|
||||
|
||||
Astroad is a pre-configured setup for Astro and Payloadcms, designed to make it easy for you to start building your website. With Astroad, you'll have a complete development environment that you can run locally using Docker. This setup simplifies the testing and development of your website before deploying it to a production environment.
|
||||
|
||||
## Important notes
|
||||
### Adding dependencies
|
||||
1. When adding a dedpendency to astro or payload, you must regenerate the yarn.lock file for the dev server toinstall the dependencies.
|
||||
2. Regenerate the lock file:
|
||||
`cd astro` or `cd payload`
|
||||
`rm yarn.lock`
|
||||
`yarn install`
|
||||
3. Restart dev server `yarn dev`
|
||||
|
||||
## Dev server getting stuck
|
||||
Sometimes the dev script gets stuck, for an unknown reason:
|
||||
```
|
||||
[+] Running 3/0
|
||||
✔ Container astroad-mongo Running 0.0s
|
||||
✔ Container astroad-payload Running 0.0s
|
||||
✔ Container astroad-astro Running
|
||||
```
|
||||
|
||||
Running `yarn stop` and then `yarn dev` should resolve the issue.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before getting started with Astroad, make sure you have the necessary software installed:
|
||||
|
@ -0,0 +1 @@
|
||||
PAYLOAD_URL=http://localhost:3001
|
@ -3,27 +3,23 @@ import tailwind from "@astrojs/tailwind";
|
||||
import image from "@astrojs/image";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import prefetch from "@astrojs/prefetch";
|
||||
import react from "@astrojs/react";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
compressHTML: true,
|
||||
server: { port: 3000 },
|
||||
build: {
|
||||
inlineStylesheets: "auto",
|
||||
inlineStylesheets: "auto"
|
||||
},
|
||||
experimental: {
|
||||
viewTransitions: true,
|
||||
},
|
||||
integrations: [
|
||||
tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false,
|
||||
},
|
||||
}),
|
||||
image({
|
||||
serviceEntryPoint: "@astrojs/image/sharp",
|
||||
}),
|
||||
prefetch({
|
||||
selector: "a",
|
||||
}),
|
||||
sitemap(),
|
||||
],
|
||||
});
|
||||
viewTransitions: true,
|
||||
integrations: [tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false
|
||||
}
|
||||
}), image({
|
||||
serviceEntryPoint: "@astrojs/image/sharp"
|
||||
}), prefetch({
|
||||
selector: "a"
|
||||
}), sitemap(), react()]
|
||||
});
|
18
astro/components.json
Normal file
18
astro/components.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.cjs",
|
||||
"css": "@/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils",
|
||||
"styles": "@/styles"
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ events {
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
@ -32,4 +33,4 @@ http {
|
||||
|
||||
error_page 404 /error_page.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,15 +9,39 @@
|
||||
"build": "astro build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/image": "^0.17.3",
|
||||
"@astrojs/prefetch": "^0.3.0",
|
||||
"@astrojs/sitemap": "^2.0.2",
|
||||
"@astrojs/tailwind": "4.0.0",
|
||||
"astro": "^2.10.12",
|
||||
"@astrojs/image": "0.18.0",
|
||||
"@astrojs/prefetch": "0.4.1",
|
||||
"@astrojs/react": "^3.1.1",
|
||||
"@astrojs/sitemap": "3.1.2",
|
||||
"@astrojs/tailwind": "5.1.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@tanstack/react-query": "^5.28.14",
|
||||
"@tanstack/react-query-devtools": "^5.29.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"astro": "^4.5.13",
|
||||
"axios": "^1.6.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"css-select": "5.1.0",
|
||||
"human-id": "^4.1.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.364.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"sharp": "^0.32.6",
|
||||
"slate-serializers": "0.4.1",
|
||||
"tailwindcss": "^3.3.3"
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.4.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.2",
|
||||
|
@ -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
BIN
astro/public/kios-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
astro/public/lumbung-logo.png
Normal file
BIN
astro/public/lumbung-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
BIN
astro/public/route-guide.png
Normal file
BIN
astro/public/route-guide.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
93
astro/src/astroTypes.ts
Normal file
93
astro/src/astroTypes.ts
Normal file
@ -0,0 +1,93 @@
|
||||
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;
|
||||
admins: User[];
|
||||
};
|
||||
|
||||
export interface Retailer extends Node {
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
location: [number, number];
|
||||
stock: Product[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
admins: User[];
|
||||
};
|
||||
|
||||
const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const;
|
||||
export type DispatchStatus = typeof DISPATCH_STATUS[number];
|
||||
|
||||
export interface Dispatch {
|
||||
id?: string;
|
||||
code?: string; //Human readable id
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
|
||||
maker: Maker;
|
||||
retailer: Retailer;
|
||||
products: Product[];
|
||||
|
||||
courier?: User;
|
||||
|
||||
timeSensitive: boolean;
|
||||
status: DispatchStatus;
|
||||
|
||||
departureDate?: string;
|
||||
arrivalDate?: string;
|
||||
}
|
||||
|
||||
export interface CreateDispatch {
|
||||
code?: string; //Human readable id
|
||||
|
||||
maker: Maker | string;
|
||||
retailer: Retailer | string;
|
||||
products: Product[] | string[] ;
|
||||
|
||||
courier?: User;
|
||||
|
||||
timeSensitive: boolean;
|
||||
status: DispatchStatus;
|
||||
}
|
67
astro/src/components/AddRetailerForm.tsx
Normal file
67
astro/src/components/AddRetailerForm.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
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"
|
||||
|
||||
|
||||
export function AddRetailerForm() {
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// Do something with the form values.
|
||||
// ✅ This will be type-safe and validated according to z schema.
|
||||
console.log(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button variant={"default"} type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
29
astro/src/components/App.tsx
Normal file
29
astro/src/components/App.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useQuery, useMutation, useQueryClient, queryOptions, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import type { ReactNode } from "react";
|
||||
import { KiosMap } from "@/components/KiosMap";
|
||||
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface AppProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const App: React.FC<AppProps> = (props) => {
|
||||
return (
|
||||
<div className="app">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{props.children}
|
||||
<KiosMap/>
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
25
astro/src/components/Contacts.tsx
Normal file
25
astro/src/components/Contacts.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
name: string | undefined;
|
||||
email?: string | undefined;
|
||||
phoneNumber?: string | undefined;
|
||||
role: 'maker' | 'retailer' | 'courier'
|
||||
}
|
||||
|
||||
const roleLabels = {
|
||||
maker: "Maker",
|
||||
retailer: "Retailer",
|
||||
courier: "Courier",
|
||||
}
|
||||
|
||||
export default function Contacts(props: Props) {
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
{props.name && <p className='text-sm font-light'>{roleLabels[props.role]}:</p>}
|
||||
{props.name && <h2 className='text-xl font-bold underline-offset-2 underline py-2'>{props.name}</h2>}
|
||||
{props.phoneNumber && <p>Phone Number: <span className='font-semibold'>{props.phoneNumber}</span></p>}
|
||||
{props.email && <p>Email: <span className='font-semibold'>{props.email}</span></p>}
|
||||
</div>
|
||||
)
|
||||
}
|
410
astro/src/components/KiosMap.tsx
Normal file
410
astro/src/components/KiosMap.tsx
Normal file
@ -0,0 +1,410 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, CircleMarker, Popup, Polyline, LayerGroup, GeoJSON, } from 'react-leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L, { LatLngBounds } from 'leaflet';
|
||||
import Contacts from './Contacts';
|
||||
import type { User, Node, Retailer, Maker, Product, Dispatch, DispatchStatus, CreateDispatch } from '../astroTypes';
|
||||
|
||||
import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/react-query";
|
||||
import { useGetMakers, useGetDispatches, useGetRetailers, useGetUser, useGetMyself, API_URL, useGetMyRetailers, useCreateDispatch } from "../utils/hooks"
|
||||
|
||||
import { Button, buttonVariants } from './ui/Button';
|
||||
|
||||
import { humanId, poolSize, minLength, maxLength } from 'human-id'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { LoginForm } from './LoginForm';
|
||||
import { hasAuthCookie } from '@/utils/authUtils';
|
||||
|
||||
//Payload longitude and latitude are mislabeled/switched in payload
|
||||
const locationSwitcharoo = (location: number[]) => {
|
||||
if (location.length === 2) {
|
||||
const correctedLocation = [location[1], location[0]]
|
||||
return correctedLocation;
|
||||
}
|
||||
console.error("locationSwitcharoo: Location array malformed")
|
||||
return location
|
||||
}
|
||||
|
||||
|
||||
const dashArrays: Record<DispatchStatus, string> = {
|
||||
requested: '20, 10',
|
||||
accepted: '0, 0',
|
||||
archived: '1, 5',
|
||||
}
|
||||
|
||||
const dashColors: Record<DispatchStatus, string> = {
|
||||
requested: '#000',
|
||||
accepted: '#000',
|
||||
archived: '#000',
|
||||
}
|
||||
|
||||
const dashColorSelected: string = '#f87171' //same as tw red 400
|
||||
|
||||
const dashOpacities: Record<DispatchStatus, number> = {
|
||||
requested: 0.7,
|
||||
accepted: 0.7,
|
||||
archived: 0.5
|
||||
}
|
||||
|
||||
interface NodeSelection {
|
||||
id: string,
|
||||
type: "maker" | "retailer" | "dispatch" | "none"
|
||||
}
|
||||
|
||||
export const KiosMap = () => {
|
||||
|
||||
const [authToken, setAuthToken] = useState<string>('')
|
||||
|
||||
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 { data: myRetailers, isLoading: isLoadingMyRetailers } = useGetMyRetailers(myself);
|
||||
|
||||
const createDispatchMutation = useCreateDispatch();
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<NodeSelection>({ id: "", type: "none" })
|
||||
|
||||
let selectedMaker: Maker | undefined = undefined;
|
||||
let selectedRetailer: Retailer | undefined = undefined;
|
||||
let selectedDispatch: Dispatch | undefined = undefined;
|
||||
|
||||
if (selectedNode.type === "maker" && makers) {
|
||||
selectedMaker = makers.find(maker => maker.id === selectedNode.id && selectedNode.type === "maker");
|
||||
}
|
||||
|
||||
if (selectedNode.type === "retailer" && retailers) {
|
||||
selectedRetailer = retailers.find(retailer => retailer.id === selectedNode.id && selectedNode.type === "retailer");
|
||||
}
|
||||
|
||||
if (selectedNode.type === "dispatch" && dispatches) {
|
||||
selectedDispatch = dispatches.find(dispatch => dispatch.id === selectedNode.id && selectedNode.type === "dispatch");
|
||||
}
|
||||
|
||||
|
||||
const handleSelectNode = (nodeId: string, typeParam: "maker" | "retailer" | "dispatch" | "none") => {
|
||||
setSelectedNode({ id: nodeId, type: typeParam })
|
||||
}
|
||||
|
||||
//params: dispatch: Dispatch, courier: User
|
||||
const handleAcceptRoute = () => {
|
||||
|
||||
}
|
||||
|
||||
const handleRequestDispatch = (products: Product[], retailer: Retailer, maker: Maker | undefined) => {
|
||||
if (maker === undefined) {
|
||||
console.error("Request dispatch error: Marker undefined")
|
||||
return
|
||||
}
|
||||
const dispatch: CreateDispatch = {
|
||||
code: humanId({
|
||||
separator: '-',
|
||||
capitalize: false,
|
||||
}),
|
||||
products: products.map((product) => {return product.id}),
|
||||
maker: maker.id,
|
||||
retailer: retailer.id,
|
||||
timeSensitive: false,
|
||||
status: "requested",
|
||||
}
|
||||
createDispatchMutation.mutate(dispatch)
|
||||
}
|
||||
|
||||
const blackDotIcon = L.divIcon({
|
||||
className: 'bg-gray-950 rounded-full',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
|
||||
const selectedDotIcon = L.divIcon({
|
||||
className: 'bg-red-400 rounded-full ring-offset-2 ring-4 ring-red-400 ring-dashed',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
|
||||
|
||||
|
||||
const bounds = new LatLngBounds(
|
||||
[-90, -180], // Southwest corner
|
||||
[90, 180] // Northeast corner
|
||||
);
|
||||
|
||||
return (
|
||||
<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="" />
|
||||
|
||||
{
|
||||
(!hasAuthCookie() && !authToken) ?
|
||||
<DialogTrigger
|
||||
className={`px-12 ${buttonVariants({ variant: "kios" })}`}
|
||||
>
|
||||
Login
|
||||
</DialogTrigger>
|
||||
: myself && <p>Logged in as: {myself?.name}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<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 !== undefined && 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 (
|
||||
<li className="flex flex-row gap-4" key={product.id}>
|
||||
{product.picture.url &&
|
||||
<img
|
||||
width={160}
|
||||
src={product.picture.url}
|
||||
alt={product.picture.alt || ''} />
|
||||
}
|
||||
<div className="flex flex-col pb-4">
|
||||
<h3 className='text-4xl text-black py-2'>
|
||||
{product.name}
|
||||
</h3>
|
||||
{product.weight &&
|
||||
<p className='text-black text-lg'
|
||||
>Weight: {product.weight}</p>
|
||||
}
|
||||
{myself ?
|
||||
(myRetailers !== undefined) &&
|
||||
<ul>
|
||||
{myRetailers.map((retailer, i) => {
|
||||
return (
|
||||
<li key={retailer.id}>
|
||||
<Button
|
||||
variant="kios"
|
||||
onClick={() => handleRequestDispatch([product], retailer, selectedMaker)}
|
||||
>
|
||||
Request product to {retailer.name}
|
||||
</Button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
: <Button disabled>Login to request</Button>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</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' key={product.id}>
|
||||
<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.maker.name}
|
||||
email={selectedDispatch.maker.email}
|
||||
phoneNumber={selectedDispatch.maker.phoneNumber}
|
||||
role={'maker'}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
<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>
|
||||
}
|
||||
|
||||
{(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 ? ', ' : '');
|
||||
});
|
||||
|
||||
//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]
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
142
astro/src/components/LoginForm.tsx
Normal file
142
astro/src/components/LoginForm.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
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"
|
||||
import { API_URL } from "@/utils/hooks"
|
||||
|
||||
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.error(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>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import Content from "@/components/Content.astro";
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import Content from "@/components/astro/Content.astro";
|
||||
import type { Post } from "@/types";
|
||||
import { getPost, getPosts } from "@/utils/payload";
|
||||
|
||||
@ -18,13 +18,13 @@ const post = id && (await getPost(id));
|
||||
|
||||
{
|
||||
post ? (
|
||||
<Layout title={`Astroad | ${post.title!}`}>
|
||||
<BaseLayout title={`Astroad | ${post.title!}`}>
|
||||
<div class="space-y-3 my-3">
|
||||
<a href="/">BACK</a>
|
||||
<h1 class="font-bold text-5xl" transition:name=`title-${post.id}`>{post.title}</h1>
|
||||
{post.content && <Content content={post.content} />}
|
||||
</div>
|
||||
</Layout>
|
||||
</BaseLayout>
|
||||
) : (
|
||||
<div>404</div>
|
||||
)
|
25
astro/src/components/astro/posts.astro
Normal file
25
astro/src/components/astro/posts.astro
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
// const { posts } = Astro.props;
|
||||
import { getPosts } from "@/utils/payload";
|
||||
const posts = await getPosts();
|
||||
|
||||
---
|
||||
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
posts.map((post) => (
|
||||
<a href={`/posts/${post.id}/`}>
|
||||
<article class="text-gray bg-gray-light px-5 py-3 rounded-md shadow-md w-64 text-center hover:-translate-y-1 transition-transform">
|
||||
<h3 class="font-bold text-lg" transition:name=`title-${post.id}`>{post.title}</h3>
|
||||
{post.publishedDate && (
|
||||
<p>
|
||||
{new Date(post.publishedDate).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
</a>
|
||||
))
|
||||
) : (
|
||||
<p>Add Posts in Payloadcms</p>
|
||||
)
|
||||
}
|
57
astro/src/components/ui/Button.tsx
Normal file
57
astro/src/components/ui/Button.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
kios: "text-black border-2 border-gray-950 py-2 px-4 hover:bg-gray-950 transition-all hover:text-white hover:font-bold rounded-none",
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
120
astro/src/components/ui/dialog.tsx
Normal file
120
astro/src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-[999] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-[999] w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
176
astro/src/components/ui/form.tsx
Normal file
176
astro/src/components/ui/form.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
25
astro/src/components/ui/input.tsx
Normal file
25
astro/src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
24
astro/src/components/ui/label.tsx
Normal file
24
astro/src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
@ -30,7 +30,7 @@ const { title } = Astro.props;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="mx-auto max-w-7xl bg-gray px-6 py-8 font-plex text-gray-200">
|
||||
<body class="mx-auto max-w-7xl bg-gray px-6 py-8 font-plex">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
11
astro/src/pages/Map.astro
Normal file
11
astro/src/pages/Map.astro
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import { App } from "@/components/App"
|
||||
---
|
||||
|
||||
<BaseLayout title="Kios">
|
||||
<App
|
||||
client:only="react"
|
||||
>
|
||||
</App>
|
||||
</BaseLayout>
|
7
astro/src/pages/forms/AddRetailer.astro
Normal file
7
astro/src/pages/forms/AddRetailer.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
import { AddRetailerForm } from "@/components/AddRetailerForm";
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
---
|
||||
<BaseLayout title="Astroad">
|
||||
<AddRetailerForm client:visible></AddRetailerForm>
|
||||
</BaseLayout>
|
@ -1,45 +1,12 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro";
|
||||
import { getPosts } from "@/utils/payload";
|
||||
|
||||
const posts = await getPosts();
|
||||
import '../styles/global.css'
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import { App } from "@/components/App"
|
||||
---
|
||||
|
||||
<Layout title="Astroad">
|
||||
<BaseLayout title="Kios">
|
||||
<main class="" >
|
||||
<h1 class="font-bold text-5xl">Astroad</h1>
|
||||
<p class="mt-3 text-lg">
|
||||
Astroad is a pre-configured setup for Astro and Payloadcms that makes it
|
||||
easy to get started with building your website. With Astroad, you'll have
|
||||
a complete development environment that you can run locally using Docker.
|
||||
This makes it easy to test and develop your website before deploying it to
|
||||
a production environment.
|
||||
<br />
|
||||
When you're ready to deploy the website on your own server, Astrotus
|
||||
comes with a production environment that requires the use of Traefik as a
|
||||
reverse proxy. This setup provides a secure and scalable production
|
||||
environment for your website.
|
||||
</p>
|
||||
<h2 class="mt-6 font-bold text-2xl">Posts</h2>
|
||||
<div class="flex gap-4 mt-3 flex-wrap">
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
posts.map((post) => (
|
||||
<a href={`/posts/${post.id}/`}>
|
||||
<article class="text-gray bg-gray-light px-5 py-3 rounded-md shadow-md w-64 text-center hover:-translate-y-1 transition-transform">
|
||||
<h3 class="font-bold text-lg" transition:name=`title-${post.id}`>{post.title}</h3>
|
||||
{post.publishedDate && (
|
||||
<p>
|
||||
{new Date(post.publishedDate).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
</a>
|
||||
))
|
||||
) : (
|
||||
<p>Add Posts in Payloadcms</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
<App
|
||||
client:only="react"
|
||||
/>
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
66
astro/src/styles/global.css
Normal file
66
astro/src/styles/global.css
Normal file
@ -0,0 +1,66 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.black-dot {
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
}
|
@ -8,27 +8,20 @@
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
posts: Post;
|
||||
users: User;
|
||||
media: Media;
|
||||
couriers: Courier;
|
||||
dispatches: Dispatch;
|
||||
makers: Maker;
|
||||
products: Product;
|
||||
retailers: Retailer;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string;
|
||||
hallo?: string;
|
||||
publishedDate?: string;
|
||||
content?: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
status?: 'draft' | 'published';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
phoneNumber?: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@ -52,3 +45,60 @@ export interface Media {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
export interface Courier {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface Dispatch {
|
||||
id: string;
|
||||
code: string;
|
||||
products: string[] | Product[];
|
||||
courier?: string | User;
|
||||
maker: string | Maker;
|
||||
retailer: string | Retailer;
|
||||
status: 'requested' | 'accepted' | 'archived';
|
||||
departure?: string;
|
||||
arrival?: string;
|
||||
timeSensitive?: boolean;
|
||||
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;
|
||||
}
|
||||
|
6
astro/src/utils.ts
Normal file
6
astro/src/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
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=/";
|
||||
}
|
156
astro/src/utils/hooks.ts
Normal file
156
astro/src/utils/hooks.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import type { User, Node, Retailer, Maker, Product, Dispatch, CreateDispatch } from '../astroTypes';
|
||||
import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { hasAuthCookie } from './authUtils';
|
||||
import { queryClient } from '@/components/App';
|
||||
|
||||
export const API_URL = "https://admin.kios.lumbung.space"
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
const getMakers = async () => {
|
||||
const url = `${API_URL}/api/makers`
|
||||
const response = await axios.get(url);
|
||||
|
||||
const makers: Maker[] = response.data.docs;
|
||||
return makers;
|
||||
|
||||
}
|
||||
|
||||
export const useGetMakers = () => {
|
||||
return useQuery<Maker[]>({
|
||||
queryFn: () => getMakers(),
|
||||
queryKey: ['makers'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
|
||||
const getRetailers = async () => {
|
||||
const url = `${API_URL}/api/retailers`
|
||||
const response = await axios.get(url);
|
||||
|
||||
const retailers: Retailer[] = response.data.docs;
|
||||
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}`
|
||||
const response = await axios.get(url);
|
||||
|
||||
const user: User = response.data.docs;
|
||||
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`
|
||||
|
||||
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.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`
|
||||
const response = await axios.get(url);
|
||||
|
||||
const dispatches: Dispatch[] = response.data.docs;
|
||||
return dispatches;
|
||||
|
||||
}
|
||||
|
||||
export const useGetDispatches = () => {
|
||||
return useQuery<Dispatch[]>({
|
||||
queryFn: () => getDispatches(),
|
||||
queryKey: ['dispatches'],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
|
||||
const createDispatch = async (dispatch: CreateDispatch) => {
|
||||
const url = `${API_URL}/api/dispatches`;
|
||||
return await axios.post(url, dispatch);
|
||||
};
|
||||
|
||||
export const useCreateDispatch = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (dispatch: CreateDispatch) => createDispatch(dispatch),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['dispatches'] });
|
||||
},
|
||||
mutationKey: ["createDispatch"]
|
||||
})
|
||||
};
|
||||
|
||||
const getRetailersByAdminId = async (user: User | undefined) => {
|
||||
if(user === undefined) {
|
||||
console.error("getMyRetailers error: user undefined")
|
||||
return []
|
||||
}
|
||||
const adminId = user.id
|
||||
const url = `${API_URL}/api/retailers`
|
||||
const response = await axios.get(url);
|
||||
|
||||
const retailers: Retailer[] = response.data.docs;
|
||||
|
||||
let myRetailers: Retailer[] = []
|
||||
for (let retailer of retailers) {
|
||||
if(retailer.admins) {
|
||||
for (let admin of retailer.admins) {
|
||||
if(admin.id === adminId) {
|
||||
myRetailers.push(retailer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return myRetailers;
|
||||
|
||||
}
|
||||
|
||||
export const useGetMyRetailers = (user: User | undefined) => {
|
||||
return useQuery<Retailer[]>({
|
||||
queryFn: () => getRetailersByAdminId(user),
|
||||
queryKey: ['myRetailers'],
|
||||
enabled: (user !== undefined)
|
||||
})
|
||||
}
|
@ -1,20 +1,78 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
|
||||
'./styles/**/*.css',
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
fontFamily: {
|
||||
plex: ["Plex", "sans-serif"],
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
DEFAULT: "#111111",
|
||||
light: "#888888",
|
||||
dark: "#222222",
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
safelist: [],
|
||||
plugins: [],
|
||||
};
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
@ -1,10 +1,16 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"types": ["@astrojs/image/client"],
|
||||
"types": [
|
||||
"@astrojs/image/client"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
}
|
4531
astro/yarn.lock
4531
astro/yarn.lock
File diff suppressed because it is too large
Load Diff
88
compose.yml
Normal file
88
compose.yml
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: git.autonomic.zone/ruangrupa/lumbung-kios-astro:latest
|
||||
environment:
|
||||
- PAYLOAD_URL=kios_lumbung_space_app
|
||||
secrets:
|
||||
- mongo_password
|
||||
- payload_secret
|
||||
networks:
|
||||
- proxy
|
||||
- internal
|
||||
deploy:
|
||||
update_config:
|
||||
failure_action: rollback
|
||||
order: start-first
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.${STACK_NAME}-astro.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.${STACK_NAME}-astro.rule=Host(`${DOMAIN}`)"
|
||||
- "traefik.http.routers.${STACK_NAME}-astro.entrypoints=web-secure"
|
||||
- "traefik.http.routers.${STACK_NAME}-astro.tls.certresolver=production"
|
||||
|
||||
payload:
|
||||
image: git.autonomic.zone/ruangrupa/lumbung-kios-payload:latest
|
||||
environment:
|
||||
- "NAME=kios"
|
||||
- "PAYLOAD_URL=${STACK_NAME}-payload"
|
||||
- "PAYLOAD_PORT=3001"
|
||||
- "PAYLOAD_SECRET_FILE=/run/secrets/payload_secret"
|
||||
- "MONGODB_USER=mongo"
|
||||
- "MONGODB_HOST=${STACK_NAME}_mongo"
|
||||
- "MONGODB_PORT=27017"
|
||||
- "MONGODB_PASSWORD_FILE=/run/secrets/mongo_password"
|
||||
- "TOKEN=${TOKEN}"
|
||||
secrets:
|
||||
- mongo_password
|
||||
- payload_secret
|
||||
networks:
|
||||
- proxy
|
||||
- internal
|
||||
deploy:
|
||||
update_config:
|
||||
failure_action: rollback
|
||||
order: start-first
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.${STACK_NAME}-payload.loadbalancer.server.port=3001"
|
||||
# FIXME switch to /admin probably using PathPrefix
|
||||
- "traefik.http.routers.${STACK_NAME}-payload.rule=Host(`admin.${DOMAIN}`)"
|
||||
- "traefik.http.routers.${STACK_NAME}-payload.entrypoints=web-secure"
|
||||
- "traefik.http.routers.${STACK_NAME}-payload.tls.certresolver=production"
|
||||
|
||||
mongo:
|
||||
image: mongo:6.0.5
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
environment:
|
||||
- "MONGO_INITDB_ROOT_USERNAME=mongo"
|
||||
- "MONGO_INITDB_ROOT_PASSWORD_FILE=/run/secrets/mongo_password"
|
||||
secrets:
|
||||
- mongo_password
|
||||
networks:
|
||||
- internal
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
internal:
|
||||
|
||||
secrets:
|
||||
payload_secret:
|
||||
external: true
|
||||
name: ${STACK_NAME}_payload_secret_${SECRET_PAYLOAD_SECRET_VERSION}
|
||||
token:
|
||||
external: true
|
||||
name: ${STACK_NAME}_token_${SECRET_TOKEN_VERSION}
|
||||
mongo_password:
|
||||
external: true
|
||||
name: ${STACK_NAME}_mongo_password_${SECRET_MONGO_PASSWORD_VERSION}
|
||||
|
||||
volumes:
|
||||
mongo:
|
@ -4,6 +4,8 @@ services:
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: astro
|
||||
environment:
|
||||
PAYLOAD_URL: ${PAYLOAD_URL}
|
||||
networks:
|
||||
- front
|
||||
depends_on:
|
||||
|
10
node_modules/.yarn-integrity
generated
vendored
Normal file
10
node_modules/.yarn-integrity
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"systemParams": "linux-x64-108",
|
||||
"modulesFolders": [],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
"lockfileEntries": {},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
@ -26,4 +26,6 @@ COPY --from=build /build/tsconfig.json ./tsconfig.json
|
||||
COPY --from=build /build/dist ./dist
|
||||
COPY --from=build /build/build ./build
|
||||
EXPOSE 3000
|
||||
CMD ["yarn", "serve"]
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["yarn", "serve"]
|
||||
|
32
payload/docker-entrypoint.sh
Executable file
32
payload/docker-entrypoint.sh
Executable file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
file_env "TOKEN"
|
||||
file_env "MONGODB_PASSWORD"
|
||||
file_env "PAYLOAD_SECRET"
|
||||
|
||||
export MONGODB_URI="mongodb://$MONGODB_USER:$MONGODB_PASSWORD@$MONGODB_HOST:$MONGODB_PORT"
|
||||
|
||||
"$@"
|
@ -13,6 +13,7 @@
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts node -r tsconfig-paths/register node_modules/payload/dist/bin/index.js generate:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.17.1",
|
||||
|
20
payload/src/collections/Couriers.ts
Normal file
20
payload/src/collections/Couriers.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
const Couriers: CollectionConfig = {
|
||||
slug: 'couriers',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
export default Couriers;
|
91
payload/src/collections/Dispatches.ts
Normal file
91
payload/src/collections/Dispatches.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
const Dispatches: CollectionConfig = {
|
||||
slug: 'dispatches',
|
||||
admin: {
|
||||
useAsTitle: 'code',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true
|
||||
},
|
||||
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'text',
|
||||
required: true,
|
||||
maxLength: 20,
|
||||
unique: true,
|
||||
admin: {
|
||||
description: "A unique identifier for the dispatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'products',
|
||||
type: 'relationship',
|
||||
relationTo: 'products',
|
||||
hasMany: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'courier',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
hasMany: false,
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'maker',
|
||||
type: 'relationship',
|
||||
relationTo: 'makers',
|
||||
hasMany: false,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'retailer',
|
||||
type: 'relationship',
|
||||
relationTo: 'retailers',
|
||||
hasMany: false,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
hasMany: false,
|
||||
required: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Requested',
|
||||
value: 'requested',
|
||||
},
|
||||
{
|
||||
label: 'Accepted',
|
||||
value: 'accepted',
|
||||
},
|
||||
{
|
||||
label: 'Archived',
|
||||
value: 'archived',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'departure',
|
||||
type: 'date',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'arrival',
|
||||
type: 'date',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'timeSensitive',
|
||||
type: 'checkbox',
|
||||
required: false,
|
||||
defaultValue: false
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Dispatches;
|
51
payload/src/collections/Makers.ts
Normal file
51
payload/src/collections/Makers.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
const Makers: CollectionConfig = {
|
||||
slug: 'makers',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'phoneNumber',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
type: 'point',
|
||||
label: 'Location',
|
||||
required: true,
|
||||
admin: {
|
||||
description: "Get latitude and longitude values for your location with https://www.latlong.net/"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'admins',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
type: 'relationship',
|
||||
relationTo: 'products',
|
||||
hasMany: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
export default Makers;
|
34
payload/src/collections/Products.ts
Normal file
34
payload/src/collections/Products.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
const Products: CollectionConfig = {
|
||||
slug: 'products',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'picture',
|
||||
type: 'relationship',
|
||||
relationTo: 'media',
|
||||
hasMany: false,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'weight',
|
||||
label: 'Weight (kg)',
|
||||
type: 'number',
|
||||
hasMany: false,
|
||||
required: false,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
export default Products;
|
52
payload/src/collections/Retailers.ts
Normal file
52
payload/src/collections/Retailers.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
import { geoPickerField } from "../customFields/geoPicker/field";
|
||||
|
||||
const Retailers: CollectionConfig = {
|
||||
slug: 'retailers',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'phoneNumber',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
type: 'point',
|
||||
label: 'Location',
|
||||
required: true,
|
||||
admin: {
|
||||
description: "Get latitude and longitude values for your location with https://www.latlong.net/"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'admins',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
type: 'relationship',
|
||||
relationTo: 'products',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Retailers;
|
@ -2,18 +2,34 @@ import { CollectionConfig } from 'payload/types';
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
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
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'phoneNumber',
|
||||
type: 'number',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
};
|
||||
|
68
payload/src/customFields/geoPicker/GeoPicker.tsx
Normal file
68
payload/src/customFields/geoPicker/GeoPicker.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
import { useField } from 'payload/components/forms';
|
||||
|
||||
export const GeoPicker: React.FC<{ path: string }> = ({ path }) => {
|
||||
const { value, setValue } = useField<string>({ path });
|
||||
const [longitude, setLongitude] = React.useState(value[0] || 0);
|
||||
const [latitude, setLatitude] = React.useState(value[1] || 0);
|
||||
const [error, setError] = React.useState("")
|
||||
|
||||
const handleCityEnter = async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?format=json&q=${e.target.value}`
|
||||
);
|
||||
const data = await response.json();
|
||||
if (data && data.length > 0) {
|
||||
const { lat, lon } = data[0];
|
||||
setLatitude(lat);
|
||||
setLongitude(lon);
|
||||
setValue([lon, lat]);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(e)
|
||||
console.error('Error fetching geolocation:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLatitudeChange = (e) => {
|
||||
setLatitude(e.target.value);
|
||||
};
|
||||
|
||||
const handleLongitudeChange = (e) => {
|
||||
setLongitude(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='field-type text'>
|
||||
<label className='field-label'>
|
||||
City
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className='field-name'
|
||||
onKeyDown={handleCityEnter}
|
||||
placeholder="Enter city to get coordinates"
|
||||
/>
|
||||
</div>
|
||||
{error != "" &&
|
||||
<p>{error}</p>
|
||||
}
|
||||
<div className="field-type point">
|
||||
<ul className='point__wrap'>
|
||||
<li>
|
||||
<label className='field-label'>Location - Longitude</label>
|
||||
<input id="field-longitude-location" type="number" name="location.longitude" value={longitude} onChange={handleLongitudeChange}></input>
|
||||
</li>
|
||||
<li>
|
||||
<label className='field-label'>Location - Latitude</label>
|
||||
<input id="field-latitude-latitude" type="number" name="location.latitude" value={latitude} onChange={handleLatitudeChange}></input>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
12
payload/src/customFields/geoPicker/field.ts
Normal file
12
payload/src/customFields/geoPicker/field.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { PointField } from 'payload/types';
|
||||
import { GeoPicker } from './GeoPicker';
|
||||
|
||||
export const geoPickerField: PointField = {
|
||||
name: 'Location',
|
||||
type: 'point',
|
||||
admin: {
|
||||
components: {
|
||||
Field: GeoPicker,
|
||||
},
|
||||
},
|
||||
}
|
@ -3,6 +3,11 @@ import path from "path";
|
||||
import Posts from "@/collections/Posts";
|
||||
import Users from "@/collections/Users";
|
||||
import Media from "@/collections/Media";
|
||||
import Couriers from "./collections/Couriers";
|
||||
import Dispatches from "./collections/Dispatches";
|
||||
import Makers from "./collections/Makers";
|
||||
import Products from "./collections/Products";
|
||||
import Retailers from "./collections/Retailers";
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: process.env.PAYLOAD_URL,
|
||||
@ -19,7 +24,20 @@ export default buildConfig({
|
||||
},
|
||||
}),
|
||||
},
|
||||
collections: [Posts, Users, Media],
|
||||
csrf: [
|
||||
'https://kios.lumbung.space',
|
||||
'https://admin.kios.lumbung.space',
|
||||
],
|
||||
collections: [
|
||||
// Posts,
|
||||
Users,
|
||||
Media,
|
||||
Couriers,
|
||||
Dispatches,
|
||||
Makers,
|
||||
Products,
|
||||
Retailers,
|
||||
],
|
||||
typescript: {
|
||||
outputFile: path.resolve("/", "types.ts"),
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
import express from "express";
|
||||
import payload from "payload";
|
||||
import cors from "cors";
|
||||
|
||||
require("dotenv").config();
|
||||
const app = express();
|
||||
@ -17,5 +18,12 @@ payload.init({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const corsOptions = {
|
||||
origin: 'https://kios.lumbung.space',
|
||||
credentials: true
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
app.use("/media", express.static("media"));
|
||||
app.listen(process.env.PAYLOAD_PORT);
|
||||
|
4307
payload/yarn.lock
4307
payload/yarn.lock
File diff suppressed because it is too large
Load Diff
48
sampleResponses/How dispatches work.ts
Normal file
48
sampleResponses/How dispatches work.ts
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
//Dispatch Represents:
|
||||
//- a connection between two locations
|
||||
//- An agreement between a courier, a maker and a retailer to transport goods (Products) from point A (maker) to point B (retailer)
|
||||
//- A dispatch has 4 statuses: offered (by a courier), requested (by a maker/retailer), accepted and archived
|
||||
//- An offered dispatch will not yet have a Product, Maker, or Retailer.
|
||||
//- A requested dispatch will have a Product, Maker and Retailer, but not a courier
|
||||
//- A dispatch is accepted (moved from offered/requested) once all parties have accepted the conditions
|
||||
//- A retailer accepts an offered route by responding with products (courier must then also confirm, but does not provide further data)
|
||||
//- A courier accepts a requested route by responding with a date of arrival (the retailer must then confirm, but does not provide further data)
|
||||
//- A retailer requests a dispatch by requesting a product they want to stock
|
||||
//- A maker requests a dispatch by requesting to stock a product with a retailer
|
||||
|
||||
|
||||
const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const;
|
||||
type DispatchStatus = typeof DISPATCH_STATUS[number];
|
||||
|
||||
type 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;
|
||||
}
|
||||
|
||||
//Courier is just a person (User) and doesn't need to be its own thing. A user is a courier when they accept a dispatch as a courier
|
||||
// Delete the couriers collection
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
productTitle: string;
|
||||
weight: number;
|
||||
img: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
355
sampleResponses/dispatches.json
Normal file
355
sampleResponses/dispatches.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"docs": [
|
||||
{
|
||||
"id": "660a8a0701275c2f8eb88344",
|
||||
"dispatchesCode": "Tropical Tap Water",
|
||||
"products": [
|
||||
{
|
||||
"id": "660a87b201275c2f8eb88302",
|
||||
"productTitle": "Tote Bags",
|
||||
"createdAt": "2024-04-01T10:08:50.417Z",
|
||||
"updatedAt": "2024-04-01T10:08:50.417Z"
|
||||
}
|
||||
],
|
||||
"typeOfTransportation": ["air"],
|
||||
"courier": {
|
||||
"id": "660a899501275c2f8eb8831b",
|
||||
"name": "Daniel Aguilar Ruvalcaba",
|
||||
"startingPoint": "Indonesia",
|
||||
"destination": "Mexico",
|
||||
"departureDate": "2024-04-15T06:00:00.000Z",
|
||||
"arrivalDate": "2024-04-16T06:00:00.000Z",
|
||||
"weightAllowance": 5,
|
||||
"createdAt": "2024-04-01T10:16:53.455Z",
|
||||
"updatedAt": "2024-04-01T10:16:53.455Z"
|
||||
},
|
||||
"timeSensitive": false,
|
||||
"status": ["routeRequested"],
|
||||
"createdAt": "2024-04-01T10:18:47.302Z",
|
||||
"updatedAt": "2024-04-01T10:18:47.302Z"
|
||||
},
|
||||
{
|
||||
"id": "66028d2a01275c2f8eb87e3a",
|
||||
"dispatchesCode": "HQL-571",
|
||||
"products": [
|
||||
{
|
||||
"id": "66028ee601275c2f8eb87e93",
|
||||
"productTitle": "Acts of Departure: Dispatches from The Last Emporium",
|
||||
"createdAt": "2024-03-26T09:01:26.239Z",
|
||||
"updatedAt": "2024-03-26T09:01:26.239Z"
|
||||
}
|
||||
],
|
||||
"startingPoint": {
|
||||
"id": "66028ec901275c2f8eb87e76",
|
||||
"name": "Display Distribute",
|
||||
"location": [114.19163986185993, 22.31656075733971],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-03-26T09:00:57.371Z",
|
||||
"updatedAt": "2024-03-26T09:00:57.371Z"
|
||||
},
|
||||
"endPoint": {
|
||||
"id": "65b0cf25afcaf765bddf4540",
|
||||
"name": "Retailer one",
|
||||
"location": [2.45, 48.9],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:49:41.635Z",
|
||||
"updatedAt": "2024-01-24T08:49:41.635Z"
|
||||
},
|
||||
"typeOfTransportation": ["air"],
|
||||
"courier": {
|
||||
"id": "66028b3801275c2f8eb87e22",
|
||||
"name": "Shuang",
|
||||
"startingPoint": "Chongqing",
|
||||
"destination": "Hong Kong",
|
||||
"departureDate": "2024-03-26T16:00:00.000Z",
|
||||
"arrivalDate": "2024-03-27T16:00:00.000Z",
|
||||
"weightAllowance": 2,
|
||||
"createdAt": "2024-03-26T08:45:44.841Z",
|
||||
"updatedAt": "2024-03-26T09:04:46.105Z"
|
||||
},
|
||||
"status": ["completed"],
|
||||
"createdAt": "2024-03-26T08:54:02.444Z",
|
||||
"updatedAt": "2024-03-26T09:05:05.667Z"
|
||||
},
|
||||
{
|
||||
"id": "65db4a5af1b7d726bba2ebe4",
|
||||
"dispatchesCode": "007",
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"startingPoint": {
|
||||
"id": "65b0cff2afcaf765bddf45a8",
|
||||
"name": "Maker 3",
|
||||
"location": [106, -6.2],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:53:06.945Z",
|
||||
"updatedAt": "2024-01-24T08:53:06.945Z"
|
||||
},
|
||||
"endPoint": {
|
||||
"id": "65b0cf25afcaf765bddf4540",
|
||||
"name": "Retailer one",
|
||||
"location": [2.45, 48.9],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:49:41.635Z",
|
||||
"updatedAt": "2024-01-24T08:49:41.635Z"
|
||||
},
|
||||
"courier": {
|
||||
"id": "65b0ced0afcaf765bddf4503",
|
||||
"name": "Joost the Courier",
|
||||
"createdAt": "2024-01-24T08:48:16.461Z",
|
||||
"updatedAt": "2024-01-24T08:48:16.461Z"
|
||||
},
|
||||
"timeSensitive": true,
|
||||
"status": ["routeRequested"],
|
||||
"createdAt": "2024-02-25T14:10:34.727Z",
|
||||
"updatedAt": "2024-02-25T19:24:29.046Z",
|
||||
"typeOfTransportation": ["car"]
|
||||
},
|
||||
{
|
||||
"id": "65b0d0aeafcaf765bddf4671",
|
||||
"dispatchesCode": "004",
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"startingPoint": {
|
||||
"id": "65b0cfd9afcaf765bddf4596",
|
||||
"name": "Maker Two",
|
||||
"location": [0.128, 51.5],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:52:41.057Z",
|
||||
"updatedAt": "2024-01-24T08:52:41.057Z"
|
||||
},
|
||||
"endPoint": {
|
||||
"id": "65b0cf7eafcaf765bddf4564",
|
||||
"name": "Retailer 3",
|
||||
"location": [9.19, 45.5],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:51:10.008Z",
|
||||
"updatedAt": "2024-01-24T08:51:10.008Z"
|
||||
},
|
||||
"courier": {
|
||||
"id": "65b0ced0afcaf765bddf4503",
|
||||
"name": "Joost the Courier",
|
||||
"createdAt": "2024-01-24T08:48:16.461Z",
|
||||
"updatedAt": "2024-01-24T08:48:16.461Z"
|
||||
},
|
||||
"status": ["routeRequested"],
|
||||
"createdAt": "2024-01-24T08:56:14.210Z",
|
||||
"updatedAt": "2024-01-24T08:56:14.210Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0d099afcaf765bddf4640",
|
||||
"dispatchesCode": "003",
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"startingPoint": {
|
||||
"id": "65b0cff2afcaf765bddf45a8",
|
||||
"name": "Maker 3",
|
||||
"location": [106, -6.2],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:53:06.945Z",
|
||||
"updatedAt": "2024-01-24T08:53:06.945Z"
|
||||
},
|
||||
"endPoint": {
|
||||
"id": "65b0cf6bafcaf765bddf4552",
|
||||
"name": "Retailer two",
|
||||
"location": [-74, 40.7],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:50:51.876Z",
|
||||
"updatedAt": "2024-01-24T08:50:51.876Z"
|
||||
},
|
||||
"courier": {
|
||||
"id": "65b0ced0afcaf765bddf4503",
|
||||
"name": "Joost the Courier",
|
||||
"createdAt": "2024-01-24T08:48:16.461Z",
|
||||
"updatedAt": "2024-01-24T08:48:16.461Z"
|
||||
},
|
||||
"status": ["completed"],
|
||||
"createdAt": "2024-01-24T08:55:53.951Z",
|
||||
"updatedAt": "2024-01-24T08:55:53.951Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0d07cafcaf765bddf460f",
|
||||
"dispatchesCode": "002",
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"startingPoint": {
|
||||
"id": "65b0d00dafcaf765bddf45ba",
|
||||
"name": "Fahad the Artist",
|
||||
"location": [-6.84, 33.9],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:53:33.106Z",
|
||||
"updatedAt": "2024-01-24T08:53:33.106Z"
|
||||
},
|
||||
"endPoint": {
|
||||
"id": "65b0cf25afcaf765bddf4540",
|
||||
"name": "Retailer one",
|
||||
"location": [2.45, 48.9],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:49:41.635Z",
|
||||
"updatedAt": "2024-01-24T08:49:41.635Z"
|
||||
},
|
||||
"courier": {
|
||||
"id": "65b0ced0afcaf765bddf4503",
|
||||
"name": "Joost the Courier",
|
||||
"createdAt": "2024-01-24T08:48:16.461Z",
|
||||
"updatedAt": "2024-01-24T08:48:16.461Z"
|
||||
},
|
||||
"status": ["routeRequested"],
|
||||
"createdAt": "2024-01-24T08:55:24.797Z",
|
||||
"updatedAt": "2024-01-24T08:55:24.797Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0d05cafcaf765bddf45de",
|
||||
"dispatchesCode": "Random-string-maybe-better-to-use-the-ID",
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"startingPoint": {
|
||||
"id": "65b0cfd9afcaf765bddf4596",
|
||||
"name": "Maker Two",
|
||||
"location": [0.128, 51.5],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:52:41.057Z",
|
||||
"updatedAt": "2024-01-24T08:52:41.057Z"
|
||||
},
|
||||
"endPoint": {
|
||||
"id": "65b0cf7eafcaf765bddf4564",
|
||||
"name": "Retailer 3",
|
||||
"location": [9.19, 45.5],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:51:10.008Z",
|
||||
"updatedAt": "2024-01-24T08:51:10.008Z"
|
||||
},
|
||||
"courier": {
|
||||
"id": "65b0ced0afcaf765bddf4503",
|
||||
"name": "Joost the Courier",
|
||||
"createdAt": "2024-01-24T08:48:16.461Z",
|
||||
"updatedAt": "2024-01-24T08:48:16.461Z"
|
||||
},
|
||||
"status": ["inTransit"],
|
||||
"createdAt": "2024-01-24T08:54:52.811Z",
|
||||
"updatedAt": "2024-01-24T08:54:52.811Z"
|
||||
}
|
||||
],
|
||||
"totalDocs": 7,
|
||||
"limit": 10,
|
||||
"totalPages": 1,
|
||||
"page": 1,
|
||||
"pagingCounter": 1,
|
||||
"hasPrevPage": false,
|
||||
"hasNextPage": false,
|
||||
"prevPage": null,
|
||||
"nextPage": null
|
||||
}
|
88
sampleResponses/makers.json
Normal file
88
sampleResponses/makers.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"docs": [
|
||||
{
|
||||
"id": "66028ec901275c2f8eb87e76",
|
||||
"name": "Display Distribute",
|
||||
"location": [114.19163986185993, 22.31656075733971],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-03-26T09:00:57.371Z",
|
||||
"updatedAt": "2024-03-26T09:00:57.371Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0d00dafcaf765bddf45ba",
|
||||
"name": "Fahad the Artist",
|
||||
"location": [-6.84, 33.9],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:53:33.106Z",
|
||||
"updatedAt": "2024-01-24T08:53:33.106Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0cff2afcaf765bddf45a8",
|
||||
"name": "Maker 3",
|
||||
"location": [106, -6.2],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:53:06.945Z",
|
||||
"updatedAt": "2024-01-24T08:53:06.945Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0cfd9afcaf765bddf4596",
|
||||
"name": "Maker Two",
|
||||
"location": [0.128, 51.5],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:52:41.057Z",
|
||||
"updatedAt": "2024-01-24T08:52:41.057Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0cfbbafcaf765bddf4584",
|
||||
"name": "Maker one",
|
||||
"location": [67, 24.86],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:52:11.469Z",
|
||||
"updatedAt": "2024-01-24T08:52:11.469Z"
|
||||
}
|
||||
],
|
||||
"totalDocs": 5,
|
||||
"limit": 10,
|
||||
"totalPages": 1,
|
||||
"page": 1,
|
||||
"pagingCounter": 1,
|
||||
"hasPrevPage": false,
|
||||
"hasNextPage": false,
|
||||
"prevPage": null,
|
||||
"nextPage": null
|
||||
}
|
31
sampleResponses/products.json
Normal file
31
sampleResponses/products.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"docs": [
|
||||
{
|
||||
"id": "660a87b201275c2f8eb88302",
|
||||
"productTitle": "Tote Bags",
|
||||
"createdAt": "2024-04-01T10:08:50.417Z",
|
||||
"updatedAt": "2024-04-01T10:08:50.417Z"
|
||||
},
|
||||
{
|
||||
"id": "66028ee601275c2f8eb87e93",
|
||||
"productTitle": "Acts of Departure: Dispatches from The Last Emporium",
|
||||
"createdAt": "2024-03-26T09:01:26.239Z",
|
||||
"updatedAt": "2024-03-26T09:01:26.239Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"totalDocs": 3,
|
||||
"limit": 10,
|
||||
"totalPages": 1,
|
||||
"page": 1,
|
||||
"pagingCounter": 1,
|
||||
"hasPrevPage": false,
|
||||
"hasNextPage": false,
|
||||
"prevPage": null,
|
||||
"nextPage": null
|
||||
}
|
58
sampleResponses/retailers.json
Normal file
58
sampleResponses/retailers.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"docs": [
|
||||
{
|
||||
"id": "65b0cf7eafcaf765bddf4564",
|
||||
"name": "Retailer 3",
|
||||
"location": [9.19, 45.5],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:51:10.008Z",
|
||||
"updatedAt": "2024-01-24T08:51:10.008Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0cf6bafcaf765bddf4552",
|
||||
"name": "Retailer two",
|
||||
"location": [-74, 40.7],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:50:51.876Z",
|
||||
"updatedAt": "2024-01-24T08:50:51.876Z"
|
||||
},
|
||||
{
|
||||
"id": "65b0cf25afcaf765bddf4540",
|
||||
"name": "Retailer one",
|
||||
"location": [2.45, 48.9],
|
||||
"products": [
|
||||
{
|
||||
"id": "65b0cefaafcaf765bddf4527",
|
||||
"productTitle": "Product one",
|
||||
"createdAt": "2024-01-24T08:48:58.369Z",
|
||||
"updatedAt": "2024-01-24T08:48:58.369Z"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-01-24T08:49:41.635Z",
|
||||
"updatedAt": "2024-01-24T08:49:41.635Z"
|
||||
}
|
||||
],
|
||||
"totalDocs": 3,
|
||||
"limit": 10,
|
||||
"totalPages": 1,
|
||||
"page": 1,
|
||||
"pagingCounter": 1,
|
||||
"hasPrevPage": false,
|
||||
"hasNextPage": false,
|
||||
"prevPage": null,
|
||||
"nextPage": null
|
||||
}
|
Reference in New Issue
Block a user