4 changed files with 386 additions and 1 deletions
+5
View File
@@ -18,6 +18,8 @@ import Volcano from "@/pages/volcano/Volcano.jsx";
import GlobalMap from "./pages/maps/GlobalMap.jsx";
import Admin from "./pages/admin/Admin.jsx";
import MyDashboard from "./pages/home/tabs/MyDashboard.jsx";
import Settings from "./pages/auth/Settings.jsx";
import WebAdmin from "./pages/auth/WebAdmin.jsx";
function AppFoundation() {
const recipe = useRecipe({ key: "dashboard" });
@@ -81,6 +83,9 @@ function App() {
<Route path="daily" element={<Placeholder name="Daily Activity" />} />
</Route>
<Route path="/settings" element={<Settings />} />
<Route path="/web-admin" element={<WebAdmin />} />
<Route path="/global-map" element={<GlobalMap />} />
<Route path="/regional-map" element={<RegionalMap />} />
<Route path="/volcano" element={<Volcano />} />
+131
View File
@@ -0,0 +1,131 @@
import { Flex, Text, Input, Field, Button, Separator, NativeSelect, Image } from '@chakra-ui/react';
import useAuthStore from "../../store/authStore.js";
import { useForm } from "react-hook-form";
import { useState } from "react";
export default function Settings() {
const { user } = useAuthStore();
const { register, handleSubmit, errors } = useForm();
const [value, setValue] = useState(undefined);
const updateProfile = useAuthStore((state) => state.updateProfile);
const onSubmit = (data, event) => {
// TODO: check for identical usernames
event.preventDefault();
console.log(data);
if (data.username && data.username.length !== "") updateProfile(data);
}
const ivansRequest = () => {
console.log(user.username, "requested ivans permissions");
// TODO: log users request in database here which will then display request in web-admin page
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction="column" w="100%" h="100%" p="15rem" pt="3rem" gap="5rem">
{/* CONTENT */}
<Flex direction="column" w="100%" h="100%" gap={4}>
{/* header */}
<Text fontWeight="semibold" fontSize="xl">Settings</Text>
<Separator borderColor="gray.300"/>
{/* personal info */}
<Flex direction="column" w="100%" gap={4}>
<Text fontWeight="semibold" fontSize="lg">Personal Information</Text>
{/* profile picture & name */}
<Flex direction="row" w="100%" align="center" justify="space-between" gap={5}>
<Flex direction="row" w="100%" align="center" gap={5}>
<Image borderRadius="full" boxSize="100px" src="https://i.pravatar.cc/300?img=10" />
<Flex direction="column">
<Text fontWeight="semibold">Name</Text>
<Text pl={2}>{user.firstName || "first name"}, {user.lastName || "last name"}</Text>
</Flex>
</Flex>
<Button variant="subtle" size="sm">Update photo</Button>
</Flex>
<Flex direction="row" w="100%" gap={5}>
<Field.Root>
<Field.Label fontWeight="semibold">Username</Field.Label>
<Input {...register("username")} placeholder={user.username} variant="subtle" />
</Field.Root>
<Field.Root>
<Field.Label fontWeight="semibold">Email</Field.Label>
<Input {...register("email")} type="email" placeholder={user.email || "example@usgs.gov"} variant="subtle" />
</Field.Root>
</Flex>
<Flex direction="row" w="100%" gap={5}>
<Field.Root pb={8}>
<Field.Label fontWeight="semibold">Descipline</Field.Label>
<NativeSelect.Root size="sm" variant="subtle">
<NativeSelect.Field
w="100%"
h="2.5rem"
placeholder="Select option"
{...register("discipline")}
>
<option value="RS">Remote Sensing</option>
<option value="CS">Computer Science</option>
<option value="Engineering">Engineering</option>
<option value="HR">Human Resources (HR)</option>
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
<Field.ErrorText></Field.ErrorText>
</Field.Root>
<Flex w="100%"></Flex> {/* filler so that discipline dropdown sizes correctly */}
</Flex>
</Flex>
<Separator borderColor="gray.300"/>
{/* permission requests */}
<Flex direction="column" gap={2}>
<Text fontWeight="semibold" fontSize="lg">Permissions</Text>
<Flex direction="row" w="100%" align="center" justify="space-between" pl={2}>
<Text>Request access to IVANS</Text>
<Button variant="subtle" size="sm" onClick={ivansRequest}>IVANS Request</Button>
</Flex>
<Flex direction="row" w="100%" align="center" justify="space-between" pl={2}>
<Text>Request access to Remote Sensing</Text>
<Button variant="subtle" size="sm" onClick={ivansRequest}>Remote Sensing Request</Button>
</Flex>
</Flex>
<Separator borderColor="gray.300" />
{/* themes */}
<Flex direction="column">
<Text fontWeight="semibold" fontSize="lg">Theme</Text>
<Flex direction="row" align="center" justify="space-between" pl={2}>
<Text fontSize="md">Appearance</Text>
<NativeSelect.Root size="sm" w="10rem" variant="subtle">
<NativeSelect.Field
placeholder="Select option"
{...register("theme")}
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
>
<option value="Light">Light</option>
<option value="Dark">Dark</option>
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
</Flex>
</Flex>
</Flex>
{/* SAVE-ALL PREFERENCES BUTTON */}
{/* TODO: grey out button until changes are detected */}
<Button type="submit" w="150px" variant="subtle" borderRadius="lg" alignSelf="center">
Save Changes
</Button>
</Flex>
</form>
);
}
+234
View File
@@ -0,0 +1,234 @@
import {Flex, Separator, Text, Table, IconButton, NativeSelect, Button} from "@chakra-ui/react";
import { useMemo } from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { Check, X, PenLine } from 'lucide-react';
import VdapApi from "../../hooks/useVdapApi.js";
import useBuildUrl from "../../hooks/useBuildUrl.js";
export default function WebAdmin() {
// TODO: add below api endpoint
// const {data: users, isLoading, error} = VdapApi('/api/v2/vdap_web_users');
const users = useMemo(() => [
{
"username": "vkuchenik",
"firstName": "Venessa",
"lastName": "Kuchenik",
"permissions": {
"ivans": 2, // 2 => requested permission
"rs": 0, // 0 => no permissions and not requested
},
"team": "Computer Science",
"group": "Super User",
},
{
"username": "amack",
"firstName": "Adeiline",
"lastName": "Mack",
"permissions": {
"ivans": 2, // 0 => no permissions and not requested
"rs": 2, // 1 => has permission
},
"team": "Remote Sensing",
"group": "Member",
}
], []);
const {
register,
handleSubmit,
control,
errors,
formState: { isDirty }
} = useForm({
defaultValues: {users: users}
});
const { fields, append, remove } = useFieldArray({
control,
name: "users" // Matches the key in defaultValues
})
const grant = (user, category) => {
// 1. post data via api
// 2. fetch the new data via api again to force re-render
console.log(user.username, "was granted", category, "permission");
}
const deny = (user, category) => {
console.log(user.username, "was denied", category, "permission");
}
const users_req_ivans = useMemo(() => {
return users.filter(user => user.permissions.ivans === 2);
}, [users]);
const users_req_rs = useMemo(() => {
return users.filter(user => user.permissions.rs === 2);
}, [users]);
const onSubmit = (data, event) => {
event.preventDefault();
console.log(data);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Flex direction="column" w="100%" h="100%" p="15rem" pt="3rem" gap="3rem">
{/* CONTENT */}
<Flex direction="column" w="100%" gap={4}>
{/* Header */}
<Text fontSize="xl" fontWeight="bold">Web Admin Panel</Text>
<Separator borderColor="gray.300"/>
{/* Permission Requests */}
<Flex direction="column" w="100%" gap={2} pb={4}>
<Text fontSize="lg" fontWeight="bold">Incoming Permission Requests</Text>
<Flex direction="row" w="100%" gap={5}>
{/* ivans requests table */}
<Flex direction="column" w="100%" gap={2}>
<Text fontSize="sm" fontWeight="bold">IVANS</Text>
<Table.Root w="100%" size="sm" variant="outline" borderRadius="lg">
<Table.Header>
<Table.Row>
<Table.ColumnHeader w="100%" fontWeight="bold">User</Table.ColumnHeader>
<Table.ColumnHeader fontWeight="bold" textAlign="left">Permission</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{users_req_ivans.map((user) => (
<Table.Row key={user.username}>
<Table.Cell w="100%">{user.lastName}, {user.firstName}</Table.Cell>
<Table.Cell >
<Flex w="100%" gap={1} justify="left">
<IconButton borderRadius="xl" size="xs" variant="subtle" onClick={() => grant(user, "ivans")}>
<Check />
</IconButton>
<IconButton borderRadius="xl" size="xs" variant="subtle" onClick={() => deny(user, "ivans")}>
<X />
</IconButton>
</Flex>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Flex>
{/* rs requests table */}
<Flex direction="column" w="100%" gap={2}>
<Text fontSize="sm" fontWeight="bold">Remote Sensing</Text>
<Table.Root w="100%" size="sm" variant="outline" borderRadius="lg">
<Table.Header>
<Table.Row>
<Table.ColumnHeader w="100%" fontWeight="bold">User</Table.ColumnHeader>
<Table.ColumnHeader fontWeight="bold" textAlign="left">Permission</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{users_req_rs.map((user) => (
<Table.Row key={user.username}>
<Table.Cell>{user.lastName}, {user.firstName}</Table.Cell>
<Table.Cell>
<Flex w="100%" gap={1} justify="left">
<IconButton borderRadius="xl" size="xs" variant="subtle" onClick={() => grant(user, "rs")}>
<Check />
</IconButton>
<IconButton borderRadius="xl" size="xs" variant="subtle" onClick={() => deny(user, "rs")}>
<X />
</IconButton>
</Flex>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Flex>
</Flex>
</Flex>
<Separator borderColor="gray.300"/>
{/* Group Assignment */}
<Flex direction="column" w="100%" gap={2} pb={4}>
<Text fontWeight="bold" fontSize="lg">Groups</Text>
<Table.Root w="50%" size="sm" variant="outline" borderRadius="lg">
<Table.Header>
<Table.Row>
<Table.ColumnHeader fontWeight="bold">User</Table.ColumnHeader>
<Table.ColumnHeader fontWeight="bold" textAlign="left">Group</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{users.map((user, index) => (
<Table.Row key={user.username}>
<Table.Cell w="100%">{user.lastName}, {user.firstName}</Table.Cell>
<Table.Cell>
<Flex w="100%" justify="left">
<NativeSelect.Root size="sm" w="175px" variant="subtle">
<NativeSelect.Field
defaultValue={user.group}
{...register(`users.${index}.group`)}
>
<option value="Super User">Super User</option>
<option value="Member">Member</option>
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
</Flex>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Flex>
<Separator borderColor="gray.300"/>
{/* Team Assignment */}
<Flex direction="column" w="100%" gap={2} pb={4}>
<Text fontWeight="bold" fontSize="lg">Teams</Text>
<Table.Root w="50%" size="sm" variant="outline" borderRadius="lg">
<Table.Header>
<Table.Row>
<Table.ColumnHeader fontWeight="bold" w="100%">User</Table.ColumnHeader>
<Table.ColumnHeader fontWeight="bold" textAlign="left">Team</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{users.map((user, index) => {
console.log("user in team: ", user.team);
return(
<Table.Row key={user.username}>
<Table.Cell>
{user.lastName}, {user.firstName}
</Table.Cell>
<Table.Cell>
<Flex w="100%" justify="right">
<NativeSelect.Root size="sm" w="175px" variant="subtle">
<NativeSelect.Field
defaultValue={user.team}
{...register(`users.${index}.team`)}
>
<option value="Computer Science">Computer Science</option>
<option value="Remote Sensing">Remote Sensing</option>
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
</Flex>
</Table.Cell>
</Table.Row>
)})}
</Table.Body>
</Table.Root>
</Flex>
</Flex>
<Button type="submit" w="100px" variant="subtle" alignSelf="center">
Save
</Button>
</Flex>
</form>
)
}
+16 -1
View File
@@ -86,7 +86,22 @@ const useAuthStore = create((set) => ({
} catch (error) {
console.error("Failed to save layout to database", error);
}
}
},
// updateProfile: async (newProfile) => {
// try {
// set((state) => ({
// user: {
// ...state.user,
// username: newProfile.username,
// }
// }));
//
// await api.post('profile/update/', { profile: newProfile });
// } catch (error) {
// console.error("Failed to update profile", error);
// }
// }
}));
export default useAuthStore;