Compare commits

...

46 Commits

Author SHA1 Message Date
2c30f4a345 Remove trailing slash on API url
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2024-04-08 11:22:39 +02:00
918c5d0214 Update cors origin
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-08 11:17:27 +02:00
324ad812e2 Set csrf domains
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-08 10:23:38 +02:00
1f8142a82f Hardcode correct api url for deploy
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-07 22:23:21 +02:00
cf33c001db Add descriptions to fields
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-07 22:21:20 +02:00
f30199e5f8 Remove console logs
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-07 19:43:08 +02:00
9be7ae81ae Complete basic request dispatch flow
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-07 19:40:38 +02:00
00cf0d9905 Auth works but not with cookie
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-06 17:43:57 +02:00
3b5ac81cc3 Auth flow functional 2024-04-06 17:28:36 +02:00
98e967f7c0 Remove field courierTo
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-05 22:42:31 +02:00
d0a9dd6a13 Display products on dispatch
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-05 22:38:48 +02:00
e0bb3a349d Create rudimentary catalogue 2024-04-05 22:24:14 +02:00
1447d8fc17 Make custom button 2024-04-05 19:00:54 +02:00
0645bbd877 Basic node information mostly done 2024-04-05 18:41:53 +02:00
ed420d5ff1 Make map nodes selectable and stylish
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-05 15:03:18 +02:00
12c43e9a00 Style dispatches 2024-04-05 13:38:19 +02:00
1a2d143c4f Fix dispatches 2024-04-05 13:07:17 +02:00
f059ca37da Move posts page
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-05 10:59:48 +02:00
3b359dd3cc Reorganize pages/components
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-05 10:59:09 +02:00
318db9322f Update readme
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-05 09:45:04 +02:00
3wc
9d3e600eae Fix depends_on
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-04 12:00:57 -03:00
3wc
2184c11448 Add docker stack auto-deployment to lumbung server
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2024-04-04 11:59:14 -03:00
3wc
a57206f8b2 Set nginx mime types
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-04 11:50:41 -03:00
3wc
35afa4a755 Make docker entrypoint executable
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-04 11:39:11 -03:00
3wc
a1f3187bc7 Move entrypoint to right location 😳
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-04 11:31:48 -03:00
3wc
a5cac66c3f Further entrypoint tweak
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-04 11:30:34 -03:00
3wc
509b6b5e8b Add docker entrypoint for loading secrets
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-04 11:29:05 -03:00
3wc
7f508fc04b Switch to ruangrupa org for image publishing
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-04 10:49:40 -03:00
974cee1cf4 Disable pre-ssg getPosts
Some checks reported errors
continuous-integration/drone/push Build was killed
2024-04-04 15:24:43 +02:00
6240a4104a Fix client:only directive
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-04 14:58:18 +02:00
f51f6daad3 Add payload url to astro docker-compose
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-04 14:46:29 +02:00
3wc
c72685679c Re-add drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-04 09:31:36 -03:00
15700ae304 Update types 2024-04-04 14:28:24 +02:00
174a163cd3 Restructure types for retailer-centric flow 2024-04-04 12:45:26 +02:00
2ebb81a981 Basic map works 2024-04-03 19:39:05 +02:00
225d45307a Fix queries 2024-04-03 19:11:33 +02:00
cb26b8edcd QueryClient setup 2024-04-03 16:23:28 +02:00
4a95f6e2d1 Start kios map vanilla -> react conversion 2024-04-03 15:31:56 +02:00
d8438e2d84 Add sample form 2024-04-03 14:23:08 +02:00
95243557c6 Update react/ts version 2024-04-03 14:22:56 +02:00
4be030ec28 Import payload config 2024-04-03 12:18:03 +02:00
816eac8470 Upgrade to astro v4 2024-04-02 16:28:04 +02:00
37d0359dae Add shadcn: form & input 2024-04-02 15:26:21 +02:00
75fe3cdcf5 Add react 2024-04-02 14:16:28 +02:00
dc4618383e Include global styles 2024-04-02 13:22:20 +02:00
2c79904102 Add shadcn 2024-04-02 13:08:24 +02:00
60 changed files with 7654 additions and 4138 deletions

51
.drone.yml Normal file
View 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

View File

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

View File

@ -0,0 +1 @@
PAYLOAD_URL=http://localhost:3001

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 5.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

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

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

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

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

View 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='&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
/>
{(makers && !isLoadingMakers) &&
<LayerGroup>
{makers.map((maker: any, index: number) => (
<Marker
eventHandlers={{
click: () => handleSelectNode(maker.id, "maker")
}}
key={maker.id}
position={[locationSwitcharoo(maker.location)[0], locationSwitcharoo(maker.location)[1]]}
icon={selectedNode.id === maker.id ? selectedDotIcon : blackDotIcon}
>
{/* <Popup>{maker.name}</Popup> */}
</Marker>
))}
</LayerGroup>
}
{(retailers && !isLoadingRetailers) &&
<LayerGroup>
{retailers.map((retailer: any, index: number) => (
<Marker
eventHandlers={{
click: () => handleSelectNode(retailer.id, "retailer")
}}
key={retailer.id}
position={[locationSwitcharoo(retailer.location)[0], locationSwitcharoo(retailer.location)[1]]}
icon={selectedNode.id === retailer.id ? selectedDotIcon : blackDotIcon}
>
{/* <Popup>{retailer.name}</Popup> */}
</Marker>
))}
</LayerGroup>
}
{(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>
);
};

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

View File

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

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

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

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

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

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

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

View File

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

View File

@ -0,0 +1,7 @@
---
import { AddRetailerForm } from "@/components/AddRetailerForm";
import BaseLayout from "@/layouts/BaseLayout.astro";
---
<BaseLayout title="Astroad">
<AddRetailerForm client:visible></AddRetailerForm>
</BaseLayout>

View File

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

View 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%;
}

View File

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

View File

@ -0,0 +1,12 @@
export const hasAuthCookie = () => {
const matches = document.cookie.match(/^(.*;)?\s*payload-token\s*=\s*[^;]+(.*)?$/) || []
const hasMatch = matches.length > 0
return hasMatch;
}
export const setAuthCookie = (value: string, expirationDays: number) => {
const date = new Date();
date.setTime(date.getTime() + (expirationDays * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = "payload-token=" + value + ";" + expires + ";path=/";
}

156
astro/src/utils/hooks.ts Normal file
View 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)
})
}

View File

@ -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")],
}

View File

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

File diff suppressed because it is too large Load Diff

88
compose.yml Normal file
View 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:

View File

@ -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
View File

@ -0,0 +1,10 @@
{
"systemParams": "linux-x64-108",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}

View File

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

View File

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

View 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;

View 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;

View 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;

View 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;

View 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;

View File

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

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

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

View File

@ -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"),
},

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

4
yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1