diff --git a/README.md b/README.md index 6e3716b..40ce6aa 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ # Project Description -This is the dashboard code for easily interacting with [Juno](https://github.com/GTBitsOfGood/juno), [Bits of Good](https://bitsofgood.org/)'s central microservice architecture. See the main repository for more details. +This is the dashboard code for easily interacting with [Juno](https://github.com/GTBitsOfGood/juno), [Bits of Good](https://bitsofgood.org/)'s central microservice architecture. See the main repository for more details. ## Prequisites @@ -32,7 +32,7 @@ Installing all needed packages: bun install ``` -## Development +## Development ### Running locally @@ -66,6 +66,6 @@ Now, you should be able to enter API request fields. The username and password s or if you are using a different project, then replace with that project name. -### Components +### Components This repository uses [shadcn/ui](https://ui.shadcn.com/) for streamlining component development. diff --git a/bun.lock b/bun.lock index 3ceb367..7b0e3b4 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -193,6 +194,8 @@ "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -211,7 +214,7 @@ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], "@radix-ui/react-label": ["@radix-ui/react-label@2.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw=="], @@ -243,6 +246,8 @@ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], @@ -1067,20 +1072,38 @@ "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-collapsible/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-collapsible/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-collapsible/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], - "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + "@radix-ui/react-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + "@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-popper/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], @@ -1093,8 +1116,12 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + "@radix-ui/react-roving-focus/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + "@radix-ui/react-select/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-select/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], @@ -1107,6 +1134,8 @@ "@radix-ui/react-toast/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + "@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-use-controllable-state/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], @@ -1165,6 +1194,20 @@ "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + + "@radix-ui/react-menu/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + + "@radix-ui/react-popover/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + + "@radix-ui/react-tooltip/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "cmdk/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], diff --git a/components.json b/components.json index 0e8b633..405a08c 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/next.config.ts b/next.config.ts index db0a372..cb651cd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/package.json b/package.json index 5feba68..ce16800 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", diff --git a/src/app/(dashboard)/projects/[projectId]/services/email/configurations/page.tsx b/src/app/(dashboard)/projects/[projectId]/services/email/configurations/page.tsx new file mode 100644 index 0000000..32d3aa4 --- /dev/null +++ b/src/app/(dashboard)/projects/[projectId]/services/email/configurations/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { EmailDomainTable } from "@/components/emailDomainTable/emailDomainTable"; +import { EmailSenderTable } from "@/components/emailSenderTable/emailSenderTable"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { getProjectById } from "@/lib/project"; +import { getEmailConfig } from "@/lib/settings"; +import { useQuery } from "@tanstack/react-query"; +import { ProjectResponse } from "juno-sdk/build/main/internal/index"; +import { Mail, Settings } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +const EmailConfigurationsPage = () => { + const { projectId } = useParams<{ projectId: string }>(); + + const [hasEmailConfig, setHasEmailConfig] = useState(null); + const [emailConfigLoading, setEmailConfigLoading] = useState(true); + + const { + isLoading, + isError, + data: project, + error, + } = useQuery({ + queryKey: ["project", projectId], + queryFn: async () => { + const result = await getProjectById(Number(projectId)); + if (!result.success) { + throw new Error(result.error); + } + return result.project; + }, + }); + + useEffect(() => { + if (isError) { + toast.error("Error", { + description: `Failed to fetch project: ${JSON.stringify(error)}`, + }); + } + }, [isError, error]); + + useEffect(() => { + const loadEmailConfig = async () => { + try { + const configRes = await getEmailConfig(String(projectId)); + if (configRes) { + setHasEmailConfig(configRes); + } + } catch (e) { + console.error("Error loading email config:", e); + toast.error("Error loading email config", { + description: "Please try again later", + }); + } finally { + setEmailConfigLoading(false); + } + }; + + loadEmailConfig(); + }, [projectId]); + + const breadcrumb = ( + + + + Projects + + + + + {isLoading ? "****" : (project?.name ?? "Unknown")} + + + + + + Email + + + + + Configurations + + + + ); + + if (emailConfigLoading) { + return ( +
+ {breadcrumb} + +

Email Configurations

+
+
+
+
+
+ ); + } + + if (!hasEmailConfig) { + return ( +
+ {breadcrumb} + +

Email Configurations

+ + +
+ + No Email Configuration +
+
+ +

+ Configure your email service first to manage senders and domains. +

+ +
+
+
+ ); + } + + return ( +
+ {breadcrumb} + +
+ + +
+
+ ); +}; + +export default EmailConfigurationsPage; diff --git a/src/components/emailDomainTable/columns.tsx b/src/components/emailDomainTable/columns.tsx new file mode 100644 index 0000000..25324d6 --- /dev/null +++ b/src/components/emailDomainTable/columns.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { ColumnDef } from "@tanstack/react-table"; + +export type EmailDomainColumn = { + id: number; + domain: string; + subdomain?: string; + valid: boolean; +}; + +export const emailDomainColumns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + /> + ), + size: 50, + }, + { + accessorKey: "domain", + header: "Domain", + }, + { + accessorKey: "subdomain", + header: "Subdomain", + cell: ({ row }) => { + return row.original.subdomain || "-"; + }, + }, + { + accessorKey: "valid", + header: "Status", + cell: ({ row }) => { + const valid = row.original.valid; + return ( + + {valid ? "Verified" : "Pending"} + + ); + }, + size: 100, + }, + { + accessorKey: "id", + header: "SendGrid ID", + size: 100, + }, +]; diff --git a/src/components/emailDomainTable/emailDomainTable.tsx b/src/components/emailDomainTable/emailDomainTable.tsx new file mode 100644 index 0000000..64fc439 --- /dev/null +++ b/src/components/emailDomainTable/emailDomainTable.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { + EmailDomainColumn, + emailDomainColumns, +} from "@/components/emailDomainTable/columns"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { getEmailDomains } from "@/lib/settings"; +import { registerJunoDomain } from "@/lib/sdkUtils"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { BaseTable } from "../baseTable"; +import AddEmailDomainForm from "../forms/AddEmailDomainForm"; +import { DialogHeader } from "../ui/dialog"; + +interface EmailDomainTableProps { + projectId: string; +} + +export function EmailDomainTable({ projectId }: EmailDomainTableProps) { + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + + const queryClient = useQueryClient(); + const { + isLoading, + isError, + data: domainsResult, + error, + } = useQuery({ + queryKey: ["emailDomains", projectId], + queryFn: async () => { + return await getEmailDomains(projectId); + }, + }); + + useEffect(() => { + if (isError) { + toast.error("Error", { + description: `Failed to fetch email domains: ${JSON.stringify(error)}`, + }); + } + if (domainsResult && !domainsResult.success && domainsResult.error) { + toast.error("Error", { + description: domainsResult.error, + }); + } + }, [isError, error, domainsResult]); + + const domainRowData: EmailDomainColumn[] = (domainsResult?.domains ?? []).map( + (domain: EmailDomainColumn) => ({ + id: domain.id, + domain: domain.domain ?? "", + subdomain: domain.subdomain, + valid: domain.valid ?? false, + }), + ); + + const addDomainHandler = useMutation({ + mutationFn: async (options: { domain: string; subdomain?: string }) => { + const result = await registerJunoDomain( + options.domain, + options.subdomain, + ); + if (!result.success) { + throw new Error(result.message); + } + return result; + }, + onSuccess: () => { + toast.success("Success", { + description: "Successfully registered domain.", + }); + queryClient.invalidateQueries({ + queryKey: ["emailDomains", projectId], + }); + }, + onSettled: () => { + setIsAddDialogOpen(false); + }, + onError: (error) => + toast.error( + `An error occurred while registering domain: ${error.message}`, + ), + }); + + return ( +
+ + + + Register Email Domain + + Register a new domain with SendGrid for email authentication + + + addDomainHandler.mutate(options)} + /> + + + +

Email Domains

+ { + setIsAddDialogOpen(true); + }} + /> +
+ ); +} diff --git a/src/components/emailSenderTable/columns.tsx b/src/components/emailSenderTable/columns.tsx new file mode 100644 index 0000000..f418e4e --- /dev/null +++ b/src/components/emailSenderTable/columns.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { ColumnDef } from "@tanstack/react-table"; + +export type EmailSenderColumn = { + id: number; + nickname: string; + fromEmail: string; + fromName: string; + replyTo: string; + address: string; + city: string; + state: string; + country: string; + zip: string; + verified: boolean; + locked: boolean; +}; + +export const emailSenderColumns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + /> + ), + size: 50, + }, + { + accessorKey: "nickname", + header: "Nickname", + }, + { + accessorKey: "fromEmail", + header: "From Email", + }, + { + accessorKey: "fromName", + header: "From Name", + }, + { + accessorKey: "replyTo", + header: "Reply To", + }, + { + accessorKey: "verified", + header: "Status", + cell: ({ row }) => { + const verified = row.original.verified; + return ( + + {verified ? "Verified" : "Pending"} + + ); + }, + size: 100, + }, +]; diff --git a/src/components/emailSenderTable/emailSenderTable.tsx b/src/components/emailSenderTable/emailSenderTable.tsx new file mode 100644 index 0000000..0cb70d6 --- /dev/null +++ b/src/components/emailSenderTable/emailSenderTable.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { + EmailSenderColumn, + emailSenderColumns, +} from "@/components/emailSenderTable/columns"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { getEmailSenders } from "@/lib/settings"; +import { registerJunoSenderAddress } from "@/lib/sdkUtils"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { BaseTable } from "../baseTable"; +import AddEmailSenderForm from "../forms/AddEmailSenderForm"; +import { DialogHeader } from "../ui/dialog"; + +interface EmailSenderTableProps { + projectId: string; +} + +export function EmailSenderTable({ projectId }: EmailSenderTableProps) { + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + + const queryClient = useQueryClient(); + const { + isLoading, + isError, + data: sendersResult, + error, + } = useQuery({ + queryKey: ["emailSenders", projectId], + queryFn: async () => { + return await getEmailSenders(projectId); + }, + }); + + useEffect(() => { + if (isError) { + toast.error("Error", { + description: `Failed to fetch email senders: ${JSON.stringify(error)}`, + }); + } + if (sendersResult && !sendersResult.success && sendersResult.error) { + toast.error("Error", { + description: sendersResult.error, + }); + } + }, [isError, error, sendersResult]); + + const senderRowData: EmailSenderColumn[] = (sendersResult?.senders ?? []).map( + (sender: EmailSenderColumn) => ({ + id: sender.id, + nickname: sender.nickname ?? "", + fromEmail: sender.fromEmail ?? "", + fromName: sender.fromName ?? "", + replyTo: sender.replyTo ?? "", + address: sender.address ?? "", + city: sender.city ?? "", + state: sender.state ?? "", + country: sender.country ?? "", + zip: sender.zip ?? "", + verified: sender.verified ?? false, + locked: sender.locked ?? false, + }), + ); + + const addSenderHandler = useMutation({ + mutationFn: async (options: { + email: string; + name: string; + replyTo: string; + nickname: string; + address: string; + city: string; + state: string; + zip: string; + country: string; + }) => { + const result = await registerJunoSenderAddress( + options.email, + options.name, + options.replyTo || undefined, + options.nickname, + options.address, + options.city, + options.state, + options.zip, + options.country, + ); + if (!result.success) { + throw new Error(result.message); + } + return result; + }, + onSuccess: () => { + toast.success("Success", { + description: "Successfully registered sender.", + }); + queryClient.invalidateQueries({ + queryKey: ["emailSenders", projectId], + }); + }, + onSettled: () => { + setIsAddDialogOpen(false); + }, + onError: (error) => + toast.error( + `An error occurred while registering sender: ${error.message}`, + ), + }); + + return ( +
+ + + + Register Email Sender + + Register a new verified sender with SendGrid + + + addSenderHandler.mutate(options)} + /> + + + +

Email Senders

+ { + setIsAddDialogOpen(true); + }} + /> +
+ ); +} diff --git a/src/components/forms/AddEmailDomainForm.tsx b/src/components/forms/AddEmailDomainForm.tsx new file mode 100644 index 0000000..e58184c --- /dev/null +++ b/src/components/forms/AddEmailDomainForm.tsx @@ -0,0 +1,91 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Button } from "../ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; +import { Input } from "../ui/input"; + +const addEmailDomainSchema = z.object({ + domain: z.string().nonempty("Domain is required"), + subdomain: z.string().optional(), +}); + +type AddEmailDomainFormProps = { + isPending: boolean; + onAddDomain: (options: { domain: string; subdomain?: string }) => void; +}; + +const AddEmailDomainForm = ({ + isPending, + onAddDomain, +}: AddEmailDomainFormProps) => { + const form = useForm({ + resolver: zodResolver(addEmailDomainSchema), + defaultValues: { + domain: "", + subdomain: "", + }, + }); + + return ( +
+ + onAddDomain({ + domain: data.domain, + subdomain: data.subdomain || undefined, + }), + )} + className="space-y-4 rounded-lg" + > + ( + + Domain + + + + + The domain you want to authenticate for sending emails + + + + )} + /> + ( + + Subdomain (optional) + + + + + Optional subdomain for the authenticated domain + + + + )} + /> + + + + ); +}; + +export default AddEmailDomainForm; diff --git a/src/components/forms/AddEmailSenderForm.tsx b/src/components/forms/AddEmailSenderForm.tsx new file mode 100644 index 0000000..bb25024 --- /dev/null +++ b/src/components/forms/AddEmailSenderForm.tsx @@ -0,0 +1,222 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Button } from "../ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form"; +import { Input } from "../ui/input"; + +const addEmailSenderSchema = z.object({ + email: z + .string() + .min(1, "Email is required") + .email("Must be a valid email address"), + name: z.string().min(1, "Name is required"), + replyTo: z.string().email("Must be a valid email address").or(z.literal("")), + nickname: z.string().min(1, "Nickname is required"), + address: z.string().min(1, "Address is required"), + city: z.string().min(1, "City is required"), + state: z.string().min(1, "State is required"), + zip: z.string().min(1, "Zip code is required"), + country: z.string().min(1, "Country is required"), +}); + +type AddEmailSenderFormProps = { + isPending: boolean; + onAddSender: (options: { + email: string; + name: string; + replyTo: string; + nickname: string; + address: string; + city: string; + state: string; + zip: string; + country: string; + }) => void; +}; + +const AddEmailSenderForm = ({ + isPending, + onAddSender, +}: AddEmailSenderFormProps) => { + const form = useForm({ + resolver: zodResolver(addEmailSenderSchema), + defaultValues: { + email: "", + name: "", + replyTo: "", + nickname: "", + address: "", + city: "", + state: "", + zip: "", + country: "", + }, + }); + + return ( +
+ + onAddSender({ + email: data.email!, + name: data.name!, + replyTo: data.replyTo ?? "", + nickname: data.nickname!, + address: data.address!, + city: data.city!, + state: data.state!, + zip: data.zip!, + country: data.country!, + }), + )} + className="space-y-4 rounded-lg" + > +
+ ( + + From Email + + + + + + )} + /> + ( + + From Name + + + + + + )} + /> +
+
+ ( + + Reply To + + + + + Defaults to From Email if empty + + + + )} + /> + ( + + Nickname + + + + Only visible in SendGrid + + + )} + /> +
+ ( + + Address + + + + + + )} + /> +
+ ( + + City + + + + + + )} + /> + ( + + State + + + + + + )} + /> +
+
+ ( + + Zip Code + + + + + + )} + /> + ( + + Country + + + + + + )} + /> +
+ + + + ); +}; + +export default AddEmailSenderForm; diff --git a/src/components/sidebar/project-sidebar.tsx b/src/components/sidebar/project-sidebar.tsx index 7315d65..471126c 100644 --- a/src/components/sidebar/project-sidebar.tsx +++ b/src/components/sidebar/project-sidebar.tsx @@ -2,6 +2,7 @@ import { ChevronDown, + ChevronRight, Files, FolderKanban, LayoutDashboard, @@ -9,6 +10,8 @@ import { Settings, Shield, User, + BarChart3, + Cog, } from "lucide-react"; import { @@ -21,6 +24,9 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, } from "@/components/ui/sidebar"; import { getProjects } from "@/lib/sdkActions"; import { useEffect, useState } from "react"; @@ -33,6 +39,11 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@radix-ui/react-collapsible"; type ProjectSidebarProps = { projectId: number; @@ -98,7 +109,7 @@ export function ProjectSidebar({ } }, [user]); - // Menu items. + // Menu items without Email (handled separately with sub-items). const items = [ { title: "Dashboard", @@ -110,11 +121,6 @@ export function ProjectSidebar({ url: `/projects/${projectId}/users`, icon: User, }, - { - title: "Email", - url: `/projects/${projectId}/services/email`, - icon: Mail, - }, { title: "Files", url: `/projects/${projectId}/services/files`, @@ -127,6 +133,19 @@ export function ProjectSidebar({ }, ]; + const emailSubItems = [ + { + title: "Analytics", + url: `/projects/${projectId}/services/email`, + icon: BarChart3, + }, + { + title: "Configurations", + url: `/projects/${projectId}/services/email/configurations`, + icon: Cog, + }, + ]; + return ( @@ -188,7 +207,44 @@ export function ProjectSidebar({ Application - {items.map((item) => ( + {items.slice(0, 2).map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + + Email + + + + + + {emailSubItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + {items.slice(2).map((item) => ( diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 9335012..dce5be1 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -242,6 +242,74 @@ export async function createAnalyticsConfig( return JSON.parse(JSON.stringify(analyticsConfig)); } +export async function getEmailSenders(projectId: string) { + const session = await getSession(); + if (!session) { + return { success: false, error: "Unauthorized", senders: [] }; + } + + if (!hasProjectAccess(session.user, Number(projectId))) { + return { + success: false, + error: "You don't have access to this project", + senders: [], + }; + } + + const junoClient = getJunoInstance(); + try { + const result = await junoClient.email.getSenders({ + userJwt: session.jwt, + projectId: projectId, + }); + return { + success: true, + senders: JSON.parse(JSON.stringify(result.senders ?? [])), + }; + } catch (error) { + console.error("Error fetching email senders:", error); + return { + success: false, + error: "Failed to fetch email senders", + senders: [], + }; + } +} + +export async function getEmailDomains(projectId: string) { + const session = await getSession(); + if (!session) { + return { success: false, error: "Unauthorized", domains: [] }; + } + + if (!hasProjectAccess(session.user, Number(projectId))) { + return { + success: false, + error: "You don't have access to this project", + domains: [], + }; + } + + const junoClient = getJunoInstance(); + try { + const result = await junoClient.email.getDomains({ + userJwt: session.jwt, + projectId: projectId, + }); + return { + success: true, + domains: JSON.parse(JSON.stringify(result.domains ?? [])), + }; + } catch (error) { + console.error("Error fetching email domains:", error); + return { + success: false, + error: "Failed to fetch email domains", + domains: [], + }; + } +} + export async function getEmailAnalytics( projectId: string, options?: { diff --git a/tsconfig.json b/tsconfig.json index cafab87..7e2337f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -26,13 +22,6 @@ "@/*": ["./src/*"] } }, - "include": [ - "next-env.d.ts", - ".next/types/**/*.ts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] }